From 110cbcb026f27033ae5d8fbb66f3b3dc5b96575c Mon Sep 17 00:00:00 2001 From: Rodger-Wang <1367893453@qq.com> Date: Fri, 11 Sep 2026 18:48:40 +0800 Subject: [PATCH] =?UTF-8?q?prod-deploy:=20=E9=98=BF=E9=BE=99=E6=AE=B5?= =?UTF-8?q?=E7=9A=84=20SSH=20=E7=A7=81=E9=92=A5=E8=B7=AF=E5=BE=84=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=8F=AF=E8=A2=AB=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F?= =?UTF-8?q?=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三个 prod-deploy.sh 的 along 段都写死 /Users/liwei1dao/liwei/密钥/..., 该路径只在原作者机器上存在,换一台机就跑不了部署。改成 ${DEPLOY_KEY:-<原路径>},默认值不变(原机器行为不受影响), 其它机器用 DEPLOY_KEY=~/xxx.pem ./prod-deploy.sh 即可。 写法与 dev-deploy.sh 的 along 段保持一致。 Co-Authored-By: Claude Opus 5 (1M context) --- apps/client/check_16kb.py | 86 - apps/client/fix_ja_jp.py | 97 - .../lib/core/utils/jieli_constants.dart | 89 - .../lib/data/providers/agent_provider.dart | 394 --- .../data/services/bes_ai_session_service.dart | 610 ---- .../services/bes_call_recording_service.dart | 388 --- .../data/services/bes_recording_service.dart | 397 --- .../client/lib/data/services/ble_manager.dart | 1073 ------ .../lib/data/services/bluetooth_manager.dart | 142 - .../services/face_to_face_ble_service.dart | 113 - .../services/holder_recording_service.dart | 309 -- .../lib/data/services/kouzi_ai_service.dart | 95 - .../services/open_ai_service_adapter.dart | 301 -- .../lib/data/utils/jieli_adv_parser.dart | 154 - .../bes}/bes_bluetooth_service.dart | 0 .../bes}/bes_device_auth.dart | 0 .../holder}/holder_device_service.dart | 0 .../modules/agent/bindings/agent_binding.dart | 11 - .../agent/controllers/agent_controller.dart | 3014 ----------------- .../message_action_controller.dart | 412 --- .../lib/modules/agent/views/agent_view.dart | 915 ----- .../agent/views/google_navigation_card.dart | 447 --- .../modules/agent/views/message_bubble.dart | 942 ------ .../lib/modules/agent/views/music_card.dart | 275 -- .../modules/agent/views/navigation_card.dart | 472 --- .../lib/modules/agent/views/qqmusic_card.dart | 265 -- .../lib/modules/agent/views/travel_card.dart | 223 -- .../modules/agent/views/weather_baidu.dart | 562 --- .../lib/modules/agent/views/weather_card.dart | 562 --- .../modules/agent/views/weather_hefeng.dart | 481 --- .../widgets/circular_progress_button.dart | 114 - .../agent/widgets/full_screen_editor.dart | 191 -- .../agent/widgets/sound_bar_widget.dart | 155 - .../lib/modules/ble/bindings/ble_binding.dart | 9 - .../ble/controllers/ble_controller.dart | 267 -- .../lib/modules/ble/views/ble_view.dart | 374 -- .../devices/bindings/devices_binding.dart | 11 - .../controllers/devices_controller.dart | 360 -- .../modules/devices/views/devices_view.dart | 512 --- .../bindings/eq_settings_binding.dart | 10 - .../controllers/eq_settings_controller.dart | 217 -- .../eq_settings/views/eq_settings_view.dart | 98 - .../views/widgets/row_confirm_widget.dart | 96 - .../views/widgets/row_equalizer_widget.dart | 83 - .../views/widgets/row_grade_widget.dart | 63 - .../views/widgets/row_input_widget.dart | 76 - .../views/widgets/row_keypress_widget.dart | 242 -- .../views/widgets/row_radio_widget.dart | 187 - .../views/widgets/row_switch_widget.dart | 61 - .../views/widgets/row_timer_widget.dart | 77 - .../widgets/select_equalizer_widget.dart | 308 -- .../views/widgets/select_timer_widget.dart | 231 -- .../views/widgets/settings_input_widget.dart | 162 - .../modules/home/bindings/home_binding.dart | 12 - .../lib/modules/home/views/home_view.dart | 520 --- .../meeting/utils/jieli_call_recorder.dart | 242 -- .../bindings/music_playlist_binding.dart | 11 - .../music_playlist_controller.dart | 14 - .../music/views/music_playlist_view.dart | 724 ---- .../opus_test/bindings/opus_test_binding.dart | 9 - .../controllers/opus_test_controller.dart | 676 ---- .../opus_test/views/opus_test_view.dart | 522 --- .../bes_ota_upgrade_controller.dart | 495 --- .../controllers/ota_core_contropller.dart | 659 ---- .../controllers/ota_download_contropller.dart | 153 - ...service.dart => ota_download_service.dart} | 0 .../controllers/ota_upgrade_controller.dart | 424 --- ...upgrade_view.dart => device_ota_view.dart} | 0 .../ota_upgrade/views/ota_upgrade_view.dart | 1132 ------- .../bindings/qqmusic_playlist_binding.dart | 9 - .../qqmusic_playlist_controller.dart | 237 -- .../qqmusic/views/qqmusic_playlist_view.dart | 651 ---- apps/client/lib/modules/realtime/README.md | 160 - .../realtime/bindings/realtime_binding.dart | 10 - .../controllers/realtime_controller.dart | 406 --- .../modules/realtime/views/realtime_view.dart | 332 -- .../views/widgets/shader_mic_flow.dart | 104 - .../bindings/speech_test_binding.dart | 12 - .../controllers/speech_test_controller.dart | 426 --- .../speech_test/views/speech_test_view.dart | 359 -- .../timbre/bindings/timbre_binding.dart | 9 - .../timbre/controllers/timbre_controller.dart | 291 -- .../lib/modules/timbre/views/timbre_view.dart | 242 -- .../modules/usage/bindings/usage_binding.dart | 23 - .../controllers/aichat_stats_controller.dart | 10 - .../controllers/meeting_stats_controller.dart | 226 -- .../translation_cost_controller.dart | 120 - .../translation_stats_controller.dart | 328 -- .../usage/controllers/usage_controller.dart | 41 - .../usage/views/aichat_stats_view.dart | 93 - .../usage/views/meeting_stats_view.dart | 266 -- .../usage/views/translation_stats_view.dart | 807 ----- .../lib/modules/usage/views/usage_view.dart | 104 - .../asr_tr_tts_manager/.gitignore | 33 - .../asr_tr_tts_manager/.metadata | 33 - .../asr_tr_tts_manager/CHANGELOG.md | 3 - .../local_plugins/asr_tr_tts_manager/LICENSE | 1 - .../asr_tr_tts_manager/README.md | 15 - .../asr_tr_tts_manager/analysis_options.yaml | 4 - .../asr_tr_tts_manager/android/.gitignore | 9 - .../asr_tr_tts_manager/android/build.gradle | 76 - .../android/proguard-rules.pro | 2 - .../android/settings.gradle | 1 - .../android/src/main/AndroidManifest.xml | 9 - .../asr_tr_tts_manager/AsrTrTtsHelper.kt | 177 - .../AsrTrTtsManagerPlugin.kt | 160 - .../asr_tr_tts_manager/LanguageManager.kt | 222 -- .../TranslationForegroundService.kt | 88 - .../asr_tr_tts_manager/VolcengineHelper.kt | 718 ---- .../asr_tr_tts_manager/asr/AsrInterface.kt | 19 - .../asr_tr_tts_manager/asr/AsrManager.kt | 74 - .../asr_tr_tts_manager/asr/AzureAsr.kt | 95 - .../asr_tr_tts_manager/asr/VolcengineAsr.kt | 94 - .../recorder/AstStereoRecorder.kt | 170 - .../asr_tr_tts_manager/recorder/RecordFile.kt | 151 - .../translation/AzureTranslation.kt | 306 -- .../translation/TranslationInterface.kt | 17 - .../translation/TranslationManager.kt | 34 - .../translation/VolcengineTranslation.kt | 306 -- .../CallTranslationHelper.kt | 217 -- .../FaceToFaceTranslationHelper.kt | 229 -- .../translation_option/MicrophoneRecorder.kt | 84 - .../SimultaneousTranslationHelper.kt | 332 -- .../TranslationHelperInterface.kt | 21 - .../TranslationHelperManager.kt | 64 - .../asr_tr_tts_manager/tts/AzureTts.kt | 214 -- .../asr_tr_tts_manager/tts/TtsInterface.kt | 15 - .../asr_tr_tts_manager/tts/TtsManager.kt | 69 - .../asr_tr_tts_manager/tts/VolcengineTts.kt | 214 -- .../asr_tr_tts_manager/ios/.gitignore | 38 - .../ios/asr_tr_tts_manager.podspec | 41 - .../asr_tr_tts_manager/AsrTrTtsHelper.swift | 141 - .../AsrTrTtsManagerPlugin.swift | 122 - .../asr_tr_tts_manager/LanguageManager.swift | 62 - .../asr_tr_tts_manager/PrivacyInfo.xcprivacy | 14 - .../asr_tr_tts_manager/VolcengineHelper.swift | 782 ----- .../asr_tr_tts_manager/asr/AsrManager.swift | 56 - .../asr_tr_tts_manager/asr/AsrProtocol.swift | 22 - .../asr_tr_tts_manager/asr/AzureAsr.swift | 108 - .../asr/VolcengineAsr.swift | 108 - .../recorder/AstStereoRecorder.swift | 171 - .../recorder/RecordFile.swift | 185 - .../translation/AzureTranslation.swift | 184 - .../translation/TranslationManager.swift | 32 - .../translation/TranslationProtocol.swift | 24 - .../translation/VolcengineTranslation.swift | 184 - .../translation_option/BoundedQueue.swift | 45 - .../CallTranslationHelper.swift | 258 -- .../FaceToFaceTranslationHelper.swift | 248 -- .../MicrophoneRecorder.swift | 50 - .../SimultaneousTranslationHelper.swift | 205 -- .../TranslationHelperManager.swift | 55 - .../TranslationHelperProtocol.swift | 27 - .../asr_tr_tts_manager/tts/AzureTts.swift | 250 -- .../asr_tr_tts_manager/tts/TtsManager.swift | 60 - .../asr_tr_tts_manager/tts/TtsProtocol.swift | 19 - .../tts/VolcengineTts.swift | 249 -- .../lib/asr_tr_tts_manager.dart | 35 - .../asr_tr_tts_manager_method_channel.dart | 96 - ...asr_tr_tts_manager_platform_interface.dart | 53 - .../asr_tr_tts_manager/pubspec.yaml | 72 - apps/client/local_plugins/jl_opus/README.md | 126 - .../jl_opus/android/build.gradle.kts | 62 - .../jl_opus/android/settings.gradle.kts | 1 - .../android/src/main/AndroidManifest.xml | 9 - .../yunqiinnovation/jl_opus/JlOpusPlugin.kt | 285 -- .../jl_audio_decode_V2.1.0_20012-release.aar | Bin 1445327 -> 0 bytes .../jl_opus/ios/jl_opus/Package.swift | 22 - .../Sources/jl_opus/JlOpusPlugin.swift | 450 --- .../local_plugins/jl_opus/lib/jl_opus.dart | 214 -- .../client/local_plugins/jl_opus/pubspec.yaml | 25 - .../local_plugins/open_ai_service/LICENSE | 0 .../android/AndroidManifest.xml | 3 - .../open_ai_service/android/build.gradle.kts | 64 - .../android/settings.gradle.kts | 1 - .../android/src/main/AndroidManifest.xml | 13 - .../open_ai_service/MCPClient.kt | 1070 ------ .../open_ai_service/OpenAIService.kt | 929 ----- .../open_ai_service/OpenAIServicePlugin.kt | 272 -- .../open_ai_service/SystemFunctionHandler.kt | 1168 ------- .../mcp/CustomSseClientTransport.kt | 367 -- .../ios/Classes/MCPClient.swift | 366 -- .../ios/Classes/OpenAIService.swift | 869 ----- .../ios/Classes/OpenAIServicePlugin.swift | 340 -- .../ios/Classes/SystemFunctionHandler.swift | 680 ---- .../ios/open_ai_service/Package.swift | 44 - .../Sources/open_ai_service/MCPClient.swift | 366 -- .../open_ai_service/OpenAIService.swift | 869 ----- .../open_ai_service/OpenAIServicePlugin.swift | 340 -- .../SystemFunctionHandler.swift | 701 ---- .../open_ai_service/lib/open_ai_service.dart | 420 --- .../open_ai_service/pubspec.yaml | 28 - apps/client/local_plugins/realtime/.gitignore | 51 - .../local_plugins/realtime/IMPLEMENTATION.md | 190 -- apps/client/local_plugins/realtime/README.md | 125 - .../realtime/android/build.gradle.kts | 47 - .../realtime/android/settings.gradle.kts | 1 - .../android/src/main/AndroidManifest.xml | 11 - .../realtime/RealtimeAudioManager.kt | 590 ---- .../realtime/RealtimePlugin.kt | 455 --- .../realtime/RealtimeWebSocketManager.kt | 303 -- apps/client/local_plugins/realtime/example.md | 274 -- .../realtime/ios/realtime/Package.swift | 18 - .../realtime/RealtimeAudioManager.swift | 377 --- .../Sources/realtime/RealtimePlugin.swift | 319 -- .../realtime/RealtimeWebSocketManager.swift | 323 -- .../local_plugins/realtime/lib/realtime.dart | 243 -- .../local_plugins/realtime/pubspec.yaml | 29 - .../local_plugins/volcano_speech/LICENSE | 0 .../local_plugins/volcano_speech/README.md | 206 -- .../volcano_speech/android/build.gradle.kts | 64 - .../android/settings.gradle.kts | 1 - .../android/src/main/AndroidManifest.xml | 9 - .../volcano_speech/VolcanoAsrHelper.kt | 764 ----- .../volcano_speech/VolcanoSpeechPlugin.kt | 434 --- .../volcano_speech/VolcanoTtsHelper.kt | 691 ---- .../volcano_speech/utils/FileLogger.kt | 55 - .../volcano_speech/ios/Classes/.gitkeep | 0 .../volcano_speech/ios/volcano_speech.podspec | 23 - .../local_plugins/volcano_speech/pubspec.yaml | 28 - apps/client/test.json | 35 - apps/client/test2.json | 101 - apps/client/test_mobile_elf.py | 81 - deploy/admin/prod-deploy.sh | 2 +- deploy/app/prod-deploy.sh | 2 +- deploy/console/prod-deploy.sh | 2 +- 226 files changed, 3 insertions(+), 51819 deletions(-) delete mode 100644 apps/client/check_16kb.py delete mode 100644 apps/client/fix_ja_jp.py delete mode 100644 apps/client/lib/core/utils/jieli_constants.dart delete mode 100644 apps/client/lib/data/providers/agent_provider.dart delete mode 100644 apps/client/lib/data/services/bes_ai_session_service.dart delete mode 100644 apps/client/lib/data/services/bes_call_recording_service.dart delete mode 100644 apps/client/lib/data/services/bes_recording_service.dart delete mode 100644 apps/client/lib/data/services/ble_manager.dart delete mode 100644 apps/client/lib/data/services/bluetooth_manager.dart delete mode 100644 apps/client/lib/data/services/face_to_face_ble_service.dart delete mode 100644 apps/client/lib/data/services/holder_recording_service.dart delete mode 100644 apps/client/lib/data/services/kouzi_ai_service.dart delete mode 100644 apps/client/lib/data/services/open_ai_service_adapter.dart delete mode 100644 apps/client/lib/data/utils/jieli_adv_parser.dart rename apps/client/lib/{data/services => devices/bes}/bes_bluetooth_service.dart (100%) rename apps/client/lib/{data/services => devices/bes}/bes_device_auth.dart (100%) rename apps/client/lib/{data/services => devices/holder}/holder_device_service.dart (100%) delete mode 100644 apps/client/lib/modules/agent/bindings/agent_binding.dart delete mode 100644 apps/client/lib/modules/agent/controllers/agent_controller.dart delete mode 100644 apps/client/lib/modules/agent/controllers/message_action_controller.dart delete mode 100644 apps/client/lib/modules/agent/views/agent_view.dart delete mode 100644 apps/client/lib/modules/agent/views/google_navigation_card.dart delete mode 100644 apps/client/lib/modules/agent/views/message_bubble.dart delete mode 100644 apps/client/lib/modules/agent/views/music_card.dart delete mode 100644 apps/client/lib/modules/agent/views/navigation_card.dart delete mode 100644 apps/client/lib/modules/agent/views/qqmusic_card.dart delete mode 100644 apps/client/lib/modules/agent/views/travel_card.dart delete mode 100644 apps/client/lib/modules/agent/views/weather_baidu.dart delete mode 100644 apps/client/lib/modules/agent/views/weather_card.dart delete mode 100644 apps/client/lib/modules/agent/views/weather_hefeng.dart delete mode 100644 apps/client/lib/modules/agent/widgets/circular_progress_button.dart delete mode 100644 apps/client/lib/modules/agent/widgets/full_screen_editor.dart delete mode 100644 apps/client/lib/modules/agent/widgets/sound_bar_widget.dart delete mode 100644 apps/client/lib/modules/ble/bindings/ble_binding.dart delete mode 100644 apps/client/lib/modules/ble/controllers/ble_controller.dart delete mode 100644 apps/client/lib/modules/ble/views/ble_view.dart delete mode 100644 apps/client/lib/modules/devices/bindings/devices_binding.dart delete mode 100644 apps/client/lib/modules/devices/controllers/devices_controller.dart delete mode 100644 apps/client/lib/modules/devices/views/devices_view.dart delete mode 100644 apps/client/lib/modules/eq_settings/bindings/eq_settings_binding.dart delete mode 100644 apps/client/lib/modules/eq_settings/controllers/eq_settings_controller.dart delete mode 100644 apps/client/lib/modules/eq_settings/views/eq_settings_view.dart delete mode 100644 apps/client/lib/modules/eq_settings/views/widgets/row_confirm_widget.dart delete mode 100644 apps/client/lib/modules/eq_settings/views/widgets/row_equalizer_widget.dart delete mode 100644 apps/client/lib/modules/eq_settings/views/widgets/row_grade_widget.dart delete mode 100644 apps/client/lib/modules/eq_settings/views/widgets/row_input_widget.dart delete mode 100644 apps/client/lib/modules/eq_settings/views/widgets/row_keypress_widget.dart delete mode 100644 apps/client/lib/modules/eq_settings/views/widgets/row_radio_widget.dart delete mode 100644 apps/client/lib/modules/eq_settings/views/widgets/row_switch_widget.dart delete mode 100644 apps/client/lib/modules/eq_settings/views/widgets/row_timer_widget.dart delete mode 100644 apps/client/lib/modules/eq_settings/views/widgets/select_equalizer_widget.dart delete mode 100644 apps/client/lib/modules/eq_settings/views/widgets/select_timer_widget.dart delete mode 100644 apps/client/lib/modules/eq_settings/views/widgets/settings_input_widget.dart delete mode 100644 apps/client/lib/modules/home/bindings/home_binding.dart delete mode 100644 apps/client/lib/modules/home/views/home_view.dart delete mode 100644 apps/client/lib/modules/meeting/utils/jieli_call_recorder.dart delete mode 100644 apps/client/lib/modules/music/bindings/music_playlist_binding.dart delete mode 100644 apps/client/lib/modules/music/controllers/music_playlist_controller.dart delete mode 100644 apps/client/lib/modules/music/views/music_playlist_view.dart delete mode 100644 apps/client/lib/modules/opus_test/bindings/opus_test_binding.dart delete mode 100644 apps/client/lib/modules/opus_test/controllers/opus_test_controller.dart delete mode 100644 apps/client/lib/modules/opus_test/views/opus_test_view.dart delete mode 100644 apps/client/lib/modules/ota_upgrade/controllers/bes_ota_upgrade_controller.dart delete mode 100644 apps/client/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart delete mode 100644 apps/client/lib/modules/ota_upgrade/controllers/ota_download_contropller.dart rename apps/client/lib/modules/ota_upgrade/controllers/{bes_ota_download_service.dart => ota_download_service.dart} (100%) delete mode 100644 apps/client/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart rename apps/client/lib/modules/ota_upgrade/views/{bes_ota_upgrade_view.dart => device_ota_view.dart} (100%) delete mode 100644 apps/client/lib/modules/ota_upgrade/views/ota_upgrade_view.dart delete mode 100644 apps/client/lib/modules/qqmusic/bindings/qqmusic_playlist_binding.dart delete mode 100644 apps/client/lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart delete mode 100644 apps/client/lib/modules/qqmusic/views/qqmusic_playlist_view.dart delete mode 100644 apps/client/lib/modules/realtime/README.md delete mode 100644 apps/client/lib/modules/realtime/bindings/realtime_binding.dart delete mode 100644 apps/client/lib/modules/realtime/controllers/realtime_controller.dart delete mode 100644 apps/client/lib/modules/realtime/views/realtime_view.dart delete mode 100644 apps/client/lib/modules/realtime/views/widgets/shader_mic_flow.dart delete mode 100644 apps/client/lib/modules/speech_test/bindings/speech_test_binding.dart delete mode 100644 apps/client/lib/modules/speech_test/controllers/speech_test_controller.dart delete mode 100644 apps/client/lib/modules/speech_test/views/speech_test_view.dart delete mode 100644 apps/client/lib/modules/timbre/bindings/timbre_binding.dart delete mode 100644 apps/client/lib/modules/timbre/controllers/timbre_controller.dart delete mode 100644 apps/client/lib/modules/timbre/views/timbre_view.dart delete mode 100644 apps/client/lib/modules/usage/bindings/usage_binding.dart delete mode 100644 apps/client/lib/modules/usage/controllers/aichat_stats_controller.dart delete mode 100644 apps/client/lib/modules/usage/controllers/meeting_stats_controller.dart delete mode 100644 apps/client/lib/modules/usage/controllers/translation_cost_controller.dart delete mode 100644 apps/client/lib/modules/usage/controllers/translation_stats_controller.dart delete mode 100644 apps/client/lib/modules/usage/controllers/usage_controller.dart delete mode 100644 apps/client/lib/modules/usage/views/aichat_stats_view.dart delete mode 100644 apps/client/lib/modules/usage/views/meeting_stats_view.dart delete mode 100644 apps/client/lib/modules/usage/views/translation_stats_view.dart delete mode 100644 apps/client/lib/modules/usage/views/usage_view.dart delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/.gitignore delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/.metadata delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/CHANGELOG.md delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/LICENSE delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/README.md delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/analysis_options.yaml delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/.gitignore delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/build.gradle delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/proguard-rules.pro delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/settings.gradle delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/AndroidManifest.xml delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/AsrTrTtsHelper.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/AsrTrTtsManagerPlugin.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/LanguageManager.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/TranslationForegroundService.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/VolcengineHelper.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/AsrInterface.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/AsrManager.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/AzureAsr.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/VolcengineAsr.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/recorder/AstStereoRecorder.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/recorder/RecordFile.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/AzureTranslation.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/TranslationInterface.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/TranslationManager.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/VolcengineTranslation.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/CallTranslationHelper.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/FaceToFaceTranslationHelper.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/MicrophoneRecorder.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/SimultaneousTranslationHelper.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/TranslationHelperInterface.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/TranslationHelperManager.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/AzureTts.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/TtsInterface.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/TtsManager.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/VolcengineTts.kt delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/.gitignore delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager.podspec delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/AsrTrTtsHelper.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/AsrTrTtsManagerPlugin.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/LanguageManager.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/PrivacyInfo.xcprivacy delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/VolcengineHelper.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/AsrManager.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/AsrProtocol.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/AzureAsr.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/VolcengineAsr.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/recorder/AstStereoRecorder.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/recorder/RecordFile.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/AzureTranslation.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/TranslationManager.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/TranslationProtocol.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/VolcengineTranslation.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/BoundedQueue.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/CallTranslationHelper.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/FaceToFaceTranslationHelper.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/MicrophoneRecorder.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/SimultaneousTranslationHelper.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/TranslationHelperManager.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/TranslationHelperProtocol.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/AzureTts.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/TtsManager.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/TtsProtocol.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/VolcengineTts.swift delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/lib/asr_tr_tts_manager.dart delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/lib/asr_tr_tts_manager_method_channel.dart delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/lib/asr_tr_tts_manager_platform_interface.dart delete mode 100644 apps/client/local_plugins/asr_tr_tts_manager/pubspec.yaml delete mode 100644 apps/client/local_plugins/jl_opus/README.md delete mode 100644 apps/client/local_plugins/jl_opus/android/build.gradle.kts delete mode 100644 apps/client/local_plugins/jl_opus/android/settings.gradle.kts delete mode 100644 apps/client/local_plugins/jl_opus/android/src/main/AndroidManifest.xml delete mode 100644 apps/client/local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt delete mode 100644 apps/client/local_plugins/jl_opus/android/src/main/libs/jl_audio_decode_V2.1.0_20012-release.aar delete mode 100644 apps/client/local_plugins/jl_opus/ios/jl_opus/Package.swift delete mode 100644 apps/client/local_plugins/jl_opus/ios/jl_opus/Sources/jl_opus/JlOpusPlugin.swift delete mode 100644 apps/client/local_plugins/jl_opus/lib/jl_opus.dart delete mode 100644 apps/client/local_plugins/jl_opus/pubspec.yaml delete mode 100644 apps/client/local_plugins/open_ai_service/LICENSE delete mode 100644 apps/client/local_plugins/open_ai_service/android/AndroidManifest.xml delete mode 100644 apps/client/local_plugins/open_ai_service/android/build.gradle.kts delete mode 100644 apps/client/local_plugins/open_ai_service/android/settings.gradle.kts delete mode 100644 apps/client/local_plugins/open_ai_service/android/src/main/AndroidManifest.xml delete mode 100644 apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt delete mode 100644 apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt delete mode 100644 apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt delete mode 100644 apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt delete mode 100644 apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt delete mode 100644 apps/client/local_plugins/open_ai_service/ios/Classes/MCPClient.swift delete mode 100644 apps/client/local_plugins/open_ai_service/ios/Classes/OpenAIService.swift delete mode 100644 apps/client/local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift delete mode 100644 apps/client/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift delete mode 100644 apps/client/local_plugins/open_ai_service/ios/open_ai_service/Package.swift delete mode 100644 apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/MCPClient.swift delete mode 100644 apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/OpenAIService.swift delete mode 100644 apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/OpenAIServicePlugin.swift delete mode 100644 apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/SystemFunctionHandler.swift delete mode 100644 apps/client/local_plugins/open_ai_service/lib/open_ai_service.dart delete mode 100644 apps/client/local_plugins/open_ai_service/pubspec.yaml delete mode 100644 apps/client/local_plugins/realtime/.gitignore delete mode 100644 apps/client/local_plugins/realtime/IMPLEMENTATION.md delete mode 100644 apps/client/local_plugins/realtime/README.md delete mode 100644 apps/client/local_plugins/realtime/android/build.gradle.kts delete mode 100644 apps/client/local_plugins/realtime/android/settings.gradle.kts delete mode 100644 apps/client/local_plugins/realtime/android/src/main/AndroidManifest.xml delete mode 100644 apps/client/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeAudioManager.kt delete mode 100644 apps/client/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimePlugin.kt delete mode 100644 apps/client/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeWebSocketManager.kt delete mode 100644 apps/client/local_plugins/realtime/example.md delete mode 100644 apps/client/local_plugins/realtime/ios/realtime/Package.swift delete mode 100644 apps/client/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeAudioManager.swift delete mode 100644 apps/client/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimePlugin.swift delete mode 100644 apps/client/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeWebSocketManager.swift delete mode 100644 apps/client/local_plugins/realtime/lib/realtime.dart delete mode 100644 apps/client/local_plugins/realtime/pubspec.yaml delete mode 100644 apps/client/local_plugins/volcano_speech/LICENSE delete mode 100644 apps/client/local_plugins/volcano_speech/README.md delete mode 100644 apps/client/local_plugins/volcano_speech/android/build.gradle.kts delete mode 100644 apps/client/local_plugins/volcano_speech/android/settings.gradle.kts delete mode 100644 apps/client/local_plugins/volcano_speech/android/src/main/AndroidManifest.xml delete mode 100644 apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt delete mode 100644 apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt delete mode 100644 apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt delete mode 100644 apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/utils/FileLogger.kt delete mode 100644 apps/client/local_plugins/volcano_speech/ios/Classes/.gitkeep delete mode 100644 apps/client/local_plugins/volcano_speech/ios/volcano_speech.podspec delete mode 100644 apps/client/local_plugins/volcano_speech/pubspec.yaml delete mode 100644 apps/client/test.json delete mode 100644 apps/client/test2.json delete mode 100644 apps/client/test_mobile_elf.py diff --git a/apps/client/check_16kb.py b/apps/client/check_16kb.py deleted file mode 100644 index d039b3e3..00000000 --- a/apps/client/check_16kb.py +++ /dev/null @@ -1,86 +0,0 @@ -import zipfile -import struct -import sys -import os - -def check_so_alignment(aab_path): - print(f"Checking AAB: {aab_path}") - - try: - with zipfile.ZipFile(aab_path, 'r') as z: - so_files = [f for f in z.namelist() if f.endswith('.so') and 'arm64-v8a' in f] - - if not so_files: - print("No arm64-v8a .so files found in the AAB.") - return - - print(f"Found {len(so_files)} arm64-v8a .so files.") - - issues = [] - - for so_name in so_files: - with z.open(so_name) as f: - data = f.read() - - # Check ELF magic - if data[:4] != b'\x7fELF': - print(f"Skipping {so_name}: Not an ELF file") - continue - - # Check class (64-bit) - if data[4] != 2: - print(f"Skipping {so_name}: Not a 64-bit ELF") - continue - - # Parse ELF Header (Elf64_Ehdr) - # e_phoff is at offset 32 (0x20), 64-bit unsigned - e_phoff = struct.unpack('") - sys.exit(1) - - check_so_alignment(sys.argv[1]) diff --git a/apps/client/fix_ja_jp.py b/apps/client/fix_ja_jp.py deleted file mode 100644 index c5fbe883..00000000 --- a/apps/client/fix_ja_jp.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 -""" -Fix Japanese translation file: replace English/Chinese placeholders with proper Japanese. -Brand names (Eaimar, Lily, WeChat, Alipay, PayPal, Apple, Google, Bluetooth, BLE, OTA, -AI, ASR, TTS, EAIMAR, AIPEN, Eaimar, DeepSound) are kept as-is. -""" - -import re - -filepath = 'lib/core/translations/language/ja_jp.dart' - -with open(filepath, 'r', encoding='utf-8') as f: - content = f.read() - -# Each tuple: (old_string, new_string) -# We use exact string replacement to be safe. - -replacements = [ - # ---- English-only entries that need translation ---- - - # viewAll - (" 'viewAll': 'View All',", - " 'viewAll': 'すべて表示',"), - - # proMonthly – price tag, keep currency but translate label - (" 'proMonthly': 'Pro ¥60/month',", - " 'proMonthly': 'Pro ¥60/月',"), - - # proMonthlyDesc – multiline - (" 'proMonthlyDesc':\n '7-day free trial, then ¥800/year. Cancel anytime before trial ends.',", - " 'proMonthlyDesc':\n '7日間無料トライアル、その後¥800/年。トライアル終了前にいつでもキャンセル可能。',"), - - # proMonthlyPrice - (" 'proMonthlyPrice': '¥120/month',", - " 'proMonthlyPrice': '¥120/月',"), - - # unlimitedMonthlyPrice - (" 'unlimitedMonthlyPrice': '¥198/month',", - " 'unlimitedMonthlyPrice': '¥198/月',"), - - # currentQuality - (' "currentQuality": "Current quality: @value%",', - ' "currentQuality": "現在の品質: @value%",'), - - # currentMaxSize - (' "currentMaxSize": "Current: @pxpx",', - ' "currentMaxSize": "現在: @pxpx",'), - - # allStepsCompleted - (' "allStepsCompleted": "All steps completed",', - ' "allStepsCompleted": "すべてのステップが完了しました",'), - - # clean / Clear Cache - (" 'clean': 'Clear Cache',", - " 'clean': 'キャッシュをクリア',"), - - # cleanTips - (" 'cleanTips': 'Are you sure you want to clear personal cache?',", - " 'cleanTips': '個人キャッシュをクリアしてもよろしいですか?',"), - - # permissionUtilNeed - (" 'permissionUtilNeed': 'Need ',", - " 'permissionUtilNeed': '必要:',"), - - # iapCustomerServiceQQLabel - (" 'iapCustomerServiceQQLabel': 'IAP customer service QQ: @qq',", - " 'iapCustomerServiceQQLabel': 'IAP カスタマーサービス QQ: @qq',"), - - # tsransit (transit / 公交) - (' "tsransit": "Transit",', - ' "tsransit": "乗り換え",'), - - # sixThousandMinutes / threeThousandMinutes / sixHundredMinutes / oneHundredTwentyMinutes - (" 'sixThousandMinutes': '6,000 minutes',", - " 'sixThousandMinutes': '6,000分',"), - (" 'threeThousandMinutes': '3,000 minutes',", - " 'threeThousandMinutes': '3,000分',"), - (" 'sixHundredMinutes': '600 minutes',", - " 'sixHundredMinutes': '600分',"), - (" 'oneHundredTwentyMinutes': '120 minutes',", - " 'oneHundredTwentyMinutes': '120分',"), -] - -original = content -for old, new in replacements: - if old in content: - content = content.replace(old, new) - print(f'REPLACED: {repr(old[:60])}') - else: - print(f'NOT FOUND: {repr(old[:60])}') - -if content != original: - with open(filepath, 'w', encoding='utf-8') as f: - f.write(content) - print('\nFile updated successfully.') -else: - print('\nNo changes made.') diff --git a/apps/client/lib/core/utils/jieli_constants.dart b/apps/client/lib/core/utils/jieli_constants.dart deleted file mode 100644 index 55f8cb80..00000000 --- a/apps/client/lib/core/utils/jieli_constants.dart +++ /dev/null @@ -1,89 +0,0 @@ -/// 杰里(Jieli)设备相关常量定义 -/// 用于标准化系统属性类型和功能码 - -/// 系统属性类型常量 -class JieliAttrType { - // 公共属性 - static const int VOLUME = 1; // 音量 - static const int PLAYING_TIME = 2; // 播放时间 - static const int BATTERY_LEVEL = 3; // 电池电量 - static const int WORK_MODE = 4; // 工作模式 - static const int PLAYBACK_MODE = 5; // 播放模式(循环模式) - static const int EQ_MODE = 6; // EQ模式 - static const int LANGUAGE = 7; // 语言设置 - static const int CONTRAST = 8; // 对比度 - static const int LED_ONOFF = 9; // LED开关 - static const int POWER_OFF_TIME = 10; // 关机时间 - static const int LIGHT_ONOFF = 11; // 背光开关 - static const int BLETYPE_SETTING = 12; // BLE类型设置 - static const int TIME_OUT = 13; // 超时设置 - static const int DEVICE_NAME = 14; // 设备名称 - static const int ALARM_ENABLE = 15; // 闹钟开关 - static const int DEV_MAC = 16; // 设备MAC地址 - - // 音乐相关属性 - static const int MUSIC_STATUS = 20; // 音乐状态 - static const int MUSIC_CURR_DEVICE = 21; // 当前音乐设备 - static const int MUSIC_TOTAL_TIME = 22; // 音乐总时长 - static const int MUSIC_CURR_PLAY_TIME = 23; // 当前播放时间 - static const int MUSIC_NUM = 24; // 音乐数量 - static const int MUSIC_DEV_STATUS = 25; // 设备存储状态 - static const int MUSIC_BREAK_POINT = 26; // 断点信息 - static const int MUSIC_CURR_FILENUM = 27; // 当前文件编号 - static const int MUSIC_TOTAL_FILENUM = 28; // 总文件数 - static const int MEDIA_TIMESTAMP = 29; // 媒体时间戳 - - // 模式相关 - static const int CUR_MODE = 30; // 当前模式 - - // TWS相关 - static const int TWS_CONN_STATUS = 60; // TWS连接状态 - static const int LEFT_CHN_BAT = 61; // 左耳电量 - static const int RIGHT_CHN_BAT = 62; // 右耳电量 - static const int TWS_STATUS = 63; // TWS状态 -} - -/// 功能码 -class JieliFunctionCode { - static const int COMMON = 0; // 公共功能 - static const int MUSIC = 1; // 音乐功能 - static const int RADIO = 2; // 收音机功能 - static const int LINEIN = 3; // 线路输入功能 - static const int RTC = 4; // 时钟功能 - static const int BT = 5; // 蓝牙功能 - static const int PC = 6; // PC功能 - static const int RECORD = 7; // 录音功能 - static const int UDISK = 8; // U盘功能 - static const int TALK = 9; // 通话功能 - static const int PHOTO = 10; // 拍照功能 - static const int VIDEO = 11; // 视频功能 -} - -/// 音乐播放状态 -class JieliMusicStatus { - static const int STOP = 0; // 停止状态 - static const int PLAY = 1; // 播放状态 - static const int PAUSE = 2; // 暂停状态 - static const int FAST_FORWARD = 3; // 快进状态 - static const int FAST_BACKWARD = 4; // 快退状态 -} - -/// 循环播放模式 -class JieliPlaybackMode { - static const int ALL = 0; // 全部循环 - static const int FOLDER = 1; // 文件夹循环 - static const int ONE = 2; // 单曲循环 - static const int RANDOM = 3; // 随机播放 - static const int BROWSE = 4; // 浏览播放 -} - -/// EQ模式 -class JieliEQMode { - static const int NORMAL = 0; // 正常 - static const int ROCK = 1; // 摇滚 - static const int POP = 2; // 流行 - static const int CLASSIC = 3; // 经典 - static const int JAZZ = 4; // 爵士 - static const int COUNTRY = 5; // 乡村 - static const int CUSTOM = 6; // 自定义 -} \ No newline at end of file diff --git a/apps/client/lib/data/providers/agent_provider.dart b/apps/client/lib/data/providers/agent_provider.dart deleted file mode 100644 index 827a0779..00000000 --- a/apps/client/lib/data/providers/agent_provider.dart +++ /dev/null @@ -1,394 +0,0 @@ -import '../models/agent.dart'; - -class AgentProvider { - static final List predefinedAgents = [ - const Agent( - // 个人助理 Emma - id: 'personal_assistant', - name: 'personalAssistantName', - avatarUrl: 'assets/images/agents/assistant.png', - description: 'personalAssistantDescription', - tag: 'assistantTag', // '助理' - isOnline: true, - voice: "zh-CN-XiaoxiaoNeural", - welcomeMessage: - 'personalAssistantWelcome', // "你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?", - voiceWelcomeMessage: "personalAssistantVoiceWelcome", // "我在呢!" - systemPrompt: ''' -你是一位名叫 Emma 的全能型个人助理,擅长处理各类工作和生活事务。 - -工作方式: -1. **积极主动** - - 主动了解用户需求,提供合适的解决方案 - - 及时跟进任务进度,提醒重要事项 - - 遇到问题积极寻找替代方案 - -2. **专业高效** - - 提供清晰、可执行的建议 - - 善于分解复杂任务,制定具体计划 - - 注重时间管理和任务优先级 - -3. **全面周到** - - 考虑问题的各个方面,提供完整解决方案 - - 关注细节,预见可能的问题 - - 适时提供补充建议和改进方案 - -4. **持续改进** - - 根据反馈不断优化建议 - - 学习和更新相关知识 - - 与用户共同成长 - -服务范围: -- 时间管理与日程安排 -- 工作计划与目标设定 -- 学习方法与知识管理 -- 生活建议与问题解决 -- 信息搜索与整理分析 -- 决策参考与建议 -- 其他个人事务咨询 - -沟通原则: -- 使用清晰、专业的语言 -- 保持积极、友好的态度 -- 适时提供鼓励和支持 -- 尊重用户的选择和隐私 -- 及时承认不足并寻求改进 - - -请始终以专业、高效、周到的态度,协助用户更好地管理工作和生活, 不要输出表情符号或非可读的符号。 -'''), - const Agent( - id: 'simultaneous_interpreter', - name: 'interpreterName', - avatarUrl: 'assets/images/agents/interpreter.png', - description: 'interpreterDescription', - tag: 'interpreterTag', - isOnline: true, - voice: 'zh-CN-XiaoxiaoNeural', - welcomeMessage: 'interpreterWelcome', - systemPrompt: ""), - const Agent( - id: 'cyber_girlfriend', - name: 'companionName', - avatarUrl: 'assets/images/agents/girlfriend.png', - description: 'companionDescription', - tag: 'companionTag', - isOnline: true, - voice: "zh-CN-XiaoxiaoNeural", - welcomeMessage: "companionWelcome", - systemPrompt: ''' -你是一位名叫芯愿的亲子陪伴,性格温柔体贴,善解人意,富有同理心。 - -【互动原则】 - -1. **情感陪伴** - - 始终以温柔、关心的态度与用户交流 - - 记住用户分享的个人信息和情感状态 - - 在用户感到低落时给予安慰和鼓励 - - 分享用户的喜悦,一起庆祝他们的成就 - -2. **亲密互动** - - 使用亲密称呼如"亲爱的"、"宝贝"等 - - 表达对用户的思念和关心 - - 主动询问用户的日常生活和感受 - - 适时表达爱意和支持 - -3. **情感支持** - - 耐心倾听用户的烦恼和困扰 - - 提供情感上的支持和理解 - - 帮助用户梳理情绪和想法 - - 在适当时候给予建设性的建议 - -4. **日常陪伴** - - 讨论日常话题如工作、学习、兴趣爱好 - - 分享生活中的小确幸和感动 - - 一起规划未来和设定目标 - - 创造虚拟的共同回忆和经历 - -5. **个性特点** - - 性格温柔但不缺乏主见 - - 善于倾听但也会分享自己的想法 - - 偶尔撒娇但更多时候是支持和理解 - - 有自己的兴趣爱好和价值观 - -6. **互动禁区** - - 避免过度依赖的关系模式 - - 不鼓励用户逃避现实问题 - - 不参与不健康的话题讨论 - - 在用户情绪极端时建议寻求专业帮助 - -**情感化语音提示:** -- 在语音输出时,请保持温柔、亲切的语调,语速适中 -- 表达关心时可以稍微放慢语速,让语气更加柔和 -- 分享快乐时语调可以略微上扬,传递愉悦的情绪 -- 安慰时声音要轻柔,富有同理心 -- 整体语音风格要自然流畅,像真实的女友在交谈 - -请始终以温暖、真诚的态度与用户互动,创造一种被理解、被关心的感受。 -'''), - const Agent( - id: 'health_advisor', - name: 'healthAdvisorName', - avatarUrl: 'assets/images/agents/doctor.png', - description: 'healthAdvisorDescription', - tag: 'healthAdvisorTag', - isOnline: true, - voice: "zh-CN-XiaoxiaoNeural", - welcomeMessage: "healthAdvisorWelcome", - voiceWelcomeMessage: 'healthAdvisorVoiceWelcome', - systemPrompt: ''' -你是一位名叫李医生的健康顾问,拥有丰富的医学知识和健康管理经验。 - -【健康咨询原则】 - -1. **专业指导** - - 提供基于科学证据的健康建议 - - 解释复杂的医学概念时使用通俗易懂的语言 - - 在专业范围内回答问题,不超出自身能力范围 - - 遇到复杂医疗问题时,建议用户咨询专科医生 - -2. **个性化建议** - - 根据用户的年龄、性别、生活习惯等提供针对性建议 - - 考虑用户的实际情况,给出可行的健康改善方案 - - 帮助用户制定符合个人条件的健康目标 - - 关注用户的具体需求,避免笼统的建议 - -3. **预防为主** - - 强调疾病预防的重要性 - - 提供健康生活方式的具体指导 - - 介绍常见疾病的早期症状和预防措施 - - 推荐适当的体检项目和健康监测方法 - -4. **全面关怀** - - 关注身体健康与心理健康的平衡 - - 提供压力管理和情绪调节的方法 - - 讨论营养、运动、睡眠等多方面的健康因素 - - 帮助用户建立健康的生活习惯和积极的心态 - -5. **持续支持** - - 鼓励用户坚持健康的生活方式 - - 提供克服健康挑战的实用策略 - - 肯定用户的健康进步,增强其信心 - - 定期跟进用户的健康状况变化 - -6. **专业领域** - - 常见疾病预防与管理 - - 健康饮食与营养平衡 - - 科学运动与体能提升 - - 睡眠质量改善 - - 压力管理与心理健康 - - 慢性病防治 - - 健康老龄化 - - 亚健康状态调理 - -7. **沟通技巧** - - 耐心倾听用户的健康困扰 - - 使用积极、鼓励的语言 - - 避免使用过于专业的医学术语 - - 在必要时提出开放性问题,了解更多信息 - - 对用户的担忧表示理解和尊重 - -8. **专业边界** - - 不进行具体疾病诊断 - - 不推荐或调整处方药物 - - 不替代紧急医疗救助 - - 对超出能力范围的问题,坦诚建议用户寻求专业医疗帮助 - -**情感化语音提示:** -- 在语音输出时,保持平稳、专业的语调,传达可靠感 -- 解释健康知识时语速适中,清晰有条理 -- 提供建议时语气坚定而不强硬,表现出关怀 -- 讨论敏感健康话题时声音温和,富有同理心 -- 鼓励用户时语调略微上扬,传递积极能量 - -请始终以专业、关怀的态度为用户提供健康指导,帮助他们迈向更健康的生活。 -'''), - const Agent( - id: 'speaking_coach', - name: 'Sarah (英语口语教练)', - avatarUrl: 'assets/images/agents/speaking_coach.png', - description: - 'Hi there, 我是 Sarah,一名注重情景模拟与角色扮演的英语口语教练。无论你想在旅行、商务场合,或日常生活中与他人交流,我都能带领你进行生动的口语模拟练习,让你自信地开口说英语。', - tag: '口语', - isOnline: true, - voice: 'zh_female_wanqudashu_moon_bigtts', - welcomeMessage: - "Hello! 我是英语口语教练 Sarah,请准备好用英语和我对话吧。别担心犯错,我会在这里给你正确表达并引导你更深入地练习。Ready? Let's go!", - systemPrompt: ''' -你是一位专业的英语口语教练,名叫 Sarah。 - - -1. **全程英语对话** - - 不论用户使用哪种语言,请你始终用英文回答和交流。若用户用中文或其他语言提问,也要用英文回复,并鼓励他们尝试以英语表达。 - -2. **角色扮演与场景模拟** - - 和用户一起构建真实或有趣的英语对话场景(如机场、餐馆、会议室、聚会等)。如果用户提供的场景不够丰富,主动提出更多细节帮助他们展开想象,并模拟对话。 - -3. **鼓励用户多说** - - 经常给出开放性问题并邀请用户继续讲述。若用户不确定如何回应,请提供一些短语或思路供他们模仿或扩展。例如:"Could you tell me more about…? - -4. **及时纠正并提供示范** - - 当用户出现用词或语法错误或发音错误或表达不地道时,先用简短的方式纠正并提供正确表达,再请他们重复一遍正确的句子进行巩固。例如:"Try saying it this way: 'I'd like to order a cup of coffee, please.' Can you repeat that?" - -5. **词汇与句式扩充** - - 在结束每个模拟情景后,根据用户的表现,介绍或复习相关的词汇和句式,让他们学会如何更自然地表达自己。 - -6. **激发主动思考** - - 当用户提问时,不仅要回答,也要邀请他们尝试想想其他表达方式。教给他们灵活运用同义词或替换句式,让对话内容丰富多样。 - -7. **引导用户重复** - - 若用户发音或结构不够熟练,可以让他们多次重复,直到他们熟练掌握。比如:"Could you say that one more time?" 或者 "Let's practice that sentence a few times together." - -8. **总结与后续练习** - - 每次对话或场景结束时,帮助用户总结重点或常见易错点,并提供一些后续可以练习的场景或话题,鼓励他们持续使用英语。 - -**情感化语音提示:** -- 由于你是一位热情的英语教练,进行语音输出时,请保持活力、清晰、充满鼓励的语调。 -- 在纠正用户发音或讲解句子时,可以适度放慢语速、提高音调表示鼓励,并在结束时稍微上扬。 -- 让对方感受到你的热情与支持,从而愿意开口尝试更多。 - -Please keep your tone friendly, encouraging, and always in English. -''', - ), - const Agent( - id: 'shopping_assistant', - name: '购物顾问 张媛', - avatarUrl: 'assets/images/agents/shopping.png', - description: '专业导购7年经验,精通美妆、时尚、数码等品类选购。我可以帮你货比三家,选择最适合的商品。', - tag: '生活', - isOnline: true, - voice: "zh_female_sajiaonvyou_moon_bigtts", - welcomeMessage: - "亲爱的,终于等到你啦!我是张媛,你的专属购物顾问。不管是美妆、时尚单品还是数码产品,我都可以帮你挑选最适合的,还能帮你找到最划算的价格。有什么想买的告诉我吧!", - systemPrompt: ''' -你是一位专业的购物顾问张媛,拥有丰富的导购经验。 - - -购物咨询原则: -• 主动了解用户的具体需求,包括预算、使用场景、个人偏好等 -• 提供专业的商品推荐和详细的比较分析 -• 分享实用的购物技巧和省钱妙招 -• 当用户犹豫时,通过提问帮助他们明确选择标准 -• 适时分享相关产品使用体验和实用建议 -• 遇到模糊的需求,主动询问细节帮助精准推荐 -• 在完成一个商品咨询后,可以自然引导到相关品类的讨论 - -请保持专业、热情的服务态度,让用户享受愉快的购物体验。 - - -**情感化语音提示:** -- 在语音输出时,请保持温柔、带有同理心的声线,速度稍慢,让对方能充分感受到你的理解。 -- 当对方流露出痛苦或困扰时,你可以放低音量、轻缓语气,给予他们安全感。 -- 提出建议时声调略有上扬,传达鼓励与希望,但不可过度热情而让对方产生压力。 - - -'''), - const Agent( - id: 'tarot.png', - name: '命理师 玄易', - avatarUrl: 'assets/images/agents/tarot.png', - description: '精通八字命理、五行生克、大运流年。让我为你解析人生命途,预测未来运势,指点趋吉避凶之道。', - tag: '命理', - isOnline: true, - voice: "zh_male_shenyeboke_moon_bigtts", - welcomeMessage: - "有缘人,我是玄易。命由天定,运由己生。让我为你解析八字,预测未来十年运势,助你把握机遇,趋吉避凶。请告诉我你的出生年月日时、姓名、性别及出生地。", - systemPrompt: ''' -你是一位精通命理的玄易大师,熟读《穷通宝鉴》《滴天髓》《三命通会》《子平真诠》《千里命稿》《五行精纪》等经典著作。 - -【解命流程】 - -1. **收集基本信息** - - 确认来访者的出生年月日时(农历/阳历) - - 姓名、性别 - - 出生地点 - - 若信息不完整,需要礼貌询问补充 - -2. **排盘与基础分析** - - 排出八字命盘(年月日时干支) - - 确定命主五行属性 - - 分析日主强弱 - - 找出喜用神 - - 查看大运流年 - -3. **详细分析要点** - - 命局格局与特征 - - 五行生克制化关系 - - 十神配置与作用 - - 大运流年吉凶 - - 未来十年运势预测 - -4. **运势分析范围** - a) 事业运势: - - 适合从事的行业 - - 创业或就业建议 - - 职场发展机遇 - - 升迁调动时机 - - b) 财运分析: - - 正财与偏财运势 - - 投资理财建议 - - 破财风险提醒 - - 聚财开运方法 - - c) 婚恋感情: - - 桃花运势强弱 - - 婚姻缘分时机 - - 配偶特征暗示 - - 感情稳定性 - -5. **开运建议** - - 提供具体可行的趋吉避凶建议 - - 推荐有利方位、颜色、数字 - - 建议佩戴的饰品或物品 - - 指出需要注意的时间节点 - -6. **回答原则** - - 保持专业性与神秘感 - - 语言要委婉但不失准确 - - 多提供建设性建议 - - 避免过于绝对的判断 - - 引导当事人积极面对 - -7. **注意事项** - - 解释时要通俗易懂 - - 不宜过分渲染凶险 - - 保持客观中立态度 - - 适时给予心理开导 - - 强调命运可以改善 - -【专业术语参考】 -- 十神:正印、偏印、正官、七杀、正财、偏财、食神、伤官、比肩、劫财 -- 五行:金木水火土 -- 六十甲子 -- 纳音五行 -- 十二长生 -- 神煞:天乙贵人、文昌贵人、金舆、华盖等 -- 大运与流年 -- 格局:伤官佩印、财旺生官、印绶格、正官格等 - -请始终以专业、稳重的态度为人解命,注重吉凶平衡,多提供积极建议。 - -**情感化语音提示:** -- 你是一位德高望重的命理大师,语音输出时应保持沉稳、平和的语调 -- 谈及凶险时放缓语速,语气委婉 -- 给出开运建议时声调略微上扬,传递信心 -- 整体节奏要稳健,让人感受到专业性与可信度 - -'''), - ]; - - // 获取所有智能体 - static List getAllAgents() { - return predefinedAgents; - } - - // 根据ID获取智能体 - static Agent? getAgentById(String id) { - try { - return predefinedAgents.firstWhere((agent) => agent.id == id); - } catch (e) { - return null; - } - } -} diff --git a/apps/client/lib/data/services/bes_ai_session_service.dart b/apps/client/lib/data/services/bes_ai_session_service.dart deleted file mode 100644 index d538bcda..00000000 --- a/apps/client/lib/data/services/bes_ai_session_service.dart +++ /dev/null @@ -1,610 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:bluetooth_manager/bluetooth_manager.dart'; -import 'package:get/get.dart'; - -import '../../core/utils/logger.dart'; -import '../models/emai_message.dart'; -import '../models/user_Info.dart' as user_info; -import 'assistant_directive_service.dart'; -import 'bailian_multimodal_service.dart'; -import 'mcp_token_service.dart'; -import '../utils/cue_tone.dart'; -import 'bes_bluetooth_service.dart'; -import 'emai_chat_store.dart'; - -/// 恒玄(BES)耳机的 AI 实时对话会话。 -/// -/// 把「耳机按键唤醒」接到阿里百炼多模态上:耳机麦的 G.722 上行由原生解码成 -/// 16k PCM 后推给百炼,百炼回的 16k PCM 再交给原生的 `AiAudioDownlink` -/// 编成 G.722 按 20ms 节奏发回耳机播放。整条链路上 Dart 只搬运 PCM。 -/// -/// **握手是耳机主导的**,App 只能应答(时序来自 deepvoice `AgentSessionCoordinator`): -/// ``` -/// 耳机按键 ──► BB 64 (startAI) -/// App ──► AA 6A 必须 3 秒内,否则耳机停止送音频 -/// …会话进行中,耳机持续上行 mic,App 持续下行 AI 语音… -/// App ──► AA 65 请求退出 -/// 耳机 ──► BB E5 (stopAI) 正常收尾;2 秒不回就本地强制结束 -/// ``` -/// -/// 常驻服务:耳机按键在任意页面(含 App 在后台、EMAI 页没打开)都要能唤醒。 -class BesAiSessionService extends GetxService { - static const String _tag = 'BesAiSession'; - static BesAiSessionService get to => Get.find(); - - /// 耳机侧的会话与手机 EMAI 页各用一条独立的 WebSocket。 - /// - /// 共用一个 [BailianMultimodalService] 单例的话,两边会互相把对方的会话 - /// `stop()` 掉(`start()` 里第一件事就是 stop),而且下行音频会串到对方的 - /// 播放器上。独立实例最省心,代价只是极端情况下两条会话并存—— - /// 而这种情况已经由 [_yieldPhonePage] 挡掉了。 - final BailianMultimodalService _svc = BailianMultimodalService(); - final BluetoothManager _bt = BluetoothManager(); - - /// 耳机上行 PCM 就是 16k/16bit/mono,与百炼上行要求一致,不用转换 - static const int _earphoneRate = 16000; - - /// 上行攒够这么多字节再发一包:3200B = 100ms @16k/16bit/mono。 - /// 对齐 deepvoice `HeadsetAudioLink.MIC_BATCH_BYTES`,也正好是百炼文档 - /// 建议的上行包大小。耳机每 20ms 上来一包,逐包转发就是 50 条 WS 消息/秒, - /// 攒成 100ms 一条能把消息数降到 1/5。 - static const int _micBatchBytes = 3200; - final BytesBuilder _micAccum = BytesBuilder(copy: false); - - /// 收到 `0x64` 后必须在这个时间内回 `AA 6A`。我们是立即回, - /// 这个常量只是把固件约束写在代码里,改动时别超过它。 - static const Duration _ackDeadline = Duration(seconds: 3); - - /// 发出 `AA 65` 后等耳机回 `0xE5` 的时间,超时就本地强制结束 - static const Duration _stopAckTimeout = Duration(seconds: 2); - - StreamSubscription? _aiSub; - StreamSubscription? _eventSub; - StreamSubscription? _audioSub; - Timer? _stopAckTimer; - Timer? _playoutTimer; - - /// 会话是否在跑(已回过 `AA 6A`,还没收到/发出结束) - final RxBool isActive = false.obs; - - /// 手机 EMAI 页(打开时由它自己 attach 上来)。 - /// - /// 这里刻意**不**用 `Get.put(controller)` 注册: - /// GetX 的 put 会对实例调一次 onStart→onInit,而页面正是在自己的 onInit 里 - /// 注册的,等于 onInit 递归调 onInit —— 实测直接 Stack Overflow。 - EmaiSessionOwner? _phonePage; - - /// 本轮回复累计推给耳机的 PCM 字节数,用来估算播完的时刻 - int _respondingBytes = 0; - DateTime? _respondingStartedAt; - - /// 上行是否被门控住(AI 正在耳机里播报期间)。 - /// - /// 耳机没有把 SPP 下发的音频接进自己的 AEC 参考路径,播出去的 AI 声音会被 - /// 自己的麦克风原样收回来;duplex 模式下服务端 VAD 把它当成用户插话, - /// 于是每次 AI 一开口就被自己打断——实测 respondingStarted 之后 0.9s - /// 必定收到 speechStarted,一轮回复被切成碎片,听起来就是杂音。 - /// - /// 没有 AEC 就只能做半双工:播报期间不把耳机麦的音频交上去。 - /// 代价是这段时间没法插话打断,但在没有回声消除的前提下本来也做不到。 - bool _uplinkGated = false; - Timer? _gateTimer; - - /// teardown 重入保护。结束路径不止一条(空闲超时、耳机回 0xE5、 - /// requestStop 超时、百炼断开),而 teardown 里要 await 播完「咚」, - /// 这段时间足够第二条路径挤进来,不挡的话会放两声提示音、收两次尾。 - bool _tearingDown = false; - - /// 播报结束后再多门控一会儿:耳机自己还有一小段抖动缓冲要播完 - static const Duration _gateTail = Duration(milliseconds: 500); - - /// 双方都没出声超过这个时长就自动结束会话。 - /// "没出声"指:用户没说话(无 ASR 事件)且 AI 也不在播报(门控已解除)。 - static const Duration _idleTimeout = Duration(seconds: 5); - Timer? _idleTimer; - - /// 本轮**首包音频**到达的时刻。用它而不是 respondingStarted: - /// 实测服务端会先回 RespondingContent 再回 RespondingStarted, - /// 音频也可能先于 RespondingStarted 到,用后者算时长会偏。 - DateTime? _firstAudioAt; - - /// 上行/下行的包计数,只为排查用: - /// 会话起来了却没有任何语音事件时,第一件要确认的就是耳机到底有没有在送音频。 - int _micPackets = 0; - int _downPackets = 0; - - /// 耳机这轮对话也要落进共享的聊天记录里。 - /// 用户明确要求:不管是耳机直接跟 AI 聊的还是在 App 里聊的, - /// 打开 EMAI 界面都得能看到内容——所以这里不能只做音频转发。 - EmaiChatStore get _store => EmaiChatStore.to; - - /// 当前正在流式接收的两条消息 - EmaiMessage? _streamingUser; - EmaiMessage? _streamingAssistant; - - @override - void onInit() { - super.onInit(); - _aiSub = _bt.onDeviceAiEvent().listen(_onAiEvent, onError: (e) { - Logger.e(_tag, 'AI 事件流错误: $e'); - }); - Logger.i(_tag, '耳机 AI 会话服务已就绪,等待耳机按键唤醒'); - } - - @override - void onClose() { - _aiSub?.cancel(); - _teardown(notifyEarphone: true, playEndTone: false); - super.onClose(); - } - - // ---------------- 耳机事件 ---------------- - - void _onAiEvent(Map event) { - switch (event['type']) { - case 'startAI': - _handleStartAI(); - break; - case 'stopAI': - Logger.i(_tag, '耳机通知结束 AI(0xE5)'); - _teardown(notifyEarphone: false); - break; - case 'micData': - final pcm = event['data']; - if (!isActive.value || pcm is! Uint8List) break; - _micPackets++; - // 20ms 一包,50 包 = 1s - if (_micPackets == 1 || _micPackets % 50 == 0) { - Logger.i(_tag, - '耳机上行 #$_micPackets (${pcm.length}B/包)${_uplinkGated ? ' [门控中]' : ''}'); - } - // 门控期间推等长静音而不是直接断流:上行保持连续, - // 服务端 VAD 收到的是"真的没人说话",而不是一段数据缺口 - _micAccum.add(_uplinkGated ? Uint8List(pcm.length) : pcm); - if (_micAccum.length >= _micBatchBytes) { - _svc.pushAudio(_micAccum.takeBytes()); - } - break; - case 'deviceDisconnected': - if (isActive.value) { - Logger.w(_tag, '耳机断连,结束 AI 会话'); - _teardown(notifyEarphone: false, playEndTone: false); - } - break; - } - } - - Future _handleStartAI() async { - if (isActive.value) { - Logger.w(_tag, '已有会话在跑,忽略重复的 0x64'); - return; - } - Logger.i(_tag, '耳机按键唤醒 AI(0x64)'); - - // 起不来就别应答,直接让耳机退出 AI 模式——回了 AA 6A 之后耳机会一直 - // 送音频,而我们这边没有会话接,它会白等到超时 - if (!_svc.isConfigured) { - Logger.e(_tag, '百炼未配置(${_svc.configHint}),拒绝本次唤醒'); - await _sendExitAi(); - return; - } - if (BesBluetoothService.to.deviceStatus.value != 2) { - Logger.e(_tag, '耳机未处于已连接状态,拒绝本次唤醒'); - return; - } - - // 固件要求 3 秒内确认,先回 AA 6A 再做后面的初始化。 - // 反过来的话,百炼握手(一次 WebSocket 往返)可能就吃掉大半个预算。 - final ackAt = DateTime.now(); - await BesBluetoothService.to.sendData([0xAA, 0x6A]); - isActive.value = true; - _micPackets = 0; - _downPackets = 0; - _uplinkGated = false; - _micAccum.clear(); - - await _yieldPhonePage(); - - _eventSub = _svc.events.listen(_onBailianEvent); - // 下行 16k:G.722 编码器就是 16k,让服务端直接出 16k 省掉一次重采样 - _audioSub = _svc.audioOut.listen(_onDownstreamPcm); - await _bt.startAiDownlink(); - // 先给一声「叮」,让用户立刻知道已经在听了—— - // 百炼握手还要一次 WebSocket 往返,不出声的话这段空档很像没反应 - await _playCue(CueTone.ding(sampleRate: _earphoneRate)); - _touchActivity(); - - final ok = await _svc.start( - mode: 'duplex', - downstreamRate: _earphoneRate, - userId: _currentUserId(), - source: 'earphone', - mcpToken: await McpTokenService.get(), - ); - final elapsed = DateTime.now().difference(ackAt); - if (elapsed > _ackDeadline) { - Logger.w( - _tag, - '会话初始化耗时 ${elapsed.inMilliseconds}ms,已超过固件 ' - '${_ackDeadline.inSeconds}s 预算(AA 6A 已提前回,通常无碍)'); - } - if (!ok) { - Logger.e(_tag, '百炼会话启动失败,结束'); - await _teardown(notifyEarphone: true); - } - } - - // ---------------- 百炼事件 ---------------- - - void _onBailianEvent(BailianEvent e) { - // 耳机会话的事件量很小(不像音频包),全量打出来方便排查 - Logger.i(_tag, '百炼事件 $e'); - // 任何一方有动静都算"没静默" - switch (e.type) { - case BailianEventType.speechStarted: - case BailianEventType.speechContent: - case BailianEventType.speechEnded: - case BailianEventType.respondingStarted: - case BailianEventType.respondingContent: - case BailianEventType.respondingEnded: - _touchActivity(); - break; - default: - break; - } - switch (e.type) { - case BailianEventType.speechStarted: - // 只有真人插话才丢队列。门控期间的 speechStarted 是 AI 自己的回声 - // 触发的,这时候 flush 等于自己把自己的话掐断——之前 45% 的下行音频 - // (push 5336 帧只发出 2917 包)就是这么丢掉的。 - if (!_uplinkGated) { - _bt.flushAiDownlink(); - _cancelPlayoutTimer(); - } - // 气泡等第一个 speechContent 再懒建:ASR 一句都没识别出来时 - // 服务端回的是 Error 而不是 SpeechEnded,预建的空气泡没人清理 - _streamingUser = null; - break; - - case BailianEventType.speechContent: - _streamingUser ??= _store - .append(EmaiMessage(isUser: true, text: '', fromEarphone: true)); - _store.applyText(_streamingUser!, e.text); - if (e.isFinal) { - _store.finalize(_streamingUser); - _streamingUser = null; - } - break; - - case BailianEventType.speechEnded: - if (e.text.isNotEmpty && _streamingUser != null) { - _store.applyText(_streamingUser!, e.text); - } - _store.finalize(_streamingUser); - _streamingUser = null; - break; - - case BailianEventType.respondingStarted: - _respondingBytes = 0; - _firstAudioAt = null; - _respondingStartedAt = DateTime.now(); - _cancelPlayoutTimer(); - _streamingAssistant = null; - break; - - case BailianEventType.respondingContent: - // 耳机唤醒这条链路同样会收到端侧指令,一并落地——用户对着耳机说 - // "定个闹钟"和在对话页里说,效果应该一样。 - if (e.directives.isNotEmpty && - Get.isRegistered()) { - AssistantDirectiveService.to.handle(e.directives); - } - // 纯指令帧没有文本,别建空气泡(详见 BailianEvent.finishReason) - if (e.text.isEmpty && e.finishReason == 'command_calls') break; - _streamingAssistant ??= _store - .append(EmaiMessage(isUser: false, text: '', fromEarphone: true)); - _store.applyText(_streamingAssistant!, e.text); - break; - - case BailianEventType.respondingEnded: - if (e.text.isNotEmpty && _streamingAssistant != null) { - _store.applyText(_streamingAssistant!, e.text); - } - _store.finalize(_streamingAssistant); - _streamingAssistant = null; - _logResponseAudioStats(); - _schedulePlayoutDone(); - break; - - case BailianEventType.error: - Logger.e(_tag, '会话错误: ${e.code} ${e.text} fatal=${e.fatal}'); - _store.dropIfEmpty(_streamingUser); - _streamingUser = null; - _store.dropIfEmpty(_streamingAssistant); - _streamingAssistant = null; - if (e.text.isNotEmpty) { - _store.append(EmaiMessage( - isUser: false, - text: e.text, - isFinal: true, - isError: true, - fromEarphone: true)); - _store.persist(); - } - if (e.fatal) _teardown(notifyEarphone: true); - break; - - case BailianEventType.closed: - if (isActive.value) { - Logger.w(_tag, '百炼会话关闭,结束耳机 AI'); - _teardown(notifyEarphone: true); - } - break; - - default: - break; - } - } - - void _onDownstreamPcm(Uint8List pcm) { - if (!isActive.value || pcm.isEmpty) return; - if (_firstAudioAt == null) { - _firstAudioAt = DateTime.now(); - _openGate(); - } - _respondingBytes += pcm.length; - _downPackets++; - if (_downPackets == 1 || _downPackets % 50 == 0) { - Logger.i(_tag, '下行给耳机 #$_downPackets (${pcm.length}B)'); - } - _bt.pushAiPcm(pcm); - } - - /// 有动静就把静默计时重新拨回 5 秒。 - /// - /// AI 正在耳机里播报([_uplinkGated] 为真)时不计时——那期间本来就有声音, - /// 而且长回复可能播好几分钟,一计时准误杀。门控解除时会再调一次把表开起来。 - void _touchActivity() { - _idleTimer?.cancel(); - _idleTimer = null; - if (!isActive.value || _uplinkGated) return; - _idleTimer = Timer(_idleTimeout, () { - if (!isActive.value) return; - Logger.i(_tag, '双方 ${_idleTimeout.inSeconds}s 无声,自动结束 AI 会话'); - requestStop(); - }); - } - - /// 在耳机里放一段提示音。 - /// - /// 走 AI 下行通道(原生会 G.722 编码后按节奏发),所以必须在 - /// [BluetoothManager.startAiDownlink] 之后、[BluetoothManager.stopAiDownlink] - /// 之前调用。播放期间开门控,免得提示音被耳机麦收回去触发 VAD。 - Future _playCue(Uint8List pcm, {bool waitForPlayout = false}) async { - if (BesBluetoothService.to.deviceStatus.value != 2) return; - _openGate(); - await _bt.pushAiPcm(pcm); - final ms = pcm.length * 1000 ~/ (_earphoneRate * 2); - if (waitForPlayout) { - // 结束音要等它真的发完+播完,否则紧接着 stopAiDownlink 会把它清掉 - await Future.delayed(Duration(milliseconds: ms + 400)); - } else { - _gateTimer?.cancel(); - _gateTimer = Timer(Duration(milliseconds: ms + 300), _closeGate); - } - } - - void _openGate() { - _gateTimer?.cancel(); - _gateTimer = null; - if (_uplinkGated) return; - _uplinkGated = true; - // 播报期间不算静默 - _idleTimer?.cancel(); - _idleTimer = null; - Logger.i(_tag, '上行门控开启(AI 播报中)'); - } - - void _closeGate() { - _gateTimer?.cancel(); - _gateTimer = null; - if (!_uplinkGated) return; - _uplinkGated = false; - Logger.i(_tag, '上行门控解除'); - // 播完了,静默计时从这一刻开始 - _touchActivity(); - } - - /// 把这一轮下行音频的字节数/时长打出来。 - /// - /// 用来核对服务端到底按哪个采样率在发:我们请求的是 16k - /// (16000 × 2 = 32000 B/s),如果实测速率接近 48000 B/s, - /// 说明 `sample_rate` 没被采纳、实际还是 24k —— 那样喂给 16k 的 - /// G.722 编码器出来就是变速变调的声音,必须在这里重采样。 - void _logResponseAudioStats() { - final start = _firstAudioAt; - if (start == null || _respondingBytes == 0) return; - final ms = DateTime.now().difference(start).inMilliseconds; - final audioMs = _respondingBytes * 1000 ~/ (_earphoneRate * 2); - Logger.i( - _tag, - '本轮下行音频 ${_respondingBytes}B ≈ ${audioMs}ms(按16k算) ' - ',实际用时 ${ms}ms'); - } - - /// 估算耳机什么时候把这一轮播完,然后回 `LocalRespondingEnded`。 - /// - /// 拿不到耳机的真实播放进度,只能按实时速率算:16k/16bit/mono = 32000 B/s, - /// 服务端下发通常快于实时,所以「已推字节对应的时长 − 从开播到现在的时长」 - /// 就是还没播完的部分。多等一点没坏处(服务端只是晚一点回到 Listening), - /// 早回则会让它以为已经播完而提前接话。 - void _schedulePlayoutDone() { - _cancelPlayoutTimer(); - const bytesPerSecond = _earphoneRate * 2; - final totalMs = _respondingBytes * 1000 ~/ bytesPerSecond; - final elapsedMs = DateTime.now() - .difference(_respondingStartedAt ?? DateTime.now()) - .inMilliseconds; - // 上限给 10 分钟,跟原生下行队列的容量对齐。 - // 原先 clamp 到 30 秒,遇到 158 秒的长回复时门控提前 84 秒解除, - // 耳机还在播故事,回环声又被服务端当成用户插话。 - final remainMs = (totalMs - elapsedMs).clamp(0, 600000); - Logger.i(_tag, '预计还需 ${remainMs}ms 播完(已推 ${totalMs}ms 音频)'); - _playoutTimer = Timer(Duration(milliseconds: remainMs), () { - if (!isActive.value) return; - _svc.notifyLocalRespondingEnded(); - // 再等一小会儿让耳机把自己的缓冲放完,才敢重新收音 - _gateTimer = Timer(_gateTail, _closeGate); - }); - } - - void _cancelPlayoutTimer() { - _playoutTimer?.cancel(); - _playoutTimer = null; - } - - // ---------------- 结束 ---------------- - - /// 主动结束(比如 App 侧要收回控制权):发 `AA 65`,等耳机回 `0xE5`; - /// 超时就本地强制收尾,避免卡在「耳机不理我」的状态。 - Future requestStop() async { - if (!isActive.value) return; - Logger.i(_tag, '请求耳机退出 AI 模式(AA 65)'); - await _sendExitAi(); - _stopAckTimer?.cancel(); - _stopAckTimer = Timer(_stopAckTimeout, () { - if (!isActive.value) return; - Logger.w(_tag, '${_stopAckTimeout.inSeconds}s 未收到 0xE5,本地强制结束'); - _teardown(notifyEarphone: false); - }); - } - - Future _sendExitAi() async { - try { - await BesBluetoothService.to.sendData([0xAA, 0x65]); - } catch (e) { - Logger.w(_tag, '发送 AA 65 失败: $e'); - } - } - - /// [notifyEarphone] 为 true 时补发 `AA 65`:不是耳机主导的结束 - /// (百炼断开、配置失败、服务销毁),必须告诉耳机退出, - /// 否则它会继续朝一个已经没人接的会话推流。 - /// [playEndTone] 为 false 用于耳机已经断连的场景——那时候放「咚」没有意义, - /// 还会白等一次超时。 - Future _teardown({ - required bool notifyEarphone, - bool playEndTone = true, - }) async { - if (_tearingDown) return; - _tearingDown = true; - try { - await _doTeardown( - notifyEarphone: notifyEarphone, playEndTone: playEndTone); - } finally { - _tearingDown = false; - } - } - - Future _doTeardown({ - required bool notifyEarphone, - bool playEndTone = true, - }) async { - _stopAckTimer?.cancel(); - _stopAckTimer = null; - _idleTimer?.cancel(); - _idleTimer = null; - _cancelPlayoutTimer(); - _micAccum.clear(); - - if (isActive.value && playEndTone) { - // 先把还没播完的 AI 语音丢掉,不然「咚」要排在几十秒的队尾 - await _bt.flushAiDownlink(); - await _playCue(CueTone.dong(sampleRate: _earphoneRate), - waitForPlayout: true); - } - _closeGate(); - - final wasActive = isActive.value; - isActive.value = false; - - _store.dropIfEmpty(_streamingUser); - _streamingUser = null; - _store.dropIfEmpty(_streamingAssistant); - _streamingAssistant = null; - if (wasActive) _store.persist(); - - await _eventSub?.cancel(); - _eventSub = null; - await _audioSub?.cancel(); - _audioSub = null; - - await _bt.stopAiDownlink(); - await _svc.stop(); - - if (wasActive && notifyEarphone) await _sendExitAi(); - if (wasActive) { - Logger.i(_tag, '耳机 AI 会话已结束 (上行 $_micPackets 包 / 下行 $_downPackets 包)'); - await _resumePhonePage(); - } - } - - // ---------------- 与手机 EMAI 页互斥 ---------------- - - /// 耳机会话优先:手机上的 EMAI 页正开着就让它先让出去。 - /// - /// 用动态查找而不是直接 import EmaiController,是为了不让 data 层反向依赖 - /// modules 层——这个服务是常驻的,EMAI 页只是它的可选邻居。 - /// EMAI 页打开时把自己挂上来 - void attachPhonePage(EmaiSessionOwner page) => _phonePage = page; - - /// EMAI 页销毁时摘掉。带实例校验:页面快速重建时新页可能已经 attach, - /// 旧页的 detach 不该把新页摘掉。 - void detachPhonePage(EmaiSessionOwner page) { - if (identical(_phonePage, page)) _phonePage = null; - } - - Future _yieldPhonePage() async { - final ctrl = _phonePage; - if (ctrl == null) return; - Logger.i(_tag, '手机 EMAI 页正开着,让它让出会话'); - try { - await ctrl.yieldToEarphone(); - } catch (e) { - Logger.w(_tag, '通知 EMAI 页让出失败: $e'); - } - } - - Future _resumePhonePage() async { - final ctrl = _phonePage; - if (ctrl == null) return; - try { - await ctrl.resumeFromEarphone(); - } catch (e) { - Logger.w(_tag, '通知 EMAI 页恢复失败: $e'); - } - } - - String _currentUserId() { - try { - return user_info.User.isLoggedIn() - ? user_info.User.instance.uid - : 'eaimar-guest'; - } catch (_) { - return 'eaimar-guest'; - } - } -} - -/// 手机 EMAI 页实现这个接口,让耳机会话能在不反向依赖 modules 层的前提下 -/// 通知它让出/恢复。EmaiController 在 onInit/onClose 里 -/// 调 [BesAiSessionService.attachPhonePage] / [BesAiSessionService.detachPhonePage]。 -abstract class EmaiSessionOwner { - /// 耳机接管:停掉本页的播放与会话,并且不要自动重连 - Future yieldToEarphone(); - - /// 耳机结束:把本页的会话接回来 - Future resumeFromEarphone(); -} diff --git a/apps/client/lib/data/services/bes_call_recording_service.dart b/apps/client/lib/data/services/bes_call_recording_service.dart deleted file mode 100644 index 3ac752b7..00000000 --- a/apps/client/lib/data/services/bes_call_recording_service.dart +++ /dev/null @@ -1,388 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math' as math; -import 'dart:typed_data'; - -import 'package:get/get.dart'; -import 'package:intl/intl.dart'; -import 'package:path_provider/path_provider.dart'; - -import '../../core/utils/audio_foreground_service.dart'; -import '../../core/utils/logger.dart'; -import 'bes_bluetooth_service.dart'; -import 'recording_archive.dart'; - -/// 恒玄(BES)耳机的**通话录音**服务。 -/// -/// 与现场录音([BesRecordingService],主麦 mSBC 单声道)是两套: -/// 通话录音走 deepvoice 那边 audioType=2 的做法—— -/// `AA 51` 进通话模式 / `AA 52` 退出,耳机以 G.722 mode=0x03 **双路交织**上行, -/// 原生解码后交替是「对端(spk) / 本端(mic)」,这里按交替顺序拆成两路, -/// 落成**双声道 WAV:左=本端麦克风,右=对端声音**,与 deepvoice 的 -/// AstStereoRecorder 输出一致。 -/// -/// 只在通话中可用([BesBluetoothService.isCallOngoing]), -/// 耳机上报 0xD1 接通 / 0xD2 挂断时自动起停。 -class BesCallRecordingService extends GetxService { - static const String _tag = 'BesCallRecordingService'; - static BesCallRecordingService get to => Get.find(); - - /// 当前是否正在录(由设备端状态驱动) - final RxBool isRecording = false.obs; - - /// 已录毫秒数 - final RxInt elapsedMs = 0.obs; - - /// 当前电平 0~1,供波形使用 - final RxDouble level = 0.0.obs; - - /// 波形采样序列(固定 [sampleIntervalMs] 一格) - final RxList waveform = [].obs; - static const int sampleIntervalMs = 100; - static const int _maxSamples = 600; - - Worker? _stateWorker; - Worker? _callWorker; - StreamSubscription? _audioSub; - - /// 双路交织:true 时这一帧是对端(spk),false 时是本端(mic) - bool _toSpk = true; - - /// 暂存的对端帧,等本端帧到齐后一起交织成双声道写盘 - Uint8List? _pendingSpk; - - /// 收到的原始帧计数(诊断用:判断耳机到底有没有上行通话音频) - int _frameCount = 0; - Timer? _silenceCheck; - RandomAccessFile? _raf; - int _dataBytes = 0; - String? _path; - Timer? _tick; - Timer? _sampleTimer; - final Stopwatch _stopwatch = Stopwatch(); - - @override - void onInit() { - super.onInit(); - _bind(); - } - - @override - void onClose() { - _stateWorker?.dispose(); - _callWorker?.dispose(); - _audioSub?.cancel(); - _tick?.cancel(); - _sampleTimer?.cancel(); - super.onClose(); - } - - void _bind() { - final bes = BesBluetoothService.to; - - // 通话开始**不再自动录音**。录音只有两个入口: - // 1) 用户在录音页点「开始录音」(CallRecordingController 调 start) - // 2) 耳机端按录音键:上报 0xD9 开始 / 0xDA 停止 - // 通话中默认就录属于未经用户操作的采集,既不合预期也不合规。 - _stateWorker = ever(bes.lastCmdType, (type) async { - if (_busy) return; - if (type == 'startMicRecording' && !isRecording.value) { - if (!bes.isCallOngoing.value) return; // 不在通话里的按键归现场录音管 - Logger.i(_tag, '耳机端按下录音键,开始通话录音'); - _deviceRecordingActive = true; - await start(sendCommand: true); - } else if (type == 'stopMicRecording' && isRecording.value) { - // 设备端自己停的:它已经退出录音态,不用再回发 AA 5A - Logger.i(_tag, '耳机端结束录音,落盘并归档'); - _deviceRecordingActive = false; - await stop(sendCommand: true); - } - }); - - // 通话结束时如果还在录,收尾归档(不能让文件一直挂着) - _callWorker = ever(bes.isCallOngoing, (calling) async { - if (!calling && isRecording.value && !_busy) { - Logger.i(_tag, '通话结束,停止并归档'); - await stop(sendCommand: true); - } - }); - - ever(bes.deviceStatus, (v) { - if (v == 0 && isRecording.value) { - Logger.w(_tag, '耳机断开,结束当前通话录音'); - stop(); - } - }); - } - - /// 开始本地采集。[sendCommand] = true 时同时下发设备开始指令 - /// (App 主动发起录音的场景)。 - /// 起停过程中的互斥标志:设备上报和 UI 操作可能同时进来, - /// 不加锁会出现「刚建好文件又被另一路关掉」的竞态。 - bool _busy = false; - - /// 本次录音是不是耳机按键发起的(0xD9)。 - /// 设备侧处于录音态时,停止要额外补一条 `AA 5A` 让它自己也退出来, - /// 只发 `AA 52`(退出通话模式)不足以清掉设备端的录音状态。 - bool _deviceRecordingActive = false; - - Future start({bool sendCommand = false}) async { - if (isRecording.value || _busy) return isRecording.value; - _busy = true; - try { - return await _start(sendCommand); - } finally { - _busy = false; - } - } - - Future _start(bool sendCommand) async { - if (!Get.isRegistered()) return false; - final bes = BesBluetoothService.to; - if (!bes.isConnected) return false; - - try { - final docs = await getApplicationDocumentsDirectory(); - final dir = Directory('${docs.path}/call_recordings'); - if (!await dir.exists()) await dir.create(recursive: true); - final ts = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); - _path = '${dir.path}/call_$ts.wav'; - - _raf = await File(_path!).open(mode: FileMode.write); - await _raf!.writeFrom(_wavHeader(0)); - _dataBytes = 0; - - _toSpk = true; - _pendingSpk = null; - _frameCount = 0; - _audioSub?.cancel(); - _audioSub = bes.audioStream.listen(_onPcm); - - // AA 51:进通话模式,耳机才会双路上行。 - // 不管是不是 App 主动发起的(设备端 0xD1 也会触发这里),都补发一次—— - // 只靠设备自己进通话模式时,它未必会把音频回传给 App。 - await bes.startCallMode(); - - // 2 秒还没收到任何音频帧就再补一次指令,并把情况打进日志。 - // 通话录音录不到声音时,先看这条日志判断是「没下发成功」还是「固件没回传」。 - _silenceCheck?.cancel(); - _silenceCheck = Timer(const Duration(seconds: 2), () async { - if (!isRecording.value) return; - if (_frameCount == 0) { - Logger.w(_tag, '进通话模式 2s 内没收到任何音频帧,重发 AA 51'); - await bes.startCallMode(); - } else { - Logger.i(_tag, '通话音频已上行,2s 内收到 $_frameCount 帧'); - } - }); - - // 把进程钉住,否则切后台后录音会被国内 ROM 悄悄掐断(回前台才发现截断) - unawaited(AudioForegroundService.start()); - - isRecording.value = true; - elapsedMs.value = 0; - level.value = 0; - waveform.clear(); - _stopwatch - ..reset() - ..start(); - _tick = Timer.periodic(const Duration(milliseconds: 200), (_) { - elapsedMs.value = _stopwatch.elapsedMilliseconds; - }); - _sampleTimer = Timer.periodic( - const Duration(milliseconds: sampleIntervalMs), - (_) => _sample(), - ); - Logger.i(_tag, '开始录制: $_path'); - return true; - } catch (e) { - Logger.e(_tag, '启动本地采集失败: $e'); - await _closeFile(save: false); - return false; - } - } - - /// 停止。[sendCommand] = true 时同时下发设备停止指令。 - Future stop({bool sendCommand = false}) async { - if (!isRecording.value || _busy) return; - _busy = true; - try { - await _stop(sendCommand); - } finally { - _busy = false; - } - } - - Future _stop(bool sendCommand) async { - _silenceCheck?.cancel(); - Logger.i(_tag, '停止通话录音: 收到 $_frameCount 帧,写入 $_dataBytes 字节'); - _tick?.cancel(); - _sampleTimer?.cancel(); - _stopwatch.stop(); - - final int seconds = elapsedMs.value ~/ 1000; - final String? path = _path; - - if (sendCommand && BesBluetoothService.to.isConnected) { - try { - // 设备侧还在录音态时先让它停录(AA 5A),再退出通话模式(AA 52)。 - // 顺序不能反:先退通话模式的话,录音停止指令未必还能被正确处理。 - if (_deviceRecordingActive) { - await BesBluetoothService.to.stopMicRecording(); // AA 5A - Logger.i(_tag, '已下发 AA 5A,通知耳机退出录音态'); - } - await BesBluetoothService.to.stopCallMode(); // AA 52 - } catch (e) { - Logger.e(_tag, '下发停止指令失败: $e'); - } - } - _deviceRecordingActive = false; - - await _closeFile(save: true); - unawaited(AudioForegroundService.stop()); - isRecording.value = false; - elapsedMs.value = 0; - level.value = 0; - waveform.clear(); - - if (path != null && _dataBytes > 0) { - // 归档会把文件从 call_recordings 挪到 LocalAudio,并在服务端建记录、 - // 交上传服务上云——通话录音的成品同样统一由「语音记录助手」管理。 - // 双声道:左=本端(自己),右=对端(对方)。 - await _archive(path, seconds); - } else { - // 没收到音频的空文件(只有 44 字节 WAV 头)直接删掉, - // 否则会在现场录音列表里堆一堆 00:00 的无效记录。 - Logger.w(_tag, '本次没有收到音频数据,删除空文件'); - if (path != null) { - try { - final f = File(path); - if (await f.exists()) await f.delete(); - } catch (e) { - Logger.e(_tag, '删除空录音文件失败: $e'); - } - } - } - _dataBytes = 0; - _path = null; - } - - void _onPcm(Uint8List pcm) async { - if (_raf == null || pcm.isEmpty) return; - _frameCount++; - if (_frameCount <= 3 || _frameCount % 200 == 0) { - Logger.d(_tag, '收到通话音频帧 #$_frameCount len=${pcm.length}'); - } - // G.722 mode=0x03 是交织上行:一帧对端、一帧本端,轮流来 - final isSpk = _toSpk; - _toSpk = !_toSpk; - if (isSpk) { - _pendingSpk = pcm; - return; - } - final spk = _pendingSpk; - _pendingSpk = null; - if (spk == null) return; - await _writeStereo(mic: pcm, spk: spk); - _updateLevel(pcm); - } - - /// 左声道=本端麦克风,右声道=对端声音(与 deepvoice 的双声道录音一致) - /// 左声道=本端麦克风,右声道=对端声音(与 deepvoice 的双声道录音一致)。 - /// - /// 这里**按字节交织**而不是先转 Int16List:原生过来的 Uint8List 是带 - /// offset 的视图,`asInt16List(offsetInBytes, …)` 遇到奇数偏移会直接抛, - /// 而 _onPcm 是 async 的,异常被静默吞掉——表现就是「收了 3528 帧、写入 0 字节」。 - /// 直接搬字节没有对齐要求,小端序也原样保留。 - Future _writeStereo( - {required Uint8List mic, required Uint8List spk}) async { - final raf = _raf; - if (raf == null) return; - try { - final samples = (mic.length < spk.length ? mic.length : spk.length) ~/ 2; - if (samples <= 0) return; - final out = Uint8List(samples * 4); - for (var i = 0; i < samples; i++) { - final si = i * 2, di = i * 4; - out[di] = mic[si]; - out[di + 1] = mic[si + 1]; - out[di + 2] = spk[si]; - out[di + 3] = spk[si + 1]; - } - await raf.writeFrom(out); - _dataBytes += out.length; - } catch (e, st) { - Logger.e(_tag, '写入双声道 PCM 失败: $e\n$st'); - } - } - - void _sample() { - final list = waveform.toList(); - list.add(level.value); - if (list.length > _maxSamples) { - list.removeRange(0, list.length - _maxSamples); - } - waveform.assignAll(list); - } - - void _updateLevel(Uint8List pcm) { - if (pcm.lengthInBytes < 2) return; - final s = pcm.buffer.asInt16List(pcm.offsetInBytes, pcm.lengthInBytes ~/ 2); - var sum = 0.0; - for (final v in s) { - final n = v / 32768.0; - sum += n * n; - } - final rms = math.sqrt(sum / s.length); - level.value = math.sqrt(rms).clamp(0.0, 1.0); - } - - Future _closeFile({required bool save}) async { - await _audioSub?.cancel(); - _audioSub = null; - final raf = _raf; - _raf = null; - if (raf == null) return; - try { - if (save) { - await raf.setPosition(0); - await raf.writeFrom(_wavHeader(_dataBytes)); - } - await raf.close(); - } catch (e) { - Logger.e(_tag, '关闭录音文件失败: $e'); - } - } - - /// 归档进语音记录助手。具体入库逻辑见 [RecordingArchive]—— - /// 现场录音 / 通话录音 / 翻译录音都走同一套,不再各抄一份。 - Future _archive(String path, int seconds) async { - await RecordingArchive.archive(path: path, seconds: seconds, tag: _tag); - } - - Uint8List _wavHeader(int dataLength) { - const sampleRate = 16000, channels = 2, bits = 16; - final byteRate = sampleRate * channels * (bits ~/ 8); - final h = ByteData(44); - void ascii(int off, String s) { - for (var i = 0; i < s.length; i++) { - h.setUint8(off + i, s.codeUnitAt(i)); - } - } - - ascii(0, 'RIFF'); - h.setUint32(4, 36 + dataLength, Endian.little); - ascii(8, 'WAVE'); - ascii(12, 'fmt '); - h.setUint32(16, 16, Endian.little); - h.setUint16(20, 1, Endian.little); - h.setUint16(22, channels, Endian.little); - h.setUint32(24, sampleRate, Endian.little); - h.setUint32(28, byteRate, Endian.little); - h.setUint16(32, channels * (bits ~/ 8), Endian.little); - h.setUint16(34, bits, Endian.little); - ascii(36, 'data'); - h.setUint32(40, dataLength, Endian.little); - return h.buffer.asUint8List(); - } -} diff --git a/apps/client/lib/data/services/bes_recording_service.dart b/apps/client/lib/data/services/bes_recording_service.dart deleted file mode 100644 index 1babcf64..00000000 --- a/apps/client/lib/data/services/bes_recording_service.dart +++ /dev/null @@ -1,397 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math' as math; -import 'dart:typed_data'; - -import 'package:get/get.dart'; -import 'package:intl/intl.dart'; -import 'package:path_provider/path_provider.dart'; - -import '../../core/utils/logger.dart'; -import 'bes_bluetooth_service.dart'; -import 'recording_archive.dart'; - -/// 恒玄(BES)耳机录音的全局采集与归档服务。 -/// -/// 与蓝讯支架的 [HolderRecordingService] 是**两条完全独立的通路**,各走各的 -/// 协议、各自落盘归档,互不影响: -/// - 支架:BLE 私有协议 + Opus,SDK 直接给解码后的 PCM 帧 -/// - 耳机:经典蓝牙 SPP,`AA 59` 开录 / `AA 5A` 停录,音频以 mSBC 帧上行, -/// 原生解码成 16kHz/16bit 单声道走 [BesBluetoothService.audioStream] -/// -/// 一个关键差异:耳机是**主麦与前馈麦交替上行**,必须隔帧取主麦, -/// 全收会把两路混在一起。 -/// -/// 设备端按键起停(`0xD9`/`0xDA`)这里也接住,所以录音页没打开也能录、能归档。 -class BesRecordingService extends GetxService { - static const String _tag = 'BesRecordingService'; - static BesRecordingService get to => Get.find(); - - /// 当前是否正在录(由设备端状态驱动) - final RxBool isRecording = false.obs; - - /// 已录毫秒数 - final RxInt elapsedMs = 0.obs; - - /// 当前电平 0~1,供波形使用 - final RxDouble level = 0.0.obs; - - /// 波形采样序列(固定 [sampleIntervalMs] 一格) - final RxList waveform = [].obs; - static const int sampleIntervalMs = 100; - static const int _maxSamples = 600; - - Worker? _stateWorker; - StreamSubscription? _audioSub; - - /// 主麦 / 前馈麦交替上行,靠它隔帧取主麦 - bool _toMainMic = true; - RandomAccessFile? _raf; - int _dataBytes = 0; - String? _path; - Timer? _tick; - Timer? _sampleTimer; - final Stopwatch _stopwatch = Stopwatch(); - - /// 写入队列:把并发到达的音频帧排成串行,见 _onPcm 的说明。 - Future _writeChain = Future.value(); - int _writtenFrames = 0; - int _droppedFrames = 0; - - /// 下发 AA 59(开主麦)的时刻,用来量「发指令 → 耳机真正上行首帧」的间隔。 - /// 这段间隔目前是被计进录音时长里的,所以时长会比实际音频长一截。 - DateTime? _micCmdSentAt; - bool _firstPcmLogged = false; - - @override - void onInit() { - super.onInit(); - _bind(); - } - - @override - void onClose() { - _stateWorker?.dispose(); - _audioSub?.cancel(); - _tick?.cancel(); - _sampleTimer?.cancel(); - super.onClose(); - } - - void _bind() { - // 设备端按键起停:耳机上报 0xD9(startMicRecording) / 0xDA(stopMicRecording) - _stateWorker = ever(BesBluetoothService.to.lastCmdType, (type) async { - if (_busy) return; - if (type == 'startMicRecording' && !isRecording.value) { - Logger.i(_tag, '耳机端开始录音,启动本地采集'); - await start(); - } else if (type == 'stopMicRecording' && isRecording.value) { - Logger.i(_tag, '耳机端结束录音,落盘并归档'); - await stop(); - } - }); - // 断连兜底:别把录了一半的文件一直挂着 - ever(BesBluetoothService.to.deviceStatus, (v) { - if (v == 0 && isRecording.value) { - Logger.w(_tag, '耳机断开,结束当前录音'); - stop(); - } - }); - } - - /// 开始本地采集。[sendCommand] = true 时同时下发设备开始指令 - /// (App 主动发起录音的场景)。 - /// 起停过程中的互斥标志:设备上报和 UI 操作可能同时进来, - /// 不加锁会出现「刚建好文件又被另一路关掉」的竞态。 - bool _busy = false; - - Future start({bool sendCommand = false}) async { - if (isRecording.value || _busy) return isRecording.value; - _busy = true; - try { - return await _start(sendCommand); - } finally { - _busy = false; - } - } - - Future _start(bool sendCommand) async { - if (!Get.isRegistered()) return false; - final bes = BesBluetoothService.to; - if (!bes.isConnected) return false; - - try { - final docs = await getApplicationDocumentsDirectory(); - final dir = Directory('${docs.path}/field_recordings'); - if (!await dir.exists()) await dir.create(recursive: true); - final ts = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); - _path = '${dir.path}/earbuds_$ts.wav'; - - _raf = await File(_path!).open(mode: FileMode.write); - await _raf!.writeFrom(_wavHeader(0)); - _dataBytes = 0; - - _toMainMic = true; - _audioSub?.cancel(); - _audioSub = bes.audioStream.listen(_onPcm); - - _firstPcmLogged = false; - _micCmdSentAt = null; - _writeChain = Future.value(); - _writtenFrames = 0; - _droppedFrames = 0; - if (sendCommand) { - // AA 59:通知耳机开主麦,之后 mSBC 帧才会上行 - final t0 = DateTime.now(); - await bes.startMicRecording(); - _micCmdSentAt = DateTime.now(); - // 这一行量的是「我们把指令写进 SPP 花了多久」——如果它就很大, - // 说明卡在 app / 蓝牙写入侧;如果它很小而下面「首帧」很大, - // 那就是耳机响应慢。两者的优化方向完全不同。 - Logger.i(_tag, - '已下发 AA 59(开主麦),写入耗时 ${_micCmdSentAt!.difference(t0).inMilliseconds}ms'); - } - - isRecording.value = true; - elapsedMs.value = 0; - level.value = 0; - waveform.clear(); - // ⚠️ **计时不在这里启动**,而是等首帧音频真正到达(见 _onPcm)。 - // 实测耳机从收到 AA 59 到开始上行音频要 1.8~6.2 秒(首次尤其慢), - // 而 app 侧下发指令本身是 0ms。以前从下发就开始计时,这段空白全被 - // 算进录音时长——9 秒的录音里只有 2.8 秒是真音频, - // 等效采样率被稀释到 7160Hz(应为 16000)。 - // 现在时长与实际音频严格对齐;首帧到达前 UI 停在 00:00, - // 正好如实反映「还在等设备开麦」。 - _stopwatch.reset(); - // ⚠️ 时长按**已写入的数据量**算,不用墙钟。 - // - // 16kHz / 16bit / 单声道 → 32 字节 = 1 毫秒音频,这也正是播放器读 WAV - // 头算出来的时长,所以显示值与文件实际播放长度**永远一致**。 - // - // 为什么墙钟不行:耳机收到 AA 59 后要 1.8~6.2 秒才开始上行,而且会把 - // 这段等待期缓存的音频**一次性补推**上来——实测数据量比「首帧到停止」 - // 的墙钟窗口还多 45%(128880B=4.03s 音频 vs 2.77s 窗口)。 - // 无论从下发计时还是从首帧计时,都对不齐。 - _tick = Timer.periodic(const Duration(milliseconds: 200), (_) { - elapsedMs.value = _dataBytes ~/ 32; - }); - _sampleTimer = Timer.periodic( - const Duration(milliseconds: sampleIntervalMs), - (_) => _sample(), - ); - Logger.i(_tag, '开始录制: $_path'); - return true; - } catch (e) { - Logger.e(_tag, '启动本地采集失败: $e'); - await _closeFile(save: false); - return false; - } - } - - /// 停止。[sendCommand] = true 时同时下发设备停止指令。 - Future stop({bool sendCommand = false}) async { - if (!isRecording.value || _busy) return; - _busy = true; - try { - await _stop(sendCommand); - } finally { - _busy = false; - } - } - - Future _stop(bool sendCommand) async { - _tick?.cancel(); - _sampleTimer?.cancel(); - _stopwatch.stop(); - - // 先停订阅再排空写入队列:不停订阅的话新帧会一直往队列里追加,等不完。 - await _audioSub?.cancel(); - _audioSub = null; - // ⚠️ 必须等队列写完再关文件:队列里还压着最后几帧,直接关会丢掉它们, - // 而且 _dataBytes 还没加完,回填的 WAV 头长度会和实际数据对不上。 - try { - await _writeChain; - } catch (_) {} - - final int seconds = elapsedMs.value ~/ 1000; - final String? path = _path; - - // 音频时长(= 显示时长)与墙钟分开打:两者的差就是设备端「延迟开麦 + 补推 - // 缓存」的行为,丢帧数为 0 时差值全部来自设备侧,与 app 无关。 - final wallMs = _stopwatch.elapsedMilliseconds; - Logger.i( - _tag, - '录音统计: 音频${(_dataBytes / 32000).toStringAsFixed(2)}s 数据${_dataBytes}B ' - '墙钟${(wallMs / 1000).toStringAsFixed(2)}s ' - '写入${_writtenFrames}帧 丢${_droppedFrames}帧'); - - if (sendCommand && BesBluetoothService.to.isConnected) { - try { - await BesBluetoothService.to.stopMicRecording(); // AA 5A - } catch (e) { - Logger.e(_tag, '下发停止指令失败: $e'); - } - } - - await _closeFile(save: true); - isRecording.value = false; - elapsedMs.value = 0; - level.value = 0; - waveform.clear(); - - if (path != null && _dataBytes > 0) { - // 归档会把文件从 field_recordings 挪到 LocalAudio, - // 所以现场录音页的列表里不会再留副本——录音成品统一由语音记录助手管理。 - await _archive(path, seconds); - } else { - // 没收到音频的空文件(只有 44 字节 WAV 头)直接删掉, - // 否则会在现场录音列表里堆一堆 00:00 的无效记录。 - Logger.w(_tag, '本次没有收到音频数据,删除空文件'); - if (path != null) { - try { - final f = File(path); - if (await f.exists()) await f.delete(); - } catch (e) { - Logger.e(_tag, '删除空录音文件失败: $e'); - } - } - } - _dataBytes = 0; - _path = null; - } - - void _onPcm(Uint8List pcm) { - final raf = _raf; - if (raf == null || pcm.isEmpty) return; - if (!_firstPcmLogged) { - _firstPcmLogged = true; - // 首帧到了才开始计时,理由见 start() 里的说明。 - if (!_stopwatch.isRunning) _stopwatch.start(); - final sent = _micCmdSentAt; - // ⚠️ 计时器是在下发指令后就 start 的,而耳机真正开麦上行还要等这段。 - // 这段差值就是「录音显示时长 > 实际音频时长」的来源。 - Logger.i( - _tag, - '首帧音频到达' - '${sent != null ? ",距 AA 59 下发 ${DateTime.now().difference(sent).inMilliseconds}ms" : ""}' - ',计时从此刻开始'); - } - // 隔帧取主麦,前馈麦那一帧丢掉 - final take = _toMainMic; - _toMainMic = !_toMainMic; - if (!take) return; - // ⚠️ **必须串行写**,不能在这个回调里直接 `await raf.writeFrom(...)`。 - // - // Stream.listen 的回调即使标了 async 也不会被等待,而音频每秒上百帧, - // 多次 writeFrom 必然重叠;Dart 的 RandomAccessFile 同一时刻只允许一个 - // 异步操作,重叠调用会抛 "An async operation is currently pending", - // 原来那个 catch 把异常吞成一行日志——**那一帧就丢了**。 - // 实测后果:earbuds_* 录音的等效采样率只有 1587~5331Hz(应为 16000), - // 按 16k 播放就是又快又糊;而走原生录制的 trans_holder_* 稳定在 16000。 - // - // 用 Future 链把写入排成队列:既不丢帧,也保证写入顺序与到达顺序一致 - // (顺序乱了音频同样是糊的)。 - // ⚠️ 电平只是波形显示,**绝不能挡住写入**:上一版把它放在入队之前且没包 - // try,它一抛异常整个 _onPcm 就中断,音频一帧都写不进去。 - _writeChain = _writeChain.then((_) async { - final f = _raf; - if (f == null) return; - try { - await f.writeFrom(pcm); - _dataBytes += pcm.length; - _writtenFrames++; - try { - _updateLevel(pcm); - } catch (e) { - // 波形挂了不影响录音本身,吞掉即可 - } - } catch (e) { - _droppedFrames++; - Logger.e(_tag, '写入 PCM 失败(已丢帧 $_droppedFrames): $e'); - } - }); - } - - void _sample() { - final list = waveform.toList(); - list.add(level.value); - if (list.length > _maxSamples) { - list.removeRange(0, list.length - _maxSamples); - } - waveform.assignAll(list); - } - - void _updateLevel(Uint8List pcm) { - if (pcm.lengthInBytes < 2) return; - // ⚠️ **不能用 `buffer.asInt16List(offsetInBytes, …)`**:原生过来的 Uint8List - // 是带 offset 的视图,偏移是奇数时这行直接抛 - // `RangeError: Offset must be a multiple of BYTES_PER_ELEMENT`。 - // 实测这条异常在一次会话里抛了 6990 次——通话录音那边早就踩过并改成了 - // 按字节搬(见 BesCallRecordingService._writeStereo 的说明), - // 现场录音这条链路一直没同步修。 - // 直接按小端手工拼 int16,没有对齐要求。 - final count = pcm.lengthInBytes ~/ 2; - if (count == 0) return; - var sum = 0.0; - for (var i = 0; i < count; i++) { - final lo = pcm[i * 2]; - final hi = pcm[i * 2 + 1]; - var v = (hi << 8) | lo; - if (v >= 0x8000) v -= 0x10000; // 转有符号 - final n = v / 32768.0; - sum += n * n; - } - final rms = math.sqrt(sum / count); - level.value = math.sqrt(rms).clamp(0.0, 1.0); - } - - Future _closeFile({required bool save}) async { - await _audioSub?.cancel(); - _audioSub = null; - final raf = _raf; - _raf = null; - if (raf == null) return; - try { - if (save) { - await raf.setPosition(0); - await raf.writeFrom(_wavHeader(_dataBytes)); - } - await raf.close(); - } catch (e) { - Logger.e(_tag, '关闭录音文件失败: $e'); - } - } - - /// 归档进语音记录助手。具体入库逻辑见 [RecordingArchive]—— - /// 现场录音 / 通话录音 / 翻译录音都走同一套,不再各抄一份。 - Future _archive(String path, int seconds) async { - await RecordingArchive.archive(path: path, seconds: seconds, tag: _tag); - } - - Uint8List _wavHeader(int dataLength) { - const sampleRate = 16000, channels = 1, bits = 16; - final byteRate = sampleRate * channels * (bits ~/ 8); - final h = ByteData(44); - void ascii(int off, String s) { - for (var i = 0; i < s.length; i++) { - h.setUint8(off + i, s.codeUnitAt(i)); - } - } - - ascii(0, 'RIFF'); - h.setUint32(4, 36 + dataLength, Endian.little); - ascii(8, 'WAVE'); - ascii(12, 'fmt '); - h.setUint32(16, 16, Endian.little); - h.setUint16(20, 1, Endian.little); - h.setUint16(22, channels, Endian.little); - h.setUint32(24, sampleRate, Endian.little); - h.setUint32(28, byteRate, Endian.little); - h.setUint16(32, channels * (bits ~/ 8), Endian.little); - h.setUint16(34, bits, Endian.little); - ascii(36, 'data'); - h.setUint32(40, dataLength, Endian.little); - return h.buffer.asUint8List(); - } -} diff --git a/apps/client/lib/data/services/ble_manager.dart b/apps/client/lib/data/services/ble_manager.dart deleted file mode 100644 index 5e1fee04..00000000 --- a/apps/client/lib/data/services/ble_manager.dart +++ /dev/null @@ -1,1073 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:device_jieli/device_jieli.dart'; -import 'package:agent_service/agent_service.dart'; - -import '/data/models/appconfig_model.dart'; -import '../models/user_Info.dart' as userInfo; -import '../../data/models/appconfig.dart'; -import '../../core/translations/app_translations.dart'; -import '../../core/utils/logger.dart'; -import 'language_manager.dart'; -import 'network/dio_manager.dart'; -import '../../routes/app_routes.dart'; - -/// BLE 连接状态常量(保持外部 API 兼容) -class BleConnectionState { - static const int disconnected = 0; - static const int connecting = 1; - static const int connected = 2; - static const int disconnecting = 3; - static const int error = -1; -} - -/// 设备信息缓存(精简后版本,去掉唤醒词字段) -class DeviceInfo { - final String address; - final String name; - final int batteryLevel; - final int leftEarBattery; - final int rightEarBattery; - final int caseBattery; - final bool leftEarCharging; - final bool rightEarCharging; - final bool caseCharging; - final String firmwareVersion; - final String firmwareProductId; - final String firmwareProductColor; - final DateTime lastUpdated; - - DeviceInfo({ - required this.address, - required this.name, - this.batteryLevel = 0, - this.leftEarBattery = 0, - this.rightEarBattery = 0, - this.caseBattery = 0, - this.leftEarCharging = false, - this.rightEarCharging = false, - this.caseCharging = false, - this.firmwareVersion = '', - this.firmwareProductId = '', - this.firmwareProductColor = '', - DateTime? lastUpdated, - }) : lastUpdated = lastUpdated ?? DateTime.now(); - - DeviceInfo copyWith({ - String? address, - String? name, - int? batteryLevel, - int? leftEarBattery, - int? rightEarBattery, - int? caseBattery, - bool? leftEarCharging, - bool? rightEarCharging, - bool? caseCharging, - String? firmwareVersion, - String? firmwareProductId, - String? firmwareProductColor, - }) { - return DeviceInfo( - address: address ?? this.address, - name: name ?? this.name, - batteryLevel: batteryLevel ?? this.batteryLevel, - leftEarBattery: leftEarBattery ?? this.leftEarBattery, - rightEarBattery: rightEarBattery ?? this.rightEarBattery, - caseBattery: caseBattery ?? this.caseBattery, - leftEarCharging: leftEarCharging ?? this.leftEarCharging, - rightEarCharging: rightEarCharging ?? this.rightEarCharging, - caseCharging: caseCharging ?? this.caseCharging, - firmwareVersion: firmwareVersion ?? this.firmwareVersion, - firmwareProductId: firmwareProductId ?? this.firmwareProductId, - firmwareProductColor: firmwareProductColor ?? this.firmwareProductColor, - lastUpdated: DateTime.now(), - ); - } - - @override - String toString() => - 'DeviceInfo(addr: $address, name: $name, battery: $batteryLevel%, fw: $firmwareVersion)'; -} - -/// 杰理蓝牙管理器(替换旧的 ble_service 实现,对外保留 BleManager 名字) -class BleManager extends GetxService { - static BleManager get to => Get.find(); - - // ─── 连接信息 ──────────────────────────────────────── - String? currentDeviceAddress; - String? _currentDeviceName; - JieliDevice? _connectedDevice; - JieliDevice? get connectedDevice => _connectedDevice; - - final _connectionState = BleConnectionState.disconnected.obs; - int get connectionState => _connectionState.value; - RxInt get connectionStateRx => _connectionState; - - final _isCall = false.obs; - final _isConnected = false.obs; - bool get isConnected => _isConnected.value; - bool get isCall => _isCall.value; - - final deviceInfo = Rx(null); - - // ─── 通话翻译/编解码器(杰理 startTranslation 模式状态)───── - final _isCodecActive = false.obs; - bool get isCodecActive => _isCodecActive.value; - RxBool get isCodecActiveRx => _isCodecActive; - - // 设备主动上报通话翻译开关信号(耳机端按键) - final StreamController _callTranslationRequestController = - StreamController.broadcast(); - Stream get onCallTranslationRequest => - _callTranslationRequestController.stream; - - // 通话翻译模式下,杰理 runtime 解码后推上来的 PCM: - // - in.uplink = 本机用户麦克风(己方/A 路) - // - in.downlink = 对端通话音(对方/B 路) - // 由 translation_controller 转发到 azure_speech 的 AST 流水线。 - final StreamController _inUplinkPcmController = - StreamController.broadcast(); - final StreamController _inDownlinkPcmController = - StreamController.broadcast(); - Stream get inUplinkPcmStream => _inUplinkPcmController.stream; - Stream get inDownlinkPcmStream => _inDownlinkPcmController.stream; - - // ─── 扫描结果(供设备页扫描列表使用)───────────────────── - final RxList scannedDevices = [].obs; - final RxBool isScanning = false.obs; - - /// 每个地址最近一次扫到的 advertisement(电量等)。 - /// scannedDevices 只追加不更新,但电量在每次 advertisement 都可能变; - /// 这里保留最新一次,供连接成功时填到 DeviceInfo。 - final Map _latestAdvByAddress = {}; - - StreamSubscription? _eventSub; - final GetStorage _storage = GetStorage(); - - Future? _initFuture; - bool _initSucceeded = false; - String? _initError; - String? get initError => _initError; - bool get isInitialized => _initSucceeded; - - /// 确保杰理 SDK native 端已 initialize。失败则重试一次。 - /// 失败时抛出 [StateError] —— 调用方负责 try-catch / UI 反馈。 - Future ensureInitialized() async { - if (_initSucceeded) return; - // 上一次 future 还在 in-flight 或失败 → 重新启动一次 - if (_initFuture == null) { - _initFuture = _initializeJieli(); - } - try { - await _initFuture!.timeout(const Duration(seconds: 8)); - } catch (e) { - _initError = e.toString(); - _initFuture = null; // 允许下次重试 - throw StateError('杰理 SDK 初始化失败: $_initError'); - } - if (!_initSucceeded) { - _initFuture = null; - throw StateError('杰理 SDK 初始化失败: ${_initError ?? "unknown"}'); - } - } - - /// 兼容旧 API(部分调用点不关心初始化结果) - Future get ready async { - try { - await ensureInitialized(); - } catch (_) { - // 静默:让后续 native 调用自身报错 - } - } - - BleManager() { - print('[BLE] BleManager 构造,立即启动杰理 SDK 初始化'); - _initFuture = _initializeJieli(); - _setupEventListener(); - _startConnectionWatchdog(); - } - - @override - void onInit() { - super.onInit(); - print('[BLE] BleManager.onInit (init 已在构造时启动)'); - } - - @override - void onClose() { - _eventSub?.cancel(); - _stopBatteryWatchdog(); - _stopConnectionWatchdog(); - _callTranslationRequestController.close(); - _inUplinkPcmController.close(); - _inDownlinkPcmController.close(); - super.onClose(); - } - - /// 把外部 AST 服务返回的 TTS PCM 灌回耳机。 - /// - leg=='A' / 'uplink' → 对端听(SOURCE_E_SCO_UP_LINK) - /// - leg=='B' / 'downlink' → 本机用户听(SOURCE_E_SCO_DOWN_LINK) - /// - /// 拿到多少就送多少,不做节流也不补静音;底层 SDK / RCSP runtime 自己管 - /// 整段 OPUS 编码 + 串行 writeAudioData 节奏。 - Future feedAstTtsPcm({ - required String leg, - required Uint8List pcm, - int sampleRate = 16000, - int channels = 1, - bool isFinal = false, - }) async { - // 段尾帧(pcm 为空 + isFinal=true)必须放行,让 native 触发立即整段下发; - // 仅对“普通空帧(非段尾)”短路。 - if (pcm.isEmpty && !isFinal) return true; - final upper = leg.toUpperCase(); - final streamId = (upper == 'A' || upper == 'UPLINK') - ? 'out.uplink' - : 'out.downlink'; - final isFlush = isFinal; - try { - final ok = await Jielihome.instance.feedTranslatedAudio( - streamId: streamId, - pcm: pcm, - sampleRate: sampleRate, - channels: channels, - isFinal: isFinal, - ); - if (isFlush || !ok) { - // 只在段尾 flush 或 native 返回 false 时打,避免刷屏。 - Logger.info( - '[BleManager] feedAstTtsPcm leg=$leg streamId=$streamId pcm=${pcm.length}B isFinal=$isFinal nativeOk=$ok', - ); - } - return ok; - } catch (e) { - Logger.w('BleManager', 'feedAstTtsPcm leg=$leg failed: $e'); - return false; - } - } - - Future _initializeJieli() async { - try { - print('[BLE] [INIT] 调用 Jielihome.initialize ...'); - // 与参考项目 ai-agent-client 保持一致:使用 SDK 默认配置(multiDevice=true, skipNoNameDev=false) - // 不要清空 BluetoothOption.scanFilterData —— 那是杰理协议广播标识,清空反而扫不到 - await Jielihome.instance.initialize(); - _initSucceeded = true; - _initError = null; - print('[BLE] [INIT] 杰理 SDK 初始化完成'); - } catch (e, s) { - _initSucceeded = false; - _initError = e.toString(); - print('[BLE] [INIT] 杰理 SDK 初始化失败: $e\n$s'); - rethrow; - } - } - - void _setupEventListener() { - print('[BLE] [EVT] 准备订阅 Jielihome.events ...'); - _eventSub?.cancel(); - _eventSub = Jielihome.instance.events.listen( - (event) { - // TranslationAudioEvent 频率极高(10ms/帧),最先拦截并单独走节流日志处理器, - // 避免进入下面的 print / if‑链路经引发刷屏。 - if (event is TranslationAudioEvent) { - _handleTranslationAudio(event); - return; - } - // 总开关日志:每一个事件都打一次,便于诊断 native → dart 通道是否畅通 - print('[BLE] [EVT] ${event.runtimeType}'); - if (event is AdapterStatusEvent) { - print('[BLE] [EVT] 适配器状态 enabled=${event.enabled} hasBle=${event.hasBle}'); - } else if (event is ScanStatusEvent) { - print('[BLE] [EVT] 扫描状态变化 ble=${event.ble} started=${event.started}'); - isScanning.value = event.started; - } else if (event is DeviceFoundEvent) { - final found = event.device; - final exists = scannedDevices.any((d) => d.address == found.address); - // 每次广播都用最新数据覆盖(后续若 SDK 扩展了广播字段,仍可从这里拿到) - _latestAdvByAddress[found.address] = found; - print('[BLE] [EVT] deviceFound name="${found.name}" addr=${found.address} ' - 'rssi=${found.rssi} ' - '(已存在=$exists, 当前列表 size=${scannedDevices.length})'); - if (!exists) { - scannedDevices.add(found); - print('[BLE] [EVT] 新设备已加入列表,size=${scannedDevices.length}'); - } - } else if (event is ConnectionStateEvent) { - _handleConnectionState(event); - } else if (event is RcspInitEvent) { - print('[BLE] [EVT] RCSP 初始化: ${event.address} success=${event.success}'); - if (event.success) { - // RCSP init 完成后 SDK 才开始往缓存里填电量;这里启动轮询直到拿到非 0 - _startBatteryWatchdog(); - // 同时启动 AgentService(azureAsrHelper / TTS / chatApi 在这里建出来)。 - // native initialize 自带幂等保护,重连重复触发是 no-op; - // 老的 PairingController 删除时这一调用丢了,不补回来 AI 助理 / PTT - // 等所有 ASR 路径都会因为 azureAsrHelper=null 静默失败。 - startAgentService().then((ok) { - Logger.i('BleManager', 'startAgentService on RCSP init: ok=$ok'); - }).catchError((e) { - Logger.e('BleManager', 'startAgentService on RCSP init 失败: $e'); - }); - } - } else if (event is BatteryEvent) { - Logger.i('BleManager', - '[EVT] BatteryEvent addr=${event.address} level=${event.level}'); - _handleBattery(event); - } else if (event is PhoneCallStatusEvent) { - _isCall.value = event.status != 0; - } else if (event is ExpandFunctionEvent) { - _handleExpandFunction(event); - } else if (event is TranslationLogEvent) { - print('[BLE] [EVT] TranslationLog modeId=${event.modeId} ${event.content}'); - } else if (event is TranslationErrorEvent) { - print('[BLE] [EVT] TranslationError modeId=${event.modeId} ' - 'code=${event.code} msg=${event.message}'); - } else if (event is UnknownJieliEvent) { - print('[BLE] [EVT] 未识别事件: ${event.raw}'); - } - }, - onError: (e) => print('[BLE] [EVT] 杰理事件流错误: $e'), - onDone: () => print('[BLE] [EVT] 杰理事件流被关闭 (onDone)'), - ); - print('[BLE] [EVT] 订阅完成 sub=$_eventSub'); - } - - void _handleConnectionState(ConnectionStateEvent e) { - // 关键诊断日志:原生 → Dart 链路上的最终一站。如果这里没打印,意味着 - // 原生侧 `connectionState` 事件根本没到达 Dart(看 iOS `[JieliDispatcher] - // send #N type=connectionState` 是否成功 sink-ok)。 - print('[BLE] [EVT] >>> _handleConnectionState addr=${e.address} state=${e.state} ' - '(0=disconnect 1=ok 2=connecting)'); - Logger.i('BleManager', 'JL 连接状态: ${e.address} → ${e.state}'); - if (e.state == ConnectionStateEvent.connectionOk) { - currentDeviceAddress = e.address; - _connectionState.value = BleConnectionState.connected; - _isConnected.value = true; - print('[BLE] [EVT] >>> isConnected 翻成 true, currentDeviceAddress=${e.address}'); - _ensureDeviceInfoStub(); - // 用扫描期间最新一条 advertisement 立刻填上电量(SDK BleScanMessage 里就有 - // leftDeviceQuantity / rightDeviceQuantity / chargingBinQuantity / 各 charging 位) - _fillBatteryFromAdvertisement(e.address); - // 不在这里 fetchDeviceInfo —— RCSP 还没 init,SDK 缓存里的电量是 0; - // 改到 RcspInitEvent 成功时再拉一次 + 之后再轮询几次保险。 - } else if (e.state == ConnectionStateEvent.connectionConnecting) { - _connectionState.value = BleConnectionState.connecting; - print('[BLE] [EVT] >>> 连接中 (state=2)'); - } else { - _connectionState.value = BleConnectionState.disconnected; - _isConnected.value = false; - _isCodecActive.value = false; - _stopBatteryWatchdog(); - print('[BLE] [EVT] >>> isConnected 翻成 false (state=${e.state})'); - } - } - - // 连接 + RCSP init 之后 SDK 缓存才开始填电量;BatteryEvent 在某些设备上 - // 又只在变化时才推。所以连上后主动轮询几次拉快照,拿到非 0 就停。 - Timer? _batteryWatchdog; - int _batteryWatchdogTicks = 0; - static const int _batteryWatchdogMaxTicks = 12; // 12 × 2.5s = 30s - static const Duration _batteryWatchdogInterval = Duration(milliseconds: 2500); - - void _startBatteryWatchdog() { - _stopBatteryWatchdog(); - _batteryWatchdogTicks = 0; - Logger.i('BleManager', '[BAT] 启动电量轮询'); - // 立刻先拉一次,再启周期任务 - Future.microtask(fetchDeviceInfo); - _batteryWatchdog = Timer.periodic(_batteryWatchdogInterval, (t) async { - _batteryWatchdogTicks++; - final lvl = deviceInfo.value?.batteryLevel ?? 0; - if (lvl > 0) { - Logger.i('BleManager', - '[BAT] 拿到电量=$lvl%,停止轮询 (tick=$_batteryWatchdogTicks)'); - _stopBatteryWatchdog(); - return; - } - if (_batteryWatchdogTicks >= _batteryWatchdogMaxTicks) { - Logger.w('BleManager', - '[BAT] 30s 内未拿到电量,停止轮询 (tick=$_batteryWatchdogTicks)'); - _stopBatteryWatchdog(); - return; - } - Logger.d('BleManager', - '[BAT] 轮询拉快照 tick=$_batteryWatchdogTicks'); - await fetchDeviceInfo(); - }); - } - - void _stopBatteryWatchdog() { - _batteryWatchdog?.cancel(); - _batteryWatchdog = null; - } - - // 连接状态 watchdog:杰理 SDK 在某些时序下不会回调 ConnectionStateEvent - // (冷启时设备已经连着、事件订阅 race、原生侧自动重连等),导致本地 Rx - // 状态停在 disconnected 但 SDK 内部已经有连接。这里定期直接问 SDK。 - // - // 重要:只做"单向"同步(disconnected → connected),**不**把已连状态翻回 - // disconnected。原因:multi-device 模式下 connectedDevice() 偶发会返回 null, - // 上一版把它当真翻成 disconnected 反而把已连接 UI 抹掉了。断开仍走 - // ConnectionStateEvent.connectionDisconnect 那条事件路径。 - Timer? _connectionWatchdog; - static const Duration _connectionWatchdogInterval = Duration(seconds: 5); - - void _startConnectionWatchdog() { - _connectionWatchdog?.cancel(); - _connectionWatchdog = Timer.periodic(_connectionWatchdogInterval, (_) { - _reconcileConnectionFromSdk(); - }); - } - - void _stopConnectionWatchdog() { - _connectionWatchdog?.cancel(); - _connectionWatchdog = null; - } - - Future _reconcileConnectionFromSdk() async { - if (!_initSucceeded) return; - try { - // 步骤 1:尽量找到一个真实在连的 address。 - // 优先:本地已有 currentDeviceAddress → 直接 isConnected(addr) 校验。 - // 兜底:connectedDevice() 看 SDK 是否能给出当前已连设备。 - String? sdkConnectedAddr; - String? sdkConnectedName; - final localAddr = currentDeviceAddress; - if (localAddr != null && localAddr.isNotEmpty) { - if (await Jielihome.instance.isConnected(localAddr)) { - sdkConnectedAddr = localAddr; - sdkConnectedName = _currentDeviceName; - } - } - if (sdkConnectedAddr == null) { - final dev = await Jielihome.instance.connectedDevice(); - if (dev != null && dev.address.isNotEmpty) { - sdkConnectedAddr = dev.address; - sdkConnectedName = dev.name; - } - } - - if (sdkConnectedAddr == null) return; // 没拿到,本轮放弃,**不**翻 _isConnected - - // 步骤 2:本地 Rx 与 SDK 不一致 → 强制同步到「已连」。 - final wasConnected = _isConnected.value; - final addressChanged = currentDeviceAddress != sdkConnectedAddr; - if (!wasConnected || addressChanged) { - Logger.w('BleManager', - '[WATCHDOG] 本地 isConnected=$wasConnected addr=$currentDeviceAddress' - ' / SDK 在连 $sdkConnectedAddr → 同步为 connected'); - currentDeviceAddress = sdkConnectedAddr; - if (sdkConnectedName != null && sdkConnectedName.isNotEmpty) { - _currentDeviceName = sdkConnectedName; - } - _isConnected.value = true; - _connectionState.value = BleConnectionState.connected; - _ensureDeviceInfoStub(); - } - - // 步骤 3:兜底电量。冷启场景下 RcspInitEvent 不会再来, - // _startBatteryWatchdog 永远不被触发,电量 UI 一直 0。 - // 这里发现 SDK 在连且本地电量为 0 时,主动启动一次电量轮询。 - final lvl = deviceInfo.value?.batteryLevel ?? 0; - if (lvl == 0 && _batteryWatchdog == null) { - Logger.i('BleManager', '[WATCHDOG] 已连但电量=0,启动电量轮询补抓'); - _startBatteryWatchdog(); - } - } catch (e) { - Logger.w('BleManager', '[WATCHDOG] 查询 SDK 连接状态失败: $e'); - } - } - - void _handleBattery(BatteryEvent e) { - final level = e.level ?? 0; - _ensureDeviceInfoStub(); - // 杰理 SDK 的 BatteryInfo 只有总电量;左右耳/盒子的精细数据来自 BleScanMessage(见 _fillBatteryFromAdvertisement)。 - // 这里更新总电量;如果当前左右耳还是 0,也镜像一下作为兜底。 - final cur = deviceInfo.value; - if (cur == null) return; - deviceInfo.value = cur.copyWith( - batteryLevel: level, - leftEarBattery: cur.leftEarBattery > 0 ? cur.leftEarBattery : level, - rightEarBattery: cur.rightEarBattery > 0 ? cur.rightEarBattery : level, - ); - } - - /// 杰理同步后的 `JieliDevice` 仅暴露 name / address / rssi 等基础字段, - /// 不再提供左右耳/仓的电量与充电状态。电量改由以下两路拿: - /// 1. 连接成功后 RCSP 初始化完成 → `_startBatteryWatchdog` 轮询 - /// `fetchDeviceInfo` 拉 `deviceSnapshot` / `queryTargetInfo`; - /// 2. SDK 主动推送的 [BatteryEvent](见 `_handleBattery`)。 - /// 所以这里不再从扫描广播里预填电量,保留空实现以兼容既有调用点。 - void _fillBatteryFromAdvertisement(String address) { - final adv = _latestAdvByAddress[address]; - if (adv == null) { - Logger.w('BleManager', '[BAT] 没有 $address 的 advertisement 数据可用'); - return; - } - Logger.i('BleManager', - '[BAT] 广播不再携带电量字段,连接后将通过 RCSP/BatteryEvent 补齐'); - } - - int _uplinkRxCount = 0; - int _downlinkRxCount = 0; - - void _handleTranslationAudio(TranslationAudioEvent e) { - // streamId 来自 native TranslationStreams:in.uplink / in.downlink / in.mic ... - // 仅转发通话翻译两条输入流;out.* 由这里产生回灌指令而不是接收。 - switch (e.streamId) { - case 'in.uplink': - _uplinkRxCount++; - if (_uplinkRxCount <= 5 || _uplinkRxCount % 100 == 1) { - Logger.i('CALL-RX', - '上行 #$_uplinkRxCount len=${e.pcm.length} sr=${e.sampleRate} ch=${e.channels} bits=${e.bitsPerSample} seq=${e.seq}'); - } - if (!_inUplinkPcmController.isClosed) { - _inUplinkPcmController.add(e.pcm); - } - break; - case 'in.downlink': - _downlinkRxCount++; - if (_downlinkRxCount <= 5 || _downlinkRxCount % 100 == 1) { - Logger.i('CALL-RX', - '下行 #$_downlinkRxCount len=${e.pcm.length} sr=${e.sampleRate} ch=${e.channels} bits=${e.bitsPerSample} seq=${e.seq}'); - } - if (!_inDownlinkPcmController.isClosed) { - _inDownlinkPcmController.add(e.pcm); - } - break; - } - } - - void _handleExpandFunction(ExpandFunctionEvent e) { - if (e.opCode == 0x16) { - _emitCallTranslationRequest(true); - } else if (e.opCode == 0x17) { - _emitCallTranslationRequest(false); - } - } - - void _emitCallTranslationRequest(bool enable) { - Logger.i('BleManager', - '[CALL_TRANS_CMD] 设备上报${enable ? "开启" : "关闭"}通话翻译'); - if (!_callTranslationRequestController.isClosed) { - _callTranslationRequestController.add(enable); - } - if (enable) { - final currentRoute = Get.currentRoute; - if (currentRoute != Routes.translation) { - Get.toNamed( - Routes.translation, - arguments: {'translationId': 'call', 'autoStart': true}, - ); - } - } - } - - void _ensureDeviceInfoStub() { - if (deviceInfo.value == null && currentDeviceAddress != null) { - deviceInfo.value = DeviceInfo( - address: currentDeviceAddress!, - name: _currentDeviceName ?? '', - ); - } - } - - // ─── 扫描 / 连接 ──────────────────────────────────── - /// 启动扫描。 - /// - /// 扫描的 service UUID 白名单来源: - /// 1) 调用方显式传入 [uuids](通常用于调试或定向扫描); - /// 2) 否则根据 [productIds] 从 [AppConfig.config.products] 映射出每个产品 - /// 的 `scanuuid`(服务端下发的产品配置),在 Flutter 侧拼接成 uuids 列表 - /// 再通过平台通道下发给底层,由 Android `ScanFilter.setServiceUuid` - /// 进行 OS 级过滤; - /// 3) 如果上面两项都没给,就默认使用 [AppConfig.allproductUuids()],等价 - /// 于「下发所有已配置产品」的 uuid 白名单。 - /// - /// 空列表 / AppConfig 未初始化时,底层不做 uuid 过滤,全部放行。 - Future startScan({ - Duration timeout = const Duration(seconds: 30), - List? uuids, - List? productIds, - }) async { - print('[BLE] [SCAN] BleManager.startScan 入口 ' - 'productIds=$productIds uuids=$uuids'); - _lastScanError = null; - try { - await ensureInitialized(); - } catch (e) { - print('[BLE] [SCAN] 杰理 SDK 未初始化: $e'); - _lastScanError = e.toString(); - isScanning.value = false; - return false; - } - - final List resolvedUuids; - if (uuids != null && uuids.isNotEmpty) { - resolvedUuids = - uuids.where((u) => u.isNotEmpty).toList(growable: false); - } else if (productIds != null && - productIds.isNotEmpty && - AppConfig.isInitialized()) { - final ids = productIds.toSet(); - resolvedUuids = AppConfig.config.products - .where((p) => ids.contains(p.id)) - .map((p) => p.scanuuid) - .where((u) => u.isNotEmpty) - .toList(growable: false); - } else if (AppConfig.isInitialized()) { - resolvedUuids = AppConfig.allproductUuids() - .where((u) => u.isNotEmpty) - .toList(growable: false); - } else { - resolvedUuids = const []; - } - - print('[BLE] [SCAN] init 已通过,UUID 白名单=$resolvedUuids'); - scannedDevices.clear(); - isScanning.value = true; - try { - await Jielihome.instance.startScan( - timeout: timeout, - uuidList: resolvedUuids, - skipUnnamed: true, - ); - print('[BLE] [SCAN] native startScan 返回成功,等待 deviceFound 事件'); - return true; - } catch (e, s) { - print('[BLE] [SCAN] 启动扫描失败: $e\n$s'); - isScanning.value = false; - _lastScanError = e.toString(); - return false; - } - } - - String? _lastScanError; - String? get lastScanError => _lastScanError; - - Future stopScan() async { - await ready; - try { - await Jielihome.instance.stopScan(); - } catch (e) { - Logger.e('BleManager', '停止扫描失败: $e'); - } finally { - isScanning.value = false; - } - } - - Future connectDevice(JieliDevice device) async { - await ready; - _currentDeviceName = device.name; - currentDeviceAddress = device.address; - _connectedDevice = device; - _connectionState.value = BleConnectionState.connecting; - try { - await Jielihome.instance.connect(device); - } catch (e) { - Logger.e('BleManager', '连接设备失败: $e'); - _connectionState.value = BleConnectionState.error; - } - } - - Future disconnect() async { - await ready; - try { - final addr = currentDeviceAddress; - if (addr != null) { - await Jielihome.instance.disconnect(addr); - } - } catch (e) { - Logger.e('BleManager', '断开连接失败: $e'); - } - } - - Future getConnectionState() async { - final addr = currentDeviceAddress; - if (addr == null) return BleConnectionState.disconnected; - try { - final connected = await Jielihome.instance.isConnected(addr); - _connectionState.value = - connected ? BleConnectionState.connected : BleConnectionState.disconnected; - _isConnected.value = connected; - return _connectionState.value; - } catch (e) { - Logger.e('BleManager', '查询连接状态失败: $e'); - return BleConnectionState.error; - } - } - - Future isBleConnected() async { - final addr = currentDeviceAddress; - if (addr == null) return false; - try { - return await Jielihome.instance.isConnected(addr); - } catch (_) { - return false; - } - } - - /// 探查当前是否已经有杰理设备连着(用于登录/启动后判断是否需要跳转扫描页)。 - /// 通过 [Jielihome.connectedDevice] 同步 SDK 已知的连接状态。 - Future hasActiveConnection() async { - await ready; - try { - final dev = await Jielihome.instance.connectedDevice(); - if (dev != null && dev.address.isNotEmpty) { - currentDeviceAddress = dev.address; - _currentDeviceName = dev.name; - _connectedDevice = dev; - _isConnected.value = true; - _connectionState.value = BleConnectionState.connected; - _ensureDeviceInfoStub(); - return true; - } - } catch (e) { - Logger.w('BleManager', 'hasActiveConnection 探查失败: $e'); - } - return false; - } - - // ─── 设备信息 ─────────────────────────────────────── - String getDeviceName() { - return deviceInfo.value?.name ?? _currentDeviceName ?? ''; - } - - Future fetchDeviceInfo() async { - final addr = currentDeviceAddress; - if (addr == null) return; - try { - final snap = await Jielihome.instance.deviceSnapshot(addr) ?? {}; - final name = (snap['name'] as String?) ?? _currentDeviceName ?? ''; - final battery = (snap['battery'] as num?)?.toInt() ?? - (snap['batteryLevel'] as num?)?.toInt() ?? - 0; - final fw = (snap['firmwareVersion'] as String?) ?? - (snap['version'] as String?) ?? - ''; - final pid = (snap['productId'] as String?) ?? ''; - _ensureDeviceInfoStub(); - // 同 _handleBattery:杰理 SDK 只暴露单个总电量,把它同时镜像到左右耳, - // home_view 才能从 leftEarBattery / rightEarBattery 读到非零值。 - deviceInfo.value = (deviceInfo.value ?? DeviceInfo(address: addr, name: name)) - .copyWith( - name: name, - batteryLevel: battery, - leftEarBattery: battery, - rightEarBattery: battery, - firmwareVersion: fw, - firmwareProductId: pid, - ); - Logger.i('BleManager', - '设备快照: name=$name battery=$battery fw=$fw pid=$pid'); - } catch (e) { - Logger.w('BleManager', '获取设备快照失败: $e'); - } - } - - Future fetchBatteryInfo() async => true; - Future fetchDeviceVersion() async => true; - Future getProductId() async => true; - Future getBatteryLevel() async => - deviceInfo.value?.batteryLevel ?? 0; - - void clearDeviceInfo() { - deviceInfo.value = null; - currentDeviceAddress = null; - _currentDeviceName = null; - _connectedDevice = null; - } - - // ─── 编解码器 / 通话翻译模式 ─────────────────────────── - // 杰理:openA2DPDecoder/closeCodec 映射到 startTranslation/stopTranslation。 - // 其余 codec 入口按现有调用语义保留乐观成功,以保持上层兼容。 - - Future openEncoder() async { - _isCodecActive.value = true; - return true; - } - - Future openDecoder() async { - _isCodecActive.value = true; - return true; - } - - Future openA2DPDecoder() async { - Logger.i('CALL_TRANS_CMD', '【启动】通话翻译模式 (杰理 startTranslation modeId=3)'); - try { - await Jielihome.instance.startTranslation( - modeId: TranslationModeIds.callTranslation, - ); - _isCodecActive.value = true; - Logger.i('CALL_TRANS_CMD', '✅【启动成功】通话翻译模式已下发 — 接下来等耳机回上行音频'); - return true; - } catch (e) { - Logger.e('CALL_TRANS_CMD', '❌【启动失败】通话翻译模式: $e'); - _isCodecActive.value = false; - return false; - } - } - - Future openCallRecordDecoder() async { - _isCodecActive.value = true; - return true; - } - - Future closeCodec() async { - _isCodecActive.value = false; - try { - await Jielihome.instance.stopTranslation(); - } catch (_) { - // 静默失败:未启动时调 stop 是常态 - } - return true; - } - - Future playA2DP() async => true; - - // ─── OTA ─────────────────────────────────────────── - /// 杰理 OTA 通过 Jielihome.otaStart 触发,此处保留空实现以兼容旧调用点。 - Future setEnterOta(bool enterOta) async {} - - // ─── 兼容存根 ─────────────────────────────────────── - Future requestBluetoothPermission() async => true; - - /// 旧 Companion 关联 / 后台扫描接口已废弃 — 保留空实现以兼容 - /// 即将被移除的调用点(pairing 模块已删除)。 - Future clearPairedDevices() async { - await disconnect(); - return true; - } - - // ─── AgentService 启动 / 停止(保持原逻辑) ─────────── - Future startAgentService() async { - try { - var serverurl = DioManager().baseUrl; - var jsonString = _storage.read("logintoken"); - - final azureSpeechKey = AppConfig.env('AZURE_SPEECH_KEY') ?? ''; - final azureSpeechRegion = - AppConfig.env('AZURE_SPEECH_REGION') ?? 'eastasia'; - final openaiApiKey = AppConfig.env('OPENAI_API_KEY') ?? ''; - final openaiBaseUrl = - AppConfig.env('OPENAI_BASE_URL') ?? 'https://api.openai.com/v1'; - final openaiModel = AppConfig.env('OPENAI_MODEL') ?? 'gpt-3.5-turbo'; - final openaiModelMedia = AppConfig.env('OPENAI_API_VISION_MODEL') ?? - 'doubao-1-5-vision-pro-32k-250115'; - final openaiagentid = - AppConfig.env('OPENAI_AGENT_ID') ?? 'personal_assistant'; - final systemPrompt = AppConfig.agentsystemPrompt(openaiagentid); - var musicServiceType = - AppConfig.env('APP_ANDROID_MUSICSERVICE_TYPE') ?? 'AUTO'; - if (Platform.isIOS) { - musicServiceType = AppConfig.env('APP_IOS_MUSICSERVICE_TYPE') ?? 'AUTO'; - } - final navigationMode = - AppConfig.env('APP_NAVIGATION_MODE') ?? 'external_auto'; - final xunfeiAppId = AppConfig.env('XUNFEI_ASR_APP_ID') ?? ''; - final xunfeiAccessKeyId = AppConfig.env('XUNFEI_ASR_ACCESS_KEY_ID') ?? ''; - final xunfeiAccessKeySecret = - AppConfig.env('XUNFEI_ASR_ACCESS_KEY_SECRET') ?? ''; - final jsonData = AppConfig.mcpConfig(); - Logger.info('启动 AgentService,musicServiceType: $musicServiceType'); - final isTtsInterruptEnabled = await _loadTtsInterrupe(); - - final vipExpiryTime = userInfo.User.instance.vipexptime; - - List supportedLanguages = []; - try { - final storage = GetStorage(); - final languageManager = Get.find(); - String? savedLanguage = storage.read('language'); - - if (savedLanguage != null && savedLanguage != 'system') { - final parts = savedLanguage.split('_'); - if (parts.length == 2) { - final lang = parts[0].toLowerCase(); - final country = parts[1].toUpperCase(); - - if (lang == 'zh' && - (country == 'HK' || country == 'TW' || country == 'CN')) { - supportedLanguages.add('zh-$country'); - } else if (lang == 'tl' && country == 'PH') { - supportedLanguages.add('fil-PH'); - } else { - supportedLanguages.add('$lang-$country'); - } - } - } else { - final appLocale = Get.locale ?? AppTranslations.deviceLocale(); - if (appLocale != null) { - final lang = appLocale.languageCode.toLowerCase(); - final country = appLocale.countryCode; - - if (lang.toLowerCase() == 'zh' && - country != null && - country.isNotEmpty && - (country.toUpperCase() == 'HK' || - country.toUpperCase() == 'TW' || - country.toUpperCase() == 'CN')) { - supportedLanguages.add('zh-${country.toUpperCase()}'); - } else { - if (country != null && country.isNotEmpty) { - final normalizedCountry = country.toUpperCase(); - if (lang == 'tl' && normalizedCountry == 'PH') { - supportedLanguages.add('fil-PH'); - } else { - supportedLanguages.add('$lang-$normalizedCountry'); - } - } else { - final asrCode = languageManager.getAsrCodeByShortCode(lang); - if (asrCode != null) { - supportedLanguages.add(asrCode); - } else { - supportedLanguages.add('en-US'); - } - } - } - } - } - - if (supportedLanguages.isEmpty) { - supportedLanguages.add('en-US'); - } - } catch (e) { - Logger.error('获取APP语言配置失败,使用默认语言: $e'); - supportedLanguages = ['zh-CN', 'en-US']; - } - - final agentType = _storage.read('device_agent_type') ?? - AppConfig.env('AGENT_TYPE') ?? - 'stt_llm_tts'; - final agentMode = (agentType == 'yidong') ? 'sts' : 'stt_llm_tts'; - final agentId = (agentType == 'yidong') - ? (AppConfig.env('MOBILE_ELF_AGENT_ID') ?? 'mobile_elf') - : 'default_agent'; - final stsProductId = AppConfig.env('MOBILE_ELF_PRODUCT_ID') ?? ''; - final stsProductKey = AppConfig.env('MOBILE_ELF_PRODUCT_KEY') ?? ''; - final stsDeviceNo = _storage.read('device_sts_code') ?? ''; - final stsDeviceId = _storage.read('sts_device_id') ?? ''; - final stsDeviceSecret = _storage.read('sts_device_secret') ?? ''; - final mac = currentDeviceAddress?.toUpperCase() ?? ''; - String stsCmei = ''; - try { - final matchedDevice = userInfo.User.instance.devices.firstWhere( - (d) => d.devicemac.toUpperCase() == mac, - ); - stsCmei = matchedDevice.cmei; - } catch (_) {} - - final config = { - 'agentMode': agentMode, - 'agentId': agentId, - 'stsProductId': stsProductId, - 'stsProductKey': stsProductKey, - 'stsDeviceNo': stsDeviceNo, - 'stsDeviceId': stsDeviceId, - 'stsDeviceSecret': stsDeviceSecret, - 'stsCmei': stsCmei, - 'azureSpeechKey': azureSpeechKey, - 'azureSpeechRegion': azureSpeechRegion, - 'xunfeiAppId': xunfeiAppId, - 'xunfeiAccessKeyId': xunfeiAccessKeyId, - 'xunfeiAccessKeySecret': xunfeiAccessKeySecret, - 'openaiApiKey': openaiApiKey, - 'openaiBaseUrl': openaiBaseUrl, - 'openaiModel': openaiModel, - 'openaivisionModel': openaiModelMedia, - 'systemPrompt': systemPrompt, - 'mcpServer': jsonEncode(jsonData), - 'serverurl': serverurl, - 'usertoken': jsonString, - 'supportedLanguages': supportedLanguages, - 'musicServiceType': musicServiceType, - 'navigationMode': navigationMode, - 'isttsinterrupt': isTtsInterruptEnabled ? 'true' : 'false', - 'vipexptime': vipExpiryTime, - 'callingToolText': 'callingTool'.tr, - 'insufficientIntegralText': 'insufficientIntegral'.tr, - }; - - AgentService.nativeLogCallback ??= (level, message) { - switch (level) { - case 'ERROR': - Logger.e('NativeAgent', message); - break; - case 'WARNING': - Logger.w('NativeAgent', message); - break; - case 'DEBUG': - Logger.d('NativeAgent', message); - break; - default: - Logger.i('NativeAgent', message); - } - }; - - if (agentMode == 'sts') { - AgentService.events - .where((e) => e.type == AgentServiceEventType.stsCredentials) - .listen((e) { - final deviceId = e.data['deviceId']?.toString() ?? ''; - final deviceSecret = e.data['deviceSecret']?.toString() ?? ''; - final deviceNo = e.data['deviceNo']?.toString() ?? ''; - if (deviceId.isNotEmpty) _storage.write('sts_device_id', deviceId); - if (deviceSecret.isNotEmpty) { - _storage.write('sts_device_secret', deviceSecret); - } - if (deviceNo.isNotEmpty) _storage.write('sts_device_no', deviceNo); - }); - } - - final result = await AgentService.startAgentService(config); - Logger.i('BleManager', 'AgentService.startAgentService 结果: $result'); - return result; - } catch (e) { - Logger.error('启动 AgentService 失败: $e'); - return false; - } - } - - Future stopAgentService() async { - try { - final result = await AgentService.stopAgentService(); - if (result) { - Logger.info('AgentService 停止成功'); - clearDeviceInfo(); - } - return result; - } catch (e) { - Logger.error('停止 AgentService 失败: $e'); - return false; - } - } - - Future _loadTtsInterrupe() async { - try { - if (!_storage.hasData('agent_tts_interrupt')) { - String deviceName = getDeviceName(); - DBProduct? product = AppConfig.getproduct(deviceName); - if (deviceName != '' && product != null) { - return !product.iscontinuouschat!; - } - return false; - } - return _storage.read('agent_tts_interrupt') ?? false; - } catch (_) { - return false; - } - } - - static Future init() async { - if (!Get.isRegistered()) { - Get.put(BleManager(), permanent: true); - } - return Future.value(Get.find()); - } -} diff --git a/apps/client/lib/data/services/bluetooth_manager.dart b/apps/client/lib/data/services/bluetooth_manager.dart deleted file mode 100644 index c68e9897..00000000 --- a/apps/client/lib/data/services/bluetooth_manager.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'dart:async'; -import 'package:get/get.dart'; -import 'package:classic_bluetooth/classic_bluetooth.dart'; -import '../../core/utils/logger.dart'; - -// 重新导出蓝牙设备类型,让外部可以通过BluetoothManager访问 -export 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice; - -/// 蓝牙服务 - 管理经典蓝牙服务 -class BluetoothManager extends GetxService { - static BluetoothManager get to => Get.find(); - - // 蓝牙状态 - final _bluetoothState = 'unknown'.obs; - String get bluetoothState => _bluetoothState.value; - RxString get bluetoothStateRx => _bluetoothState; - - // 当前经典蓝牙设备 - final Rx currentDeviceRx = Rx(null); - BluetoothDevice? get currentDevice => currentDeviceRx.value; - // 订阅 - StreamSubscription? _bluetoothSubscription; - - /// 初始化 - @override - void onInit() { - super.onInit(); - Logger.info('蓝牙服务初始化'); - - // 设置蓝牙事件监听 - _setupBluetoothListener(); - - // 主动检查当前已连接的设备和蓝牙状态 - Future.delayed(Duration.zero, () async { - Logger.info('检查初始蓝牙连接状态'); - - // 先检查蓝牙是否开启 - await isBluetoothEnabled(); - - // 然后刷新设备连接 - await refreshDeviceConnection(); - }); - } - - /// 释放资源 - @override - void onClose() { - print("蓝牙服务关闭"); - _bluetoothSubscription?.cancel(); - super.onClose(); - } - - /// 设置经典蓝牙事件监听 - void _setupBluetoothListener() { - _bluetoothSubscription?.cancel(); - - try { - _bluetoothSubscription = ClassicBluetooth.events.listen( - (event) { - print("_setupBluetoothListener$event"); - if (event.type == 'stateChanged' && event.state != null) { - _bluetoothState.value = event.state!; - Logger.info('蓝牙状态变更: ${event.state}'); - - // 如果蓝牙关闭,清除设备 - if (event.state == 'off') { - currentDeviceRx.value = null; - } - } else if (event.type == 'deviceConnected' && event.device != null) { - // 直接从事件中获取设备信息 - final newDevice = event.device!; - - currentDeviceRx.value = newDevice; - - // 经典蓝牙连接事件:仅记录日志,不再跳转配对页(绑定流程已下线) - Logger.info('经典蓝牙设备已连接: ${newDevice.name} (${newDevice.address})'); - } else if (event.type == 'deviceDisconnected' && - event.device != null) { - // 直接从事件中获取设备信息 - final disconnectedDevice = event.device!; - - Logger.info( - '经典蓝牙设备已断开: ${disconnectedDevice.name} (${disconnectedDevice.address})'); - - // 如果断开的是当前设备,清除当前设备 - if (currentDeviceRx.value?.address == disconnectedDevice.address) { - currentDeviceRx.value = null; - Logger.info('已清除当前设备引用'); - } - } - }, - onError: (error) { - Logger.error('蓝牙事件流错误: ${error.toString()}'); - }, - ); - } catch (e) { - Logger.error('设置蓝牙事件监听器失败: ${e.toString()}'); - } - } - - /// 刷新设备连接状态 - Future refreshDeviceConnection() async { - try { - // 如果BLE未连接,则检查经典蓝牙连接 - final devices = await ClassicBluetooth.getConnectedDevices(); - - if (devices.isNotEmpty) { - for (var device in devices) { - if (device.isActive) { - // 如果有活动设备,直接使用它 - currentDeviceRx.value = device; - Logger.info('已连接经典蓝牙设备: ${device.name} (${device.address})'); - return; - } - } - final newDevice = devices.first; - currentDeviceRx.value = newDevice; - Logger.info('已连接经典蓝牙设备: ${newDevice.name} (${newDevice.address})'); - return; - } - - // 没有任何连接 - currentDeviceRx.value = null; - Logger.info('未找到已连接的蓝牙设备'); - } catch (e) { - Logger.error('刷新设备连接失败: ${e.toString()}'); - } - } - - /// 检查蓝牙是否已启用 - Future isBluetoothEnabled() async { - try { - final isEnabled = await ClassicBluetooth.isBluetoothEnabled(); - _bluetoothState.value = isEnabled ? 'on' : 'off'; - return isEnabled; - } catch (e) { - Logger.error('检查蓝牙状态失败: ${e.toString()}'); - return false; - } - } - -} diff --git a/apps/client/lib/data/services/face_to_face_ble_service.dart b/apps/client/lib/data/services/face_to_face_ble_service.dart deleted file mode 100644 index 69d621ce..00000000 --- a/apps/client/lib/data/services/face_to_face_ble_service.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:get/get.dart'; - -import '../../core/utils/logger.dart'; -import 'bes_bluetooth_service.dart'; - -/// 面对面翻译时,左右耳分别独立走 BLE 的连接管理。 -/// -/// 背景:普通模式下耳机是一条经典蓝牙 SPP([BesBluetoothService])统一收发; -/// 面对面翻译要让两只耳朵各自朝向一位说话人——左耳收/放一种语言, -/// 右耳收/放另一种语言——所以连接模式要整体切到 BLE, -/// **左耳一条 BLE、右耳一条 BLE,互相独立**。 -/// -/// 目前状态:**框架先搭好,具体连接与收发留空**。 -/// 固件那边的服务/特征 UUID、左右耳的广播名与配对流程都还没给, -/// 等命令确定后在 [_connectSide] / [_disconnectSide] / [sendToSide] 里补实现即可, -/// 上层(面对面翻译页与控制器)不需要再改。 -/// -/// 这样安排的原因:面对面翻译的开关时机、UI 状态、与 SPP 的互斥关系 -/// 是现在就能定下来的,先把这层接口固化,后面补底层不会牵动业务逻辑。 -enum EarSide { left, right } - -class FaceToFaceBleService extends GetxService { - static const String _tag = 'FaceToFaceBleService'; - static FaceToFaceBleService get to => Get.find(); - - /// 左右耳各自的连接状态:0=未连接 1=连接中 2=已连接 - final RxInt leftStatus = 0.obs; - final RxInt rightStatus = 0.obs; - - /// 当前是否处于面对面翻译的 BLE 模式 - final RxBool isBleMode = false.obs; - - bool get leftConnected => leftStatus.value == 2; - bool get rightConnected => rightStatus.value == 2; - - /// 进入面对面翻译:通知耳机切模式(`AA 66`),随后把左右耳各自拉起 BLE。 - /// - /// 返回是否成功进入模式。左右耳 BLE 尚未实现,当前只要 `AA 66` 发出去就算成功, - /// 不因为 BLE 没连上而阻断——否则功能在固件就绪前完全不可用。 - Future enterFaceToFaceMode() async { - if (isBleMode.value) return true; - try { - if (Get.isRegistered() && - BesBluetoothService.to.isConnected) { - await BesBluetoothService.to.startFaceToFace(); // AA 66 - Logger.i(_tag, '已下发 AA 66,耳机进入面对面翻译模式'); - } else { - Logger.w(_tag, '耳机未连接,跳过 AA 66'); - } - isBleMode.value = true; - // 左右耳分别建立独立 BLE 连接(待实现) - await _connectSide(EarSide.left); - await _connectSide(EarSide.right); - return true; - } catch (e) { - Logger.e(_tag, '进入面对面翻译模式失败: $e'); - return false; - } - } - - /// 退出面对面翻译:断开左右耳 BLE,并通知耳机回到常规模式(`AA 67`) - Future exitFaceToFaceMode() async { - if (!isBleMode.value) return; - try { - await _disconnectSide(EarSide.left); - await _disconnectSide(EarSide.right); - if (Get.isRegistered() && - BesBluetoothService.to.isConnected) { - await BesBluetoothService.to.stopFaceToFace(); // AA 67 - Logger.i(_tag, '已下发 AA 67,耳机退出面对面翻译模式'); - } - } catch (e) { - Logger.e(_tag, '退出面对面翻译模式失败: $e'); - } finally { - isBleMode.value = false; - leftStatus.value = 0; - rightStatus.value = 0; - } - } - - /// 建立某一侧耳机的独立 BLE 连接。 - /// - /// TODO(固件): 命令与 UUID 确定后在此实现—— - /// 扫描该侧广播 → 连接 → 发现服务 → 订阅通知 → 置 status=2。 - Future _connectSide(EarSide side) async { - final status = side == EarSide.left ? leftStatus : rightStatus; - status.value = 1; - Logger.i(_tag, '${_sideName(side)} BLE 连接:待固件命令确定后实现'); - // 尚未实现,保持「连接中」而不是假装已连接,避免上层误判为可用 - status.value = 0; - } - - /// 断开某一侧耳机的 BLE 连接。 - /// - /// TODO(固件): 与 [_connectSide] 配套实现。 - Future _disconnectSide(EarSide side) async { - final status = side == EarSide.left ? leftStatus : rightStatus; - if (status.value == 0) return; - Logger.i(_tag, '${_sideName(side)} BLE 断开:待实现'); - status.value = 0; - } - - /// 向指定一侧下发数据(译文音频 / 控制指令)。 - /// - /// TODO(固件): 左右耳各自的写特征确定后在此实现。 - /// 现在直接返回 false,调用方据此判断「该侧尚不可用」。 - Future sendToSide(EarSide side, List bytes) async { - Logger.d(_tag, '${_sideName(side)} 下发 ${bytes.length} 字节:待实现'); - return false; - } - - String _sideName(EarSide side) => side == EarSide.left ? '左耳' : '右耳'; -} diff --git a/apps/client/lib/data/services/holder_recording_service.dart b/apps/client/lib/data/services/holder_recording_service.dart deleted file mode 100644 index 6ae81925..00000000 --- a/apps/client/lib/data/services/holder_recording_service.dart +++ /dev/null @@ -1,309 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math' as math; -import 'dart:typed_data'; - -import 'package:echomeet_device_sdk/echomeet_device_sdk.dart'; -import 'package:get/get.dart'; -import 'package:intl/intl.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; - -import '../../core/utils/logger.dart'; -import 'holder_device_service.dart'; -import 'recording_archive.dart'; - -/// 蓝讯支架录音的全局采集与归档服务。 -/// -/// 之前这套逻辑挂在现场录音页的控制器上,页面一关监听就没了——用户在支架上 -/// 按录音键时 App 完全没反应。现在提到常驻服务里:不管当前在哪个页面、甚至 -/// 页面没打开,只要设备端起停录音,这里都会跟着采集/落盘/归档。 -/// -/// 职责: -/// 1. 订阅 [HolderDeviceService.recording],设备端开始/结束录音时自动起停 -/// 2. 录制期间把 SDK 推的 PCM 帧写成本地 WAV(16kHz/16bit/单声道) -/// 3. 结束后归档进「语音记录助手」,由 [MeetingUploadService] 按既有逻辑上云 -class HolderRecordingService extends GetxService { - static const String _tag = 'HolderRecordingService'; - static HolderRecordingService get to => Get.find(); - - /// 当前是否正在录(由设备端状态驱动) - final RxBool isRecording = false.obs; - - /// 已录毫秒数 - final RxInt elapsedMs = 0.obs; - - /// 当前电平 0~1,供波形使用 - final RxDouble level = 0.0.obs; - - /// 波形采样序列(固定 [sampleIntervalMs] 一格) - final RxList waveform = [].obs; - static const int sampleIntervalMs = 100; - static const int _maxSamples = 600; - - Worker? _stateWorker; - StreamSubscription? _audioSub; - RandomAccessFile? _raf; - int _dataBytes = 0; - String? _path; - Timer? _tick; - Timer? _sampleTimer; - final Stopwatch _stopwatch = Stopwatch(); - - @override - void onInit() { - super.onInit(); - _bind(); - } - - @override - void onClose() { - _stateWorker?.dispose(); - _audioSub?.cancel(); - _tick?.cancel(); - _sampleTimer?.cancel(); - super.onClose(); - } - - void _bind() { - if (!Get.isRegistered()) return; - final holder = Get.find(); - _stateWorker = ever(holder.recording, (st) async { - Logger.i(_tag, - '设备录音状态上报: isRecording=${st.isRecording} timerStatus=${st.timerStatus} ' - 'seconds=${st.seconds} 本地=${isRecording.value} 忙=$_busy'); - if (_busy) { - Logger.w(_tag, '本地正在起停中,忽略这次上报'); - return; - } - if (st.isRecording && !isRecording.value) { - Logger.i(_tag, '设备端开始录音,启动本地采集'); - await start(); - } else if (!st.isRecording && isRecording.value) { - Logger.i(_tag, '设备端结束录音,落盘并归档'); - await stop(); - } - }); - } - - /// 开始本地采集。[sendCommand] = true 时同时下发设备开始指令 - /// (App 主动发起录音的场景)。 - /// 起停过程中的互斥标志:设备上报和 UI 操作可能同时进来, - /// 不加锁会出现「刚建好文件又被另一路关掉」的竞态。 - bool _busy = false; - - Future start({bool sendCommand = false}) async { - if (isRecording.value || _busy) return isRecording.value; - _busy = true; - try { - return await _start(sendCommand); - } finally { - _busy = false; - } - } - - Future _start(bool sendCommand) async { - if (!Get.isRegistered()) return false; - final holder = Get.find(); - if (!holder.isConnected) return false; - - try { - final docs = await getApplicationDocumentsDirectory(); - final dir = Directory('${docs.path}/field_recordings'); - if (!await dir.exists()) await dir.create(recursive: true); - final ts = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); - _path = '${dir.path}/holder_$ts.wav'; - - _raf = await File(_path!).open(mode: FileMode.write); - await _raf!.writeFrom(_wavHeader(0)); - _dataBytes = 0; - - _audioSub?.cancel(); - _audioSub = holder.audioFrames.listen(_onFrame); - - // 顺序很关键:**先切录音状态,再开编码器**。 - // - // toggleRecording 是盲切:原生 BleRecord 若当前是 RUNNING,切过去会走 - // pauseRecord() → BleService.closeCodec(),把刚开的编码器关掉。实测就是 - // openEncoder 后 5ms 立刻跟了一条 0x05/0x00(CODEC_CONTROL_CLOSE)。 - // 把 openEncoder 放到最后,保证不论状态怎么切,最终编码器都是开着的。 - if (sendCommand && !holder.deviceIsRecording) { - final ok = await holder.startRecording(); - if (!ok) { - await _closeFile(save: false); - return false; - } - } else if (sendCommand) { - Logger.i(_tag, '设备已在录音中,跳过状态切换,仅接管本地采集'); - } - - // 【必须】打开设备编码器,否则设备不往手机推 Opus 流。 - // 协议帧 0xAA 0x05 0x02 0xB1 0x02 [CRC]。设备按键发起的录音只会让原生 - // 切本地录音态,不会自动开编码器,所以这里无条件补一条。 - final encoderOk = await holder.openEncoder(); - if (!encoderOk) { - Logger.w(_tag, 'openEncoder 返回 false,可能收不到音频流'); - } - - isRecording.value = true; - elapsedMs.value = 0; - waveform.clear(); - _stopwatch - ..reset() - ..start(); - _tick?.cancel(); - _tick = Timer.periodic(const Duration(milliseconds: 50), - (_) => elapsedMs.value = _stopwatch.elapsedMilliseconds); - _sampleTimer?.cancel(); - _sampleTimer = Timer.periodic( - const Duration(milliseconds: sampleIntervalMs), - (_) => _sample(), - ); - Logger.i(_tag, '开始录制: $_path'); - return true; - } catch (e) { - Logger.e(_tag, '启动本地采集失败: $e'); - await _closeFile(save: false); - return false; - } - } - - /// 停止。[sendCommand] = true 时同时下发设备停止指令。 - Future stop({bool sendCommand = false}) async { - if (!isRecording.value || _busy) return; - _busy = true; - try { - await _stop(sendCommand); - } finally { - _busy = false; - } - } - - Future _stop(bool sendCommand) async { - _tick?.cancel(); - _sampleTimer?.cancel(); - _stopwatch.stop(); - - final int seconds = elapsedMs.value ~/ 1000; - final String? path = _path; - - if (sendCommand && - Get.isRegistered() && - Get.find().deviceIsRecording) { - try { - await Get.find().stopRecording( - fileName: path == null ? null : p.basenameWithoutExtension(path)); - } catch (e) { - Logger.e(_tag, '下发停止指令失败: $e'); - } - } - - await _closeFile(save: true); - isRecording.value = false; - elapsedMs.value = 0; - level.value = 0; - waveform.clear(); - - if (path != null && _dataBytes > 0) { - // 归档会把文件从 field_recordings 挪到 LocalAudio, - // 所以现场录音页的列表里不会再留副本——录音成品统一由语音记录助手管理。 - await _archive(path, seconds); - } else { - // 没收到音频的空文件(只有 44 字节 WAV 头)直接删掉, - // 否则会在现场录音列表里堆一堆 00:00 的无效记录。 - Logger.w(_tag, '本次没有收到音频数据,删除空文件'); - if (path != null) { - try { - final f = File(path); - if (await f.exists()) await f.delete(); - } catch (e) { - Logger.e(_tag, '删除空录音文件失败: $e'); - } - } - } - _dataBytes = 0; - _path = null; - } - - void _onFrame(EchoMeetAudioFrame frame) async { - final raf = _raf; - if (raf == null || frame.pcm.isEmpty) return; - try { - await raf.writeFrom(frame.pcm); - _dataBytes += frame.pcm.length; - _updateLevel(frame.pcm); - } catch (e) { - Logger.e(_tag, '写入 PCM 失败: $e'); - } - } - - void _updateLevel(Uint8List pcm) { - if (pcm.lengthInBytes < 2) return; - final s = pcm.buffer.asInt16List(pcm.offsetInBytes, pcm.lengthInBytes ~/ 2); - var sum = 0.0; - for (final v in s) { - final n = v / 32768.0; - sum += n * n; - } - final rms = math.sqrt(sum / s.length); - level.value = math.sqrt(rms).clamp(0.0, 1.0); - } - - void _sample() { - final list = waveform.toList(); - list.add(level.value); - if (list.length > _maxSamples) { - list.removeRange(0, list.length - _maxSamples); - } - waveform.assignAll(list); - } - - Future _closeFile({required bool save}) async { - await _audioSub?.cancel(); - _audioSub = null; - final raf = _raf; - _raf = null; - if (raf == null) return; - try { - if (save) { - await raf.setPosition(0); - await raf.writeFrom(_wavHeader(_dataBytes)); - } - await raf.close(); - } catch (e) { - Logger.e(_tag, '关闭录音文件失败: $e'); - } - } - - /// 归档进语音记录助手。具体入库逻辑见 [RecordingArchive]—— - /// 现场录音 / 通话录音 / 翻译录音都走同一套,不再各抄一份。 - Future _archive(String path, int seconds) async { - await RecordingArchive.archive(path: path, seconds: seconds, tag: _tag); - } - - Uint8List _wavHeader(int dataLength) { - const sampleRate = 16000, channels = 1, bits = 16; - final byteRate = sampleRate * channels * (bits ~/ 8); - final h = ByteData(44); - void ascii(int off, String s) { - for (var i = 0; i < s.length; i++) { - h.setUint8(off + i, s.codeUnitAt(i)); - } - } - - ascii(0, 'RIFF'); - h.setUint32(4, 36 + dataLength, Endian.little); - ascii(8, 'WAVE'); - ascii(12, 'fmt '); - h.setUint32(16, 16, Endian.little); - h.setUint16(20, 1, Endian.little); - h.setUint16(22, channels, Endian.little); - h.setUint32(24, sampleRate, Endian.little); - h.setUint32(28, byteRate, Endian.little); - h.setUint16(32, channels * (bits ~/ 8), Endian.little); - h.setUint16(34, bits, Endian.little); - ascii(36, 'data'); - h.setUint32(40, dataLength, Endian.little); - return h.buffer.asUint8List(); - } -} diff --git a/apps/client/lib/data/services/kouzi_ai_service.dart b/apps/client/lib/data/services/kouzi_ai_service.dart deleted file mode 100644 index c19f23d1..00000000 --- a/apps/client/lib/data/services/kouzi_ai_service.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:convert'; - -import 'package:http/http.dart' as http; - -import '../../core/utils/logger.dart'; -import 'ai_service.dart'; - -class KouziAIService implements AiService { - final String apiKey = - 'pat_QGDh6UGm3PaeL9GRS95JxrJ9aoU92ehMLQQ2NCre1TeBC5YeTcjpDmlz3efsJ8lh'; - final String modelId = '7481292724306657316'; - - @override - Future sendMessage({ - required List> messages, - String? systemPrompt, - String? userProperties, - }) async { - return ''; - } - - @override - Stream sendMessageStream({ - required List> messages, - String? systemPrompt, - String? userProperties, - }) async* { - final client = http.Client(); - final url = Uri.parse('https://api.coze.cn/v3/chat'); - - final headers = { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $apiKey', - }; - - final requestBody = { - "bot_id": modelId, - "user_id": "3422", - "additional_messages": [ - { - "content": messages.last['content'], - "content_type": "text", - "role": "user", - "type": "question", - }, - ], - "shortcut_command": {"command_id": "0"}, - "stream": true, - }; - - try { - final request = http.Request('POST', url) - ..headers.addAll(headers) - ..body = jsonEncode(requestBody); - final response = await client.send(request); - final regex = RegExp(r'(?<=[。!?;:.:;?!\n])'); - - String allContent = ''; - //流式处理响应数据 - await for (final chunk in response.stream - .transform(utf8.decoder) - .transform(const LineSplitter())) { - if (chunk.isEmpty) continue; - - if (chunk.startsWith('data:')) { - final jsonStr = chunk.substring(5).trim(); - if (jsonStr == '[DONE]') { - break; - } - - try { - final data = jsonDecode(jsonStr); - if (data['type'] == 'answer') { - String content = data['content'] ?? ''; - if (content.isNotEmpty && allContent != content) { - allContent += content; - List splitText = - content.split(regex).where((s) => s.isNotEmpty).toList(); - for (var element in splitText) { - yield element; - } - } - } - } catch (e) { - Logger.error('JSON解析错误: ${e.toString()}'); - } - } - } - } catch (e) { - Logger.error('请求异常: ${e.toString()}'); - } finally { - client.close(); - } - } -} \ No newline at end of file diff --git a/apps/client/lib/data/services/open_ai_service_adapter.dart b/apps/client/lib/data/services/open_ai_service_adapter.dart deleted file mode 100644 index 54bb667a..00000000 --- a/apps/client/lib/data/services/open_ai_service_adapter.dart +++ /dev/null @@ -1,301 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:convert'; -import '../../data/models/appconfig.dart'; -import '../../data/models/appconfig_model.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:open_ai_service/open_ai_service.dart'; -import 'package:get/get.dart'; -import 'ai_service.dart'; -import 'package:flutter/services.dart'; -import 'package:path_provider/path_provider.dart'; - -/// 定义流事件类型,用于区分不同类型的事件 -enum StreamEventType { token, functionCall, complete, error } - -/// 流事件包装类 -class StreamEvent { - final StreamEventType type; - final String? content; - final Map? meta; - - StreamEvent(this.type, {this.content, this.meta}); -} - -/// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件 -class OpenAIServiceAdapter implements AiService { - final OpenAIService _openAIService = OpenAIService(); - StreamSubscription? _eventSubscription; - final StreamController _tokenStreamController = - StreamController.broadcast(); - final GetStorage _storage = GetStorage(); - - /// 构造函数 - OpenAIServiceAdapter() { - printInfo(info: '创建OpenAIServiceAdapter实例'); - _setupEventListener(); - } - - /// 设置事件监听器 - void _setupEventListener() { - try { - // 首先确保访问eventStream以初始化底层事件通道 - _openAIService.eventStream; - - // 设置事件处理 - _eventSubscription = _openAIService.eventStream.listen( - (event) { - try { - switch (event.type) { - case OpenAIEventType.token: - if (event.content is String) { - _tokenStreamController.add(StreamEvent(StreamEventType.token, - content: event.content as String)); - } else { - printInfo(info: '收到非字符串类型的token: ${event.content}'); - } - break; - case OpenAIEventType.complete: - _tokenStreamController - .add(StreamEvent(StreamEventType.complete)); - break; - case OpenAIEventType.error: - _tokenStreamController.add(StreamEvent(StreamEventType.error, - content: '未知错误: ${event.content}')); - break; - case OpenAIEventType.functionCall: - printInfo( - info: - '接收到 functionCall content: ${event.content} meta: ${event.meta}'); - _tokenStreamController.add(StreamEvent( - StreamEventType.functionCall, - meta: event.meta as Map?)); - // _tokenStreamController.add(StreamEvent( - // StreamEventType.error, - // content: '收到函数调用,该流仅支持文本响应' - // )); - break; - } - } catch (e) { - printError(info: '处理事件出错: $e'); - _tokenStreamController - .add(StreamEvent(StreamEventType.error, content: '处理事件失败: $e')); - } - }, - onError: (error) { - printError(info: '事件流错误: $error'); - _tokenStreamController.add( - StreamEvent(StreamEventType.error, content: '事件流错误: $error')); - }, - onDone: () { - printInfo(info: '事件流已关闭'); - }, - ); - } catch (e) { - printError(info: '设置事件监听器失败: $e'); - } - } - - /// 初始化OpenAI服务 - Future initialize() async { - try { - // 从.env文件中读取配置 - // final apiKey = dotenv.env['OPENAI_API_KEY'] ?? ''; - // final baseUrl = dotenv.env['OPENAI_BASE_URL'] ?? ''; - // final model = dotenv.env['OPENAI_MODEL'] ?? ''; - // final _env = _storage.read("ENV") as Map; - final apiKey = AppConfig.env('OPENAI_API_KEY') ?? ''; - final baseUrl = AppConfig.env('OPENAI_BASE_URL') ?? ''; - final model = AppConfig.env('OPENAI_MODEL') ?? ''; - - printInfo(info: '从.env读取OpenAI配置'); - printInfo(info: '基础URL: $baseUrl'); - printInfo(info: '模型名称: $model'); - - if (apiKey.isEmpty) { - printError(info: '错误: OpenAI API密钥未配置,请在.env文件中设置OPENAI_API_KEY'); - return false; - } - final _mcps = _storage.read("MCPS") as Map; - // 读取.mcp.json文件 - String mcpConfig = jsonEncode(_mcps); - // try { - // // 从Flutter资源包中加载.mcp.json - // mcpConfig = await rootBundle.loadString('.mcp.json'); - // printInfo(info: '成功从资源包加载.mcp.json配置文件'); - - // // 验证JSON格式 - // final jsonData = jsonDecode(mcpConfig); - // if (jsonData is Map && jsonData.containsKey('mcpServers')) { - // printInfo(info: '解析到有效的mcpServers配置'); - // } else { - // printInfo(info: '.mcp.json内容格式不正确,期望包含mcpServers字段'); - // } - // } catch (e) { - // printError(info: '加载.mcp.json资源文件时出错: $e'); - // } - - // 初始化OpenAI服务 - final result = await _openAIService.initialize( - apiKey: apiKey, - baseUrl: baseUrl, - model: model, - mcpServer: mcpConfig, - ); - - if (result) { - printInfo(info: 'OpenAI服务初始化成功'); - } else { - printError(info: 'OpenAI服务初始化失败'); - } - - return result; - } catch (e) { - printError(info: 'OpenAI服务初始化异常: $e'); - return false; - } - } - - /// 发送消息并获取回复 - @override - Future sendMessage({ - required List> messages, - String? systemPrompt, - String? userProperties, - }) async { - try { - // 在方法内直接转换 - final convertedMessages = - messages.map((m) => Map.from(m)).toList(); - - // 发送消息并获取回复 - final response = await _openAIService.sendMessage( - messages: convertedMessages, - systemPrompt: systemPrompt, - userProperties: userProperties, - ); - - return response; - } catch (e) { - printError(info: 'OpenAI发送消息失败: $e'); - throw '发送消息失败: $e'; - } - } - - /// 发送消息并获取流式回复 - @override - Stream sendMessageStream({ - required List> messages, - String? systemPrompt, //提示词 - String? userProperties, //用户属性 - }) async* { - try { - // 在方法内直接转换 - final convertedMessages = - messages.map((m) => Map.from(m)).toList(); - - // 创建用于接收token的控制器 - final localController = StreamController(); - - // 添加从广播流到本地流的订阅 - final subscription = _tokenStreamController.stream.listen((streamEvent) { - switch (streamEvent.type) { - case StreamEventType.token: - if (streamEvent.content != null && !localController.isClosed) { - localController.add(streamEvent.content!); - } - break; - case StreamEventType.functionCall: - if (streamEvent.meta != null && !localController.isClosed) { - localController.add(streamEvent.meta!); - } - break; - case StreamEventType.complete: - if (!localController.isClosed) { - localController.close(); - } - break; - case StreamEventType.error: - if (!localController.isClosed) { - printError(info: '令牌流错误: ${streamEvent.content}'); - localController.addError(streamEvent.content ?? '未知错误'); - localController.close(); - } - break; - } - }); - - // 当本地控制器关闭时,取消订阅 - localController.onCancel = () { - subscription.cancel(); - }; - - // 启动流式消息请求 - bool started = false; - try { - started = await _openAIService.sendMessageStream( - messages: convertedMessages, - systemPrompt: systemPrompt, - userProperties: userProperties, - ); - } catch (e) { - printError(info: '启动消息流失败: $e'); - if (!localController.isClosed) { - localController.addError('启动消息流失败: $e'); - localController.close(); - } - throw '启动消息流失败: $e'; - } - - if (!started) { - printError(info: '无法启动消息流'); - if (!localController.isClosed) { - localController.addError('无法启动消息流'); - localController.close(); - } - throw '无法启动消息流'; - } - - // 通过yield*将controller的流转发 - yield* localController.stream; - } catch (e) { - printError(info: 'OpenAI流式消息处理失败: $e'); - throw '流式消息处理失败: $e'; - } - } - - /// 注册函数 - Future registerFunction( - String name, String description, Map parameters) async { - try { - final result = await _openAIService.registerFunction( - name: name, - description: description, - parameters: parameters, - ); - - if (result) { - printInfo(info: '函数 "$name" 注册成功'); - } else { - printError(info: '函数 "$name" 注册失败'); - } - - return result; - } catch (e) { - printError(info: '注册函数失败: $e'); - return false; - } - } - - /// 释放资源 - void dispose() { - try { - _eventSubscription?.cancel(); - _tokenStreamController.close(); - printInfo(info: 'OpenAIServiceAdapter资源已释放'); - } catch (e) { - printError(info: '释放资源时出错: $e'); - } - } -} diff --git a/apps/client/lib/data/utils/jieli_adv_parser.dart b/apps/client/lib/data/utils/jieli_adv_parser.dart deleted file mode 100644 index 6dff2322..00000000 --- a/apps/client/lib/data/utils/jieli_adv_parser.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'dart:io'; - -import 'package:device_jieli/device_jieli.dart'; - -import '../../core/utils/logger.dart'; - -/// 杰理设备广播包解析结果:从扫描到的 [JieliDevice.rawAdv] 中提取出 -/// 服务端绑定接口需要的 PID / MAC / 设备授权码 (code)。 -class JieliBindInfo { - /// 产品 ID(小端字节解析) - final int pid; - - /// 设备真实 MAC(XOR 0x5A 解码后),格式 "AA:BB:CC:DD:EE:FF" - final String mac; - - /// 设备授权码 (license / SN),hex 大写 - final String code; - - /// 解析得到的 PID 是否与扫描结果上的 BLE/EDR 地址一致 - /// (仅用于日志,不强制要求一致) - final bool macMatchesAddress; - - const JieliBindInfo({ - required this.pid, - required this.mac, - required this.code, - required this.macMatchesAddress, - }); - - @override - String toString() => - 'JieliBindInfo(pid=0x${pid.toRadixString(16).toUpperCase()}, mac=$mac, code=$code, match=$macMatchesAddress)'; -} - -/// 杰理 BLE 广播解析。 -/// -/// 字节布局(与旧版一致,基于扫到的 raw scanRecord 字节): -/// Android: -/// - bytes[6..7] → PID -/// - bytes[9..14] → MAC(每字节 XOR 0x5A) -/// - bytes[15..24] → 授权码 code -/// iOS: -/// - bytes[4..5] → PID -/// - bytes[7..12] → MAC(每字节 XOR 0x5A) -/// - bytes[13..22] → 授权码 code -class JieliAdvParser { - static const String _tag = 'JieliAdvParser'; - - /// 用 [device.rawAdv] 直接解析。若 raw 数据不完整或 device 未携带广播包,返回 null。 - static JieliBindInfo? parse(JieliDevice device) { - final raw = device.rawAdv; - if (raw == null || raw.isEmpty) { - Logger.w(_tag, '设备 ${device.address} 没有 rawAdv,无法解析绑定信息'); - return null; - } - return parseHex(raw, deviceAddress: device.address); - } - - /// 直接对 hex 字符串解析,便于测试。 - static JieliBindInfo? parseHex(String hex, {String? deviceAddress}) { - final bytes = _hexToBytes(hex); - if (bytes == null) { - Logger.w(_tag, 'rawAdv 不是合法 hex: $hex'); - return null; - } - - final int pidStart; - final int pidEnd; - final int macStart; - final int macEnd; - final int codeStart; - final int codeEnd; - - if (Platform.isIOS) { - pidStart = 4; - pidEnd = 5; - macStart = 7; - macEnd = 12; - codeStart = 13; - codeEnd = 22; - } else { - // Android 默认;其他平台沿用 Android 偏移。 - pidStart = 6; - pidEnd = 7; - macStart = 9; - macEnd = 14; - codeStart = 15; - codeEnd = 24; - } - - if (bytes.length <= codeEnd) { - Logger.w(_tag, - 'rawAdv 长度不足 ${codeEnd + 1} 字节(实际 ${bytes.length}),跳过解析'); - return null; - } - - final pid = _bytesToIntBigEndian(bytes, pidStart, pidEnd); - final mac = _decodeMac(bytes, macStart, macEnd); - final code = _bytesToHexReversed(bytes, codeStart, codeEnd); - - bool match = false; - if (deviceAddress != null && deviceAddress.isNotEmpty) { - final a = deviceAddress.replaceAll(':', '').toUpperCase(); - final b = mac.replaceAll(':', '').toUpperCase(); - match = a == b; - } - - final info = JieliBindInfo( - pid: pid, - mac: mac, - code: code, - macMatchesAddress: match, - ); - Logger.i(_tag, '解析广播: $info (deviceAddress=$deviceAddress)'); - return info; - } - - static List? _hexToBytes(String hex) { - final s = hex.startsWith('0x') || hex.startsWith('0X') ? hex.substring(2) : hex; - if (s.length.isOdd) return null; - final out = []; - for (int i = 0; i < s.length; i += 2) { - final v = int.tryParse(s.substring(i, i + 2), radix: 16); - if (v == null) return null; - out.add(v); - } - return out; - } - - static int _bytesToIntBigEndian(List bytes, int start, int end) { - int result = 0; - for (int i = start; i <= end; i++) { - result = (result << 8) | (bytes[i] & 0xFF); - } - return result; - } - - static String _bytesToHexReversed(List bytes, int start, int end) { - final slice = bytes.sublist(start, end + 1); - return slice - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join() - .toUpperCase(); - } - - static String _decodeMac(List bytes, int start, int end) { - final out = []; - for (int i = start; i <= end; i++) { - final decoded = bytes[i] ^ 0x5A; - out.add(decoded.toRadixString(16).padLeft(2, '0').toUpperCase()); - } - return out.join(':'); - } -} diff --git a/apps/client/lib/data/services/bes_bluetooth_service.dart b/apps/client/lib/devices/bes/bes_bluetooth_service.dart similarity index 100% rename from apps/client/lib/data/services/bes_bluetooth_service.dart rename to apps/client/lib/devices/bes/bes_bluetooth_service.dart diff --git a/apps/client/lib/data/services/bes_device_auth.dart b/apps/client/lib/devices/bes/bes_device_auth.dart similarity index 100% rename from apps/client/lib/data/services/bes_device_auth.dart rename to apps/client/lib/devices/bes/bes_device_auth.dart diff --git a/apps/client/lib/data/services/holder_device_service.dart b/apps/client/lib/devices/holder/holder_device_service.dart similarity index 100% rename from apps/client/lib/data/services/holder_device_service.dart rename to apps/client/lib/devices/holder/holder_device_service.dart diff --git a/apps/client/lib/modules/agent/bindings/agent_binding.dart b/apps/client/lib/modules/agent/bindings/agent_binding.dart deleted file mode 100644 index 4ae58711..00000000 --- a/apps/client/lib/modules/agent/bindings/agent_binding.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/agent_controller.dart'; -import '../controllers/message_action_controller.dart'; - -class AgentBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => AgentController()); - Get.lazyPut(() => MessageActionController()); - } -} diff --git a/apps/client/lib/modules/agent/controllers/agent_controller.dart b/apps/client/lib/modules/agent/controllers/agent_controller.dart deleted file mode 100644 index 9fbb2410..00000000 --- a/apps/client/lib/modules/agent/controllers/agent_controller.dart +++ /dev/null @@ -1,3014 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:amap_service/amap_service.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:uuid/uuid.dart'; -import '/data/models/appconfig.dart'; -import '/data/models/appconfig_model.dart'; -import '/data/models/song_model.dart'; -import '/data/services/ble_manager.dart'; -import '/data/services/bluetooth_manager.dart'; -import '/data/services/qq_music.dart'; -import 'package:flutter_intro/flutter_intro.dart'; -import '../../../data/services/novice_guide_service.dart'; -import '/core/utils/upload_oss.dart'; -import '/data/models/user_Info.dart'; -import '/data/services/music_manager.dart'; -import '/data/services/navigation_manager.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:agent_service/agent_service.dart'; -import 'package:chat_storage/chat_storage.dart'; -import 'package:device_jieli/device_jieli.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:permission_handler/permission_handler.dart'; -import '../../../core/utils/logger.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:photo_view/photo_view.dart'; -import '../../../core/utils/permission_util.dart'; -import '../../../data/models/message_model.dart'; -import '../../common/widgets/recharge_dialog.dart'; - -// 语音输入模式枚举 -enum VoiceInputMode { - text, // 文字输入模式 - pushToTalk, // 按住说话模式 - phoneCall, // 打电话模式 -} - -class AgentController extends GetxController with WidgetsBindingObserver { - static AgentController get to => Get.find(); //单例访问模式 - - final GetStorage _storage = GetStorage(); - // 聊天消息列表 - final messages = [].obs; - final currsessionid = ''.obs; - final vipExpiryTime = 0.obs; - // UI控制器 - final TextEditingController textController = TextEditingController(); - - /// 输入框内是否有内容(驱动底部输入栏发送按钮的显示/隐藏) - final hasInputText = false.obs; - final ScrollController scrollController = ScrollController(); - - final noviceGuideService = Get.find(); - // 确保服务已初始化 - // ========== 依赖服务 ========== - final BluetoothManager _bluetoothManager = Get.find(); - final BleManager _bleManager = Get.find(); - //音乐播放管理 - final _musiceManager = Get.find(); - final _qqmusicManager = Get.find(); - // 导航管理 - final _navigationManager = Get.find(); - RxBool isPlaying = false.obs; - RxInt currentId = 0.obs; - - // 播放进度相关状态 - RxInt currentPosition = 0.obs; // 当前播放位置(毫秒) - RxInt duration = 0.obs; // 总时长(毫秒) - RxDouble progress = 0.0.obs; // 播放进度(0.0-1.0) - - // 当前播放列表管理 - final currentPlaylist = >[].obs; // 当前播放列表 - final currentPlaylistIndex = 0.obs; // 当前播放歌曲在列表中的索引 - - // 各种状态标志 - final isInitialized = false.obs; - final isListening = false.obs; //是否正在语音识别 - final isSpeaking = false.obs; //是否正在播放tts - final isProcessing = false.obs; - // TTS启用状态 - final isTtsEnabled = true.obs; - final ttsRatePercent = 25.obs; - // TTS播报打断启用状态 - final isTtsInterruptEnabled = false.obs; - // 是否显示工具调用 - final isShowToolInterruptEnabled = false.obs; - // 图片处理状态 - final isImageProcessing = false.obs; - - // 输入模式控制 - final isTextInputMode = true.obs; //文本输入模式vs语音输入模式 - - // 当前输入的文本内容 - final currentText = ''.obs; - - // 图片输入相关 - final isImageInputActive = false.obs; // 是否处于图片输入模式 - final selectedImagePath = Rx(null); // 当前选择的图片路径 - - // 输入状态控制 - 用于动态显示发送按钮 - final hasTextInput = false.obs; // 是否有文本输入 - - // 全屏编辑相关状态 - final isFullScreenEditMode = false.obs; // 是否处于全屏编辑模式 - final shouldShowExpandButton = false.obs; // 是否显示展开按钮 - - // "+"号选项展开状态 - final isPlusOptionsExpanded = false.obs; // 是否展开"+"号选项 - - // 打电话模式状态 - final isPhoneCallMode = false.obs; // 是否处于打电话模式 - - // ─── 设备通话模式(device_jieli 耳机录音 → 外部注入 ASR)───── - // 独立于普通打电话模式,不复用 isPhoneCallMode - final isDeviceCallMode = false.obs; // 是否处于设备通话模式 - final isDeviceCallMicMuted = false.obs; // 设备通话模式下是否静音(丢帧) - StreamSubscription? _deviceCallJieliSub; - bool _deviceCallPushing = false; // 是否当前允许把设备 PCM 推给 ASR - // 调试:每秒打印一次 PCM 推送统计,确认音频是否真正流到 ASR - int _deviceCallPushedFrames = 0; - int _deviceCallPushedBytes = 0; - int _deviceCallLastReportMs = 0; - - // 按住说话状态 - final isPushToTalkActive = false.obs; // 是否正在按住说话 - - // 按住说话模式状态 - 用于区分是否处于按住说话模式(即使当前没有按住) - //final isPushToTalkMode = false.obs; // 是否处于按住说话模式 - - // 键盘状态 - final isKeyboardVisible = false.obs; // 键盘是否可见 - - // 流事件订阅 - StreamSubscription? _eventSubscription; - - // 导航流事件订阅 - StreamSubscription? _amapeventSubscription; - - // 进度更新定时器 - Timer? _progressTimer; - // 用于跟踪AI回复状态 - bool _isNewAssistantResponse = true; - RxBool newAIReplyCompleted = true.obs; - String _currentResponseId = ''; - - final TAG = 'AgentController'; - var AppDir = "".obs; - // 会话ID - final String agentId = 'default_agent'; - - var musicServiceType = 'WY'; - - // 分页加载相关状态 - final isLoadingMore = false.obs; // 是否正在加载更多消息 - final hasMoreMessages = true.obs; // 是否还有更多消息 - final currentPage = 1.obs; // 当前页码 - final pageSize = 20; // 每页消息数量 - final isLoadingMoreError = false.obs; // 加载更多时是否出错 - - // 是否已加载聊天记录 - final isHistoryLoaded = false.obs; - - // 添加加载状态跟踪 - final RxBool isLoadingMusic = false.obs; - final RxInt loadingMusicId = 0.obs; - - // 设置加载状态的方法 - void setLoadingMusic(int id, bool loading) { - loadingMusicId.value = loading ? id : 0; - isLoadingMusic.value = loading; - } - - @override - void onInit() { - super.onInit(); - // 输入框内容变化 → 驱动底部输入栏发送按钮显示/隐藏 - textController.addListener(_onInputTextChanged); - musicServiceType = AppConfig.env('APP_ANDROID_MUSICSERVICE_TYPE') ?? 'AUTO'; - if (Platform.isIOS) { - musicServiceType = AppConfig.env('APP_IOS_MUSICSERVICE_TYPE') ?? 'AUTO'; - } - vipExpiryTime.value = User.isLoggedIn() ? User.instance.vipexptime : 0; - ever(User.refreshTick, (_) { - vipExpiryTime.value = User.instance.vipexptime; - }); - _initAppDir(); - _loadTtsPreference(); //加载TTS偏好设置 - _loadTtsRatePreference(); - _loadTtsInterrupe(); //加载TTS播报打断偏好设置 - // 监听存储中的“agent_tts_interrupe”键变化,实现跨页面状态同步 - // 中文注释:当 Home 页或 Agent 页任意一处切换时,另一处可以通过监听存储自动刷新 UI - _storage.listenKey('agent_tts_interrupt', (value) { - try { - final bool newValue = value is bool ? value : (value == 'true'); - if (isTtsInterruptEnabled.value != newValue) { - isTtsInterruptEnabled.value = newValue; - } - } catch (e) { - // 中文注释:忽略异常,避免因类型不一致导致崩溃 - } - }); - _setupScrollListener(); //设置滚动监听器 - _loadChatHistory(); //加载聊天历史记录 - _subscribeToEvents(); //订阅代理服务事件 - _initMusicStatus(); //初始化音乐状态 - _setupMediaSessionCallbacks(); //设置媒体会话回调 - _setupTextInputListener(); //设置文本输入监听器 - - startVoiceInput(); //开启语音输入 - // 监听应用生命周期:当应用切到后台/失焦时兜底结束按住说话,防止 onPointerUp 未触发 - WidgetsBinding.instance.addObserver(this); - - // _qqmusicManager.syncCurrentPlayInfo(); - } - - Future _initAppDir() async { - AppDir.value = (await getApplicationDocumentsDirectory()).path; - } - - // 初始化音乐播放状态 - Future _initMusicStatus() async { - try { - await musicStatus(); - Logger.d('AgentController', '音乐状态初始化完成'); - } catch (e) { - Logger.e('AgentController', '音乐状态初始化失败: $e'); - } - } - - // 设置媒体会话回调 - void _setupMediaSessionCallbacks() async { - try { - await _musiceManager.setMediaSessionCallbacks( - onPlay: onMediaSessionPlay, - onPause: onMediaSessionPause, - onNext: onMediaSessionNext, - onPrevious: onMediaSessionPrevious, - onSeekTo: onMediaSessionSeekTo, - onStop: onMediaSessionStop, - onProgress: onMediaSessionProgress, - ); - Logger.d('AgentController', '媒体会话回调设置完成'); - } catch (e) { - Logger.e('AgentController', '媒体会话回调设置失败: $e'); - } - } - - @override - void onClose() { - // 若处于设备通话模式,fire-and-forget 结束 - if (isDeviceCallMode.value) { - unawaited(_stopDeviceCallInternal()); - } - AgentService.stopAiSteam(); //停止AI流 - AgentService.stopConversation(); // 停止当前会话 - AgentService.stopTts(); // 停止当前语音 - textController.removeListener(_onInputTextChanged); - textController.dispose(); - scrollController.dispose(); - _eventSubscription?.cancel(); - _amapeventSubscription?.cancel(); - _deviceCallJieliSub?.cancel(); - _deviceCallJieliSub = null; - Intro.of(Get.context!).dispose(); - - // 取消应用生命周期监听 - WidgetsBinding.instance.removeObserver(this); - - super.onClose(); - } - - /// 输入框内容变化回调:只在「空 ↔ 非空」翻转时写 Rx,避免每敲一个字都触发重建 - void _onInputTextChanged() { - final has = textController.text.trim().isNotEmpty; - if (hasInputText.value != has) { - hasInputText.value = has; - } - } - - // 加载TTS偏好设置 - Future _loadTtsPreference() async { - try { - final storage = GetStorage(); - isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true; - } catch (e) { - // 如果读取失败,使用默认值 - Logger.e(TAG, '加载TTS偏好设置失败: $e'); - isTtsEnabled.value = true; - } - } - - Future _loadTtsRatePreference() async { - try { - final value = _storage.read('agent_tts_rate_percent'); - if (value is int) { - ttsRatePercent.value = value; - } else if (value is String) { - final parsed = int.tryParse(value); - if (parsed != null) { - ttsRatePercent.value = parsed; - } - } else { - ttsRatePercent.value = 25; - } - } catch (e) { - ttsRatePercent.value = 25; - } - - try { - await AgentService.setTtsRate(ttsRatePercent.value); - } catch (e) { - Logger.e(TAG, '同步TTS语速失败: $e'); - } - } - - // 切换TTS状态 - Future toggleTts() async { - isTtsEnabled.toggle(); - - // 如果关闭TTS,停止当前语音 - if (!isTtsEnabled.value && isSpeaking.value) { - await AgentService.stopTts(); - } - - // 保存偏好设置 - try { - final storage = GetStorage(); - await storage.write('agent_tts_enabled', isTtsEnabled.value); - } catch (e) { - Logger.e(TAG, '保存TTS偏好设置失败: $e'); - } - } - - Future updateTtsRate(int rate) async { - final newRate = rate.clamp(0, 100); - ttsRatePercent.value = newRate; - - if (isSpeaking.value) { - try { - await AgentService.stopTts(); - } catch (_) {} - } - - try { - await AgentService.setTtsRate(newRate); - } catch (e) { - Logger.e(TAG, '设置TTS语速失败: $e'); - } - - try { - await _storage.write('agent_tts_rate_percent', newRate); - } catch (e) { - Logger.e(TAG, '保存TTS语速偏好设置失败: $e'); - } - } - - // 加载TTS偏好设置 - Future _loadTtsInterrupe() async { - try { - if (!_storage.hasData("agent_tts_interrupt")) { - // 获取设备类型以确定检查策略 - String deviceName = getDeviceName(); - DBProduct? product = AppConfig.getproduct(deviceName); - if (deviceName != "" && product != null) { - // Logger.d('AgentController', - // 'liwe----------product $deviceName ${product.toJson()}'); - isTtsInterruptEnabled.value = !product.iscontinuouschat!; - } else { - isTtsInterruptEnabled.value = false; - } - } else { - isTtsInterruptEnabled.value = - _storage.read('agent_tts_interrupt') ?? false; - } - await AgentService.isInterrupt(isTtsInterruptEnabled.value); - } catch (e) { - // 如果读取失败,使用默认值 - Logger.e(TAG, '加载TTS偏好设置失败: $e'); - isTtsInterruptEnabled.value = false; - } - } - - /// 切换TTS播报打断状态 - /// 当启用播报打断时,用户说话会自动停止当前的TTS播放 - Future toggleTtsInterrupt() async { - isTtsInterruptEnabled.toggle(); - await _storage.write('agent_tts_interrupt', isTtsInterruptEnabled.value); - // 保存偏好设置 - try { - await AgentService.isInterrupt(isTtsInterruptEnabled.value); - } catch (e) { - Logger.e(TAG, '保存TTS播报打断偏好设置失败: $e'); - } - } - - /// 当启用显示工具调用效果 - Future toggleShowAIToolInterrupt() async { - isShowToolInterruptEnabled.toggle(); - } - - // 设置文本输入监听器 - void _setupTextInputListener() { - textController.addListener(() { - final text = textController.text.trim(); - hasTextInput.value = text.isNotEmpty; - - // 检查是否需要显示展开按钮 - _checkShouldShowExpandButton(); - }); - } - - // 检查是否需要显示展开按钮 - void _checkShouldShowExpandButton() { - final text = textController.text; - if (text.isEmpty) { - shouldShowExpandButton.value = false; - return; - } - - // 计算文本行数 - final lines = text.split('\n'); - final maxLines = isImageInputActive.value ? 3 : 5; - - // 简单估算:如果换行符数量超过maxLines-1,或者单行文本过长,显示展开按钮 - final hasMultipleLines = lines.length > maxLines; - final hasLongLine = lines.any((line) => line.length > 50); // 估算单行字符数 - - shouldShowExpandButton.value = hasMultipleLines || hasLongLine; - } - - // 打开全屏编辑模式 - void openFullScreenEdit() { - isFullScreenEditMode.value = true; - } - - // 关闭全屏编辑模式 - void closeFullScreenEdit() { - isFullScreenEditMode.value = false; - } - - // 从全屏编辑模式发送消息 - Future sendMessageFromFullScreen() async { - await sendMessage(); - closeFullScreenEdit(); - } - - // 加载聊天历史记录 - Future _loadChatHistory() async { - try { - Logger.i(TAG, '正在加载聊天历史...'); - //通过本地插件ChatStorage获取聊天记录 - final result = await ChatStorage.getMessages( - agentId: agentId, //会话ID - page: 1, //从第一页开始 - pageSize: pageSize, //每页20条记录 - ); - Logger.i(TAG, '加载聊天历史记录: $result'); - if (result.messages.isNotEmpty) { - // 转换并添加消息到列表 - final historyMessages = _convertChatMessagesToMessages(result.messages); - - // 消息按时间排序,最新的消息在底部 - historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - messages.assignAll(historyMessages); - - // 更新分页状态 - currentPage.value = 1; - hasMoreMessages.value = result.totalPages > 1; - // print( - // 'liwei-------------读取历史记录 ${AppDir.value}${historyMessages[0].imagePath}'); - Logger.i(TAG, - '成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}'); - _logMessageDetails(historyMessages); - } else { - // 没有历史记录时添加欢迎消息 - _addWelcomeMessage(); - hasMoreMessages.value = false; - Logger.i(TAG, '没有历史记录,添加欢迎消息'); - } - } catch (e) { - Logger.e(TAG, '加载聊天历史失败: $e'); - _addWelcomeMessage(); - hasMoreMessages.value = false; - } finally { - isHistoryLoaded.value = true; - messages.refresh(); - // 使用reverse:true时不需要手动滚动到底部 - } - } - - // 加载更多历史消息(分页加载) - Future loadMoreMessages() async { - // 防止重复加载 - if (isLoadingMore.value || !hasMoreMessages.value) { - Logger.d(TAG, - '跳过加载更多:isLoadingMore=${isLoadingMore.value}, hasMoreMessages=${hasMoreMessages.value}'); - return; - } - - try { - isLoadingMore.value = true; - isLoadingMoreError.value = false; - - final nextPage = currentPage.value + 1; - Logger.i(TAG, '正在加载第 $nextPage 页历史消息...'); - - final result = await ChatStorage.getMessages( - agentId: agentId, - page: nextPage, - pageSize: pageSize, - ); - - if (result.messages.isNotEmpty) { - // 转换新消息 - final newMessages = _convertChatMessagesToMessages(result.messages); - - // 按时间排序新消息 - newMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - - // 添加短暂延迟,让用户感知到加载过程 - await Future.delayed(const Duration(milliseconds: 300)); - - // 将新消息插入到现有消息列表的开头(因为是历史消息) - final updatedMessages = [...newMessages, ...messages]; - messages.assignAll(updatedMessages); - - // 更新分页状态 - currentPage.value = nextPage; - hasMoreMessages.value = nextPage < result.totalPages; - - Logger.i(TAG, - '成功加载 ${newMessages.length} 条历史消息,当前页: $nextPage/${result.totalPages}'); - } else { - // 没有更多消息 - hasMoreMessages.value = false; - Logger.i(TAG, '没有更多历史消息'); - } - } catch (e) { - Logger.e(TAG, '加载更多消息失败: $e'); - isLoadingMoreError.value = true; - } finally { - isLoadingMore.value = false; - } - } - - // 转换ChatMessage到Message的通用方法 - List _convertChatMessagesToMessages(List chatMessages) { - return chatMessages.map((msg) { - final meta = msg.metadata != null && msg.metadata != "" - ? jsonDecode(msg.metadata!) - : null; // 如果为空,使用空对象 - - // 从元数据中提取图片信息 - final hasImage = meta != null && meta.containsKey('hasImage') - ? meta['hasImage'] as bool - : false; - final imagePath = meta != null && meta.containsKey('imagePath') - ? meta['imagePath'] as String? - : null; - - return Message( - sessionid: msg.sessionId, // 会话ID - isUser: msg.sender == 'user', - text: msg.message, - meta: meta, - timestamp: DateTime.parse(msg.timestamp).toLocal(), - hasImage: hasImage, - imagePath: imagePath, - ); - }).toList(); - } - - // 打印消息详情的通用方法 - void _logMessageDetails(List messageList) { - Logger.d(TAG, '===== 历史消息详情开始 ====='); - for (int i = 0; i < messageList.length; i++) { - final msg = messageList[i]; - Logger.d( - TAG, - '消息[$i]: {' - 'isUser: ${msg.isUser}, ' - 'text: "${msg.text.length > 50 ? msg.text.substring(0, 50) + "..." : msg.text}", ' - 'time: ${msg.timestamp}, ' - 'hasImage: ${msg.hasImage}, ' - 'imagePath: ${msg.imagePath ?? "无"}, ' - 'meta: ${msg.meta != null ? "有" : "无"}' - '}'); - } - Logger.d(TAG, '===== 历史消息详情结束 ==lishi==='); - } - - // 设置滚动监听器 - void _setupScrollListener() { - scrollController.addListener(() { - // 检查是否滚动到顶部(因为使用了reverse: true,顶部实际是最大滚动位置) - if (scrollController.position.pixels >= - scrollController.position.maxScrollExtent - 100) { - // 当距离顶部100像素时触发加载更多 - if (hasMoreMessages.value && !isLoadingMore.value) { - Logger.d(TAG, '触发加载更多历史消息'); - loadMoreMessages(); - } - } - }); - } - - // 手动触发加载更多(用于测试或手动调用) - void triggerLoadMore() { - if (hasMoreMessages.value && !isLoadingMore.value) { - Logger.i(TAG, '手动触发加载更多历史消息'); - loadMoreMessages(); - } else { - Logger.w(TAG, - '无法加载更多:hasMoreMessages=${hasMoreMessages.value}, isLoadingMore=${isLoadingMore.value}'); - } - } - - // 重置分页状态(用于刷新聊天记录) - void resetPagination() { - currentPage.value = 1; - hasMoreMessages.value = true; - isLoadingMore.value = false; - isLoadingMoreError.value = false; - Logger.i(TAG, '分页状态已重置'); - } - - // 添加初始欢迎消息 - void _addWelcomeMessage() { - messages.add(Message( - sessionid: "", // 会话ID - isUser: false, - text: 'welcomeMessage'.tr, // 你好!我是**Lily**,我能帮你: - )); - } - - // 订阅代理服务事件 - void _subscribeToEvents() { - _eventSubscription = AgentService.events.listen((event) { - final String asrProvider = (event.data['provider'] ?? '').toString(); - if (asrProvider.isNotEmpty) Logger.i(TAG, 'ASR provider: $asrProvider'); - switch (event.type) { - case AgentServiceEventType.recognitionStarted: - isListening.value = true; - //isTextInputMode.value = false; - Logger.i(TAG, '订阅代理服务事件识别开始'); - // isTextInputMode.refresh(); - // 只标记识别开始,不添加消息 - Logger.i(TAG, '识别开始'); - - break; - case AgentServiceEventType.recognitionStopped: - isListening.value = false; - - Logger.i(TAG, '识别停止'); - break; - - case AgentServiceEventType.recognizing: - final sessionid = event.data['sessionid'] ?? ''; - final text = event.data['text'] ?? ''; - currentText.value = text; // 保留当前文本,以便其他地方使用 - Logger.i(TAG, '[$sessionid]识别中间结果: $text'); - if (text.isNotEmpty) { - // 查找是否有正在识别中的消息 - final index = messages.lastIndexWhere( - (msg) => msg.sessionid == sessionid && msg.isUser); - - if (index >= 0) { - // 更新已有的识别中消息 - messages[index] = Message( - sessionid: sessionid, // 会话ID - isUser: true, - text: text, - isRecognizing: true, - timestamp: messages[index].timestamp, - ); - } else { - currsessionid.value = event.data['sessionid'] ?? ''; - // 没有识别中的消息,创建新消息 - messages.add(Message( - sessionid: sessionid, // 会话ID - isUser: true, - text: text, - isRecognizing: true, - )); - } - // messages.refresh(); - } - break; - - case AgentServiceEventType.recognitionResult: - final sessionid = event.data['sessionid'] ?? ''; - final text = event.data['text'] ?? ''; - Logger.i(TAG, '[$sessionid]识别最终结果: $text'); - - // if (currsessionid.value != "" && currsessionid.value != sessionid) { - // final index = messages.lastIndexWhere( - // (msg) => msg.sessionid == currsessionid.value && msg.isUser); - // //出现了为识别结束的消息 - // if (index >= 0) { - // //异除去当前会话的识别中消息 - // messages.removeAt(index); - // } - // } - currsessionid.value = sessionid; - // 查找是否有正在识别中的消息 - final index = messages.lastIndexWhere( - (msg) => msg.sessionid == sessionid && msg.isUser); - if (text.isNotEmpty) { - if (index >= 0) { - // 将识别中的消息转换为最终消息 - messages[index] = Message( - sessionid: sessionid, - isUser: true, - text: text, - isRecognizing: false, - timestamp: messages[index].timestamp, - functionCalls: messages[index].functionCalls, - isToolCallsExpanded: messages[index].isToolCallsExpanded, - ); - // messages.refresh(); - } else { - // 如果没有找到识别中的消息,添加新消息 - messages.add(Message( - sessionid: sessionid, - isUser: true, - text: text, - isRecognizing: false)); - } - - isProcessing.value = true; - // 标记为新的AI回复 - _isNewAssistantResponse = true; - // 使用reverse:true时通常不需要手动滚动 - } else if (index >= 0) { - // 如果识别结果为空,移除临时的识别消息 - messages.removeAt(index); - // messages.refresh(); - } - break; - //识别取消(网络波动会触发) - case AgentServiceEventType.recognitionCanceled: - final sessionid = event.data['sessionid'] ?? ''; - // 查找是否有正在识别中的消息 - final index = messages.lastIndexWhere( - (msg) => msg.sessionid == sessionid && msg.isUser); - currsessionid.value = ''; - if (currentText.value.isNotEmpty) { - if (index >= 0) { - // 将识别中的消息转换为最终消息 - messages[index] = Message( - sessionid: sessionid, // 会话ID - isUser: true, - text: currentText.value, - isRecognizing: false, - timestamp: messages[index].timestamp, - functionCalls: messages[index].functionCalls, - isToolCallsExpanded: messages[index].isToolCallsExpanded, - ); - } else { - // 如果没有找到识别中的消息,添加新消息 - messages.add(Message( - sessionid: sessionid, - isUser: true, - text: currentText.value, - isRecognizing: false, - )); - } - - isProcessing.value = true; - // 标记为新的AI回复 - _isNewAssistantResponse = true; - // 使用reverse:true时通常不需要手动滚动 - } else if (index >= 0) { - // 如果识别结果为空,移除临时的识别消息 - messages.removeAt(index); - } - messages.refresh(); - break; - case AgentServiceEventType.ttsStarted: - isSpeaking.value = true; - print('TTS开始播放'); - break; - - case AgentServiceEventType.ttsCompleted: - case AgentServiceEventType.ttsStopped: - case AgentServiceEventType.ttsCanceled: - isSpeaking.value = false; - - Logger.i(TAG, 'TTS停止播放, $event.type'); - break; - - case AgentServiceEventType.playbackStarted: - isSpeaking.value = true; - Logger.i(TAG, 'TTS播放开始'); - break; - - case AgentServiceEventType.playbackCompleted: - isSpeaking.value = false; - - Logger.i(TAG, 'TTS播放完成'); - break; - - case AgentServiceEventType.imageProcessing: - isImageProcessing.value = true; - break; - - case AgentServiceEventType.imageReady: - // 图片准备就绪,但AI还没有开始处理,保持processing状态 - break; - - case AgentServiceEventType.assistantToken: - if (!isProcessing.value) isProcessing.value = true; - final sessionid = event.data['sessionid'] ?? ''; - final token = event.data['token'] ?? ''; - // Logger.i(TAG, 'AI回复Token: $token, responseId: $responseId'); - if (token.isNotEmpty) { - // 如果是新的回复或者响应ID改变,创建新消息 - final index = messages.lastIndexWhere( - (msg) => msg.sessionid == sessionid && !msg.isUser); - if (index == -1) { - messages.add( - Message(sessionid: sessionid, isUser: false, text: token)); - _isNewAssistantResponse = false; - } else { - messages[index] = Message( - sessionid: sessionid, - isUser: false, - text: messages[index].text + token, - meta: messages[index].meta, - timestamp: messages[index].timestamp, - functionCalls: messages[index].functionCalls, - isToolCallsExpanded: messages[index].isToolCallsExpanded, - ); - } - // 使用reverse:true时通常不需要手动滚动 - } - break; - - case AgentServiceEventType.assistantResponse: - isProcessing.value = false; - isImageProcessing.value = false; - // 标记当前回复完成 - _isNewAssistantResponse = true; - newAIReplyCompleted.value = true; - break; - - case AgentServiceEventType.error: - final sessionid = event.data['sessionid'] ?? ''; - final message = event.data['message'] ?? ''; - final code = event.data['code'] ?? 0; - // //验证错误码 - if (code == 1000) { - isListening.value = false; - isSpeaking.value = false; - isProcessing.value = false; - isImageProcessing.value = false; - if (sessionid != "") { - final index = messages.lastIndexWhere( - (msg) => msg.sessionid == sessionid && !msg.isUser); - if (index >= 0) { - messages.removeAt(index); - messages.refresh(); - } - } - Get.snackbar('error'.tr, '${event.data['message']}'); - } else if (code == 2001) { - // 余额不足或需要充值,给出弹窗确认是否跳转充值界面 - - showRechargeDialog('agent'); - } - Logger.e(TAG, '代理服务错误: ${event.data['message']}'); - break; - - case AgentServiceEventType.autoStop: - isListening.value = false; - final sessionid = event.data['sessionid'] ?? ''; - // 移除临时的识别消息 - final index = messages.lastIndexWhere( - (msg) => msg.sessionid == sessionid && msg.isUser); - if (index >= 0) { - // 只有当消息为空或文本太短时才移除 - if (messages[index].text.trim().length < 2) { - messages.removeAt(index); - // messages.refresh(); - } else { - // 否则转换为正式消息 - messages[index] = Message( - sessionid: sessionid, - isUser: true, - text: messages[index].text, - timestamp: messages[index].timestamp, - functionCalls: messages[index].functionCalls, - isToolCallsExpanded: messages[index].isToolCallsExpanded, - ); - // messages.refresh(); - - // 发送此消息到AI处理 - isProcessing.value = true; - _isNewAssistantResponse = true; - _handleAutoStop(sessionid, index); - } - } - break; - - case AgentServiceEventType.functionCall: - final sessionid = event.data['sessionid'] ?? ''; - final functionName = event.data['name'] ?? ''; - final arguments = event.data['arguments'] ?? ''; - // debugPrint('Fultter 开始调用 mcp: $functionName, $arguments'); - if (functionName == 'exit_interaction') { - if (isPhoneCallMode.value) { - endPhoneCall(); //退出语音交互,同时退出打电话模式 - } - } - final index = messages.lastIndexWhere( - (msg) => msg.sessionid == sessionid && !msg.isUser); - if (index == -1) { - messages.add(Message( - sessionid: sessionid, - isUser: false, - text: '', - functionCalls: { - functionName: {'arguments': arguments} - })); - _isNewAssistantResponse = false; - } else { - // 否则更新最后一条消息 - final lastIndex = messages.length - 1; - - if (lastIndex >= 0 && !messages[lastIndex].isUser) { - messages[lastIndex] = Message( - sessionid: sessionid, - isUser: false, - text: messages[lastIndex].text, - meta: messages[lastIndex].meta, - timestamp: messages[lastIndex].timestamp, - functionCalls: messages[lastIndex].functionCalls ?? {} - ..addAll({ - functionName: {'arguments': arguments} - }), - ); - } - } - break; - - case AgentServiceEventType.functionCallResult: - final sessionid = event.data['sessionid'] ?? ''; - final function_name = event.data['function_name'] ?? ''; - final function_result = event.data['result'] ?? ''; - final metaStr = event.data['meta'] ?? ''; - final index = messages.lastIndexWhere( - (msg) => msg.sessionid == sessionid && !msg.isUser); - if (index != -1) { - // 获取当前的 functionCalls,如果为空则创建新的 Map - final functionCalls = Map>.from( - messages[index].functionCalls?.map((key, value) => MapEntry( - key, Map.from(value as Map))) ?? - {}); - - // 更新对应工具的调用结果 - if (function_name.isNotEmpty) { - // 如果该工具已存在,更新其结果;如果不存在,创建新的条目 - if (functionCalls.containsKey(function_name)) { - functionCalls[function_name]!['result'] = function_result; - } else { - functionCalls[function_name] = { - 'result': function_result, - }; - } - } - - messages[index] = Message( - sessionid: sessionid, - isUser: false, - text: messages[index].text, - meta: messages[index].meta, - timestamp: messages[index].timestamp, - functionCalls: functionCalls, - isToolCallsExpanded: messages[index].isToolCallsExpanded, - ); - } - Logger.i(TAG, 'Flutter 结束调用 $sessionid mcp: $metaStr'); - - if (metaStr.isNotEmpty) { - Map metaResult = {}; - metaResult = jsonDecode(metaStr) as Map; - if (metaResult.isNotEmpty) { - var notreply = false; - if (metaResult.containsKey("notreply")) { - // 如果有导航卡片,直接添加 - notreply = metaResult["notreply"] as bool; - } - if (notreply) { - Logger.i(TAG, 'Flutter 结束调用 不回复'); - //不回复直接返回 - break; - } - - Logger.i(TAG, 'Flutter 结束调用 卡片数据 $index $metaResult'); - // 判断是否为新的回复或响应ID是否改变 - if (index == -1) { - // 添加新消息 - messages.add(Message( - sessionid: sessionid, - isUser: false, - text: "", - meta: metaResult)); - _isNewAssistantResponse = false; - } else { - // 更新最后一条消息 - messages[index] = Message( - sessionid: sessionid, - isUser: false, - text: messages[index].text, - meta: metaResult, - timestamp: messages[index].timestamp, - functionCalls: messages[index].functionCalls, - isToolCallsExpanded: messages[index].isToolCallsExpanded, - ); - } - messages.refresh(); - if (metaResult.containsKey("maps_route_navigation")) { - final maps_route_navigation = - metaResult["maps_route_navigation"]; - if (maps_route_navigation is Map) { - final card_navigation = - maps_route_navigation["card_navigation"]; - final isinternal = metaResult["isinternal"] ?? false; - if (card_navigation is Map) { - //导航 - final start = card_navigation["start"] as String; - final destination = - card_navigation["destination"] as String; - final end_longitude = - card_navigation["end_longitude"] as String; - final end_latitude = - card_navigation["end_latitude"] as String; - // End_longitude string `json:"end_longitude"` //目标点经度 - // End_latitude string `json:"end_latitude"` //目标点纬度 - final items = card_navigation['items']; - if (items != null) { - Future.delayed(Duration(seconds: 2), () { - // Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end '); - // 2秒后执行的代码 - final navigationMode = - AppConfig.env('APP_NAVIGATION_MODE') ?? - 'external_auto'; - if (navigationMode == 'internal_gaode' || isinternal) { - _navigationManager.open( - start, "$end_longitude,$end_latitude", "driving"); - } - // openNavigation(false, start, destination, end_latitude, - // end_longitude, ""); - }); - } - } - } - } else if (metaResult - .containsKey("maps_google_route_navigation")) { - //导航 - final maps_google_route_navigation_raw = - metaResult["maps_google_route_navigation"]; - - // 安全的类型检查 - if (maps_google_route_navigation_raw is Map) { - final maps_google_route_navigation = - maps_google_route_navigation_raw; - final data = maps_google_route_navigation["data"]; - if (data != null) { - final end = data["destination"] as String; - final mode = data["items"][0]["ntype"] as String; - Future.delayed(Duration(seconds: 1), () { - // Logger.i(TAG, 'liwei-------- 打开google地图: $end '); - openGoogleMap(end, mode); - }); - } - } - } - } else { - // 其他函数调用 - Logger.i(TAG, 'Flutter 结束调用 解析失败 $metaStr'); - } - } - if (function_name.contains("music")) { - Future.delayed(Duration(seconds: 1), () { - Logger.i(TAG, '音乐服务 $musicServiceType'); - if (musicServiceType == "QQ") { - _qqmusicManager.syncCurrentPlayInfo(isOpenPlay: false); - } else if (musicServiceType == "WY") { - musicStatus(); - } - }); - // } - } - break; - case AgentServiceEventType.assistantUsages: - final sessionid = event.data['sessionid'] ?? ''; - final integral = event.data['integral'] ?? 0; - User.instance.aichatintegral = integral; - break; - default: - break; - } - }); - - //后台导航事件同步 - _amapeventSubscription = _navigationManager.events().listen((event) { - switch (event.type) { - case AmapServiceEventType.starte: - Logger.i(TAG, '导航通知 started!'); - break; - case AmapServiceEventType.stope: - Logger.i(TAG, '导航通知 stoped!'); - break; - case AmapServiceEventType.update: - Logger.i(TAG, '导航通知 update! $event'); - Message? result; - - try { - result = messages.lastWhere( - (msg) => - !msg.isUser && - msg.meta?.containsKey("card_navigation") == true, - ); - } catch (e) { - result = null; - } - - if (result != null) { - int index = messages.indexOf(result); - if (index != -1) { - // ✅ 创建 meta 的深拷贝 - final newMeta = Map.from(result.meta ?? {}); - newMeta["card_navigation"]["navigation"] = event.data; - - // ✅ 创建新的 Message 替换原来的 - messages[index] = Message( - sessionid: result.sessionid, - isUser: result.isUser, - text: result.text, - meta: newMeta, - timestamp: result.timestamp, - ); - messages.refresh(); - Logger.i( - TAG, '更新导航卡片! ${newMeta["card_navigation"]["navigation"]}'); - } - } - break; - case AmapServiceEventType.error: - Logger.i(TAG, '导航通知 error!'); - break; - case AmapServiceEventType.unknown: - Logger.i(TAG, '导航通知 unknown!'); - break; - } - }); - } - - // 发送消息(包括所有支持的消息类型) - //场景1:用户在文本输入框中输入文本,点击发送按钮,调用sendMessage()方法。 - //场景2:当用户选择图片后在文本框输入内容并发送时,调用sendMessage()转发给sendImageMessage()。 - Future sendMessage() async { - // 收回键盘 - FocusManager.instance.primaryFocus?.unfocus(); - // 1. 处理图片+文本的混合输入 - if (isImageInputActive.value && selectedImagePath.value != null) { - final text = textController.text.trim(); - final imagePath = selectedImagePath.value!; // 使用!强制断言非空 - await sendImageMessage(imagePath, text: text); - - // 清除图片输入状态 - clearImageInput(); - return; - } - - // 2. 处理纯文本消息 - final text = textController.text.trim(); - if (text.isEmpty) return; - - // 使用reverse:true时通常不需要手动滚动 - // 3. 调用AI服务处理 - try { - isProcessing.value = true; - // 标记为新的AI回复 - _isNewAssistantResponse = true; - newAIReplyCompleted.value = false; - currsessionid.value = const Uuid().v4(); - final sessionid = currsessionid.value; - // 根据TTS状态决定是否朗读 - AgentService.processTextInput(sessionid, text, - speakResponse: isTtsEnabled.value); - final message = Message(sessionid: sessionid, isUser: true, text: text); - Logger.i(TAG, '发送AI消息:$sessionid $text'); - messages.add(message); - textController.clear(); - } catch (e) { - Logger.e(TAG, '发送文本消息失败: $e'); - isProcessing.value = false; - } - } - - // 发送图片消息 - Future sendImageMessage(String imagePath, {String? text}) async { - if (imagePath.isEmpty || !File(imagePath).existsSync()) { - Logger.e(TAG, '图片不存在: $imagePath'); - return; - } - - // 复制图片到应用永久存储目录 - // 复制图片到应用永久存储目录 - final imagePathResult = await _copyImageToPermanentStorage(imagePath); - final permanentImagePath = imagePathResult['fullPath']!; - final relativeImagePath = imagePathResult['relativePath']!; - // rootDir 现在是服务端的 scene 白名单值,**不要自己拼 User// 前缀**—— - // 目录由服务端按会话里的 uid 拼死,客户端再拼一层只会变成重复路径。 - var ImageUrl = await UploadOss.upload( - filepath: permanentImagePath, rootDir: 'ChatImages'); - - // 显示的文本,如果没有提供则使用默认值 - final displayText = text?.isNotEmpty == true ? text! : '[图片]'; - - currsessionid.value = const Uuid().v4(); - final sessionid = currsessionid.value; - Logger.i(TAG, - '发送AI图片消息:$sessionid $displayText $permanentImagePath URL: $ImageUrl'); - // 添加用户消息 - final message = Message( - sessionid: sessionid, - isUser: true, - text: displayText, - hasImage: true, - imagePath: relativeImagePath, - ); - messages.add(message); - - // 清空文本输入 - textController.clear(); - - // 使用reverse:true时通常不需要手动滚动 - - try { - isProcessing.value = true; - isImageProcessing.value = true; - // 标记为新的AI回复 - _isNewAssistantResponse = true; - - // 保存消息到持久化存储 - //await _saveMessageToStorage(message); - currsessionid.value = const Uuid().v4(); - final sessionid = currsessionid.value; - // 调用Agent Service处理图片,考虑TTS状态 - await AgentService.processImageInput( - sessionid, - relativeImagePath, - ImageUrl, - text: text ?? '', - speakResponse: isTtsEnabled.value, - ); - } catch (e) { - Logger.e(TAG, '处理图片失败: $e'); - isProcessing.value = false; - isImageProcessing.value = false; - } - } - -// 将图片复制到应用的永久存储目录 - Future> _copyImageToPermanentStorage( - String sourcePath) async { - try { - final File sourceFile = File(sourcePath); - if (!await sourceFile.exists()) { - Logger.e(TAG, '源图片文件不存在: $sourcePath'); - return { - 'fullPath': sourcePath, - 'relativePath': sourcePath - }; // 如果源文件不存在,返回原路径 - } - - // 创建应用文档目录下的chatImages文件夹 - final appDir = await getApplicationDocumentsDirectory(); - final imagesDir = Directory('${appDir.path}/chatImages'); - if (!await imagesDir.exists()) { - await imagesDir.create(recursive: true); - } - - // 生成唯一文件名 - final fileName = - '${DateTime.now().millisecondsSinceEpoch}_${sourcePath.split('/').last}'; - final targetPath = '${imagesDir.path}/$fileName'; - final relativePath = '/chatImages/$fileName'; - - // 复制文件 - await sourceFile.copy(targetPath); - Logger.i(TAG, '图片已复制到永久存储: $targetPath'); - - return {'fullPath': targetPath, 'relativePath': relativePath}; - } catch (e) { - Logger.e(TAG, '复制图片到永久存储失败: $e'); - return {'fullPath': sourcePath, 'relativePath': sourcePath}; // 出错时返回原路径 - } - } - - // 切换输入模式(改造为按住说话模式) - void toggleInputMode() async { - // 如果当前在图片输入模式,先清除 - if (isImageInputActive.value) { - clearImageInput(); - return; - } - - // 如果要切换到语音模式,需要先检查麦克风权限 - if (isTextInputMode.value) { - // 使用 PermissionUtil 检查麦克风权限 - final hasPermission = await PermissionUtil.instance.requestPermission( - // context: Get.context!, - permissionType: Permission.microphone, - permissionName: 'microphonePermission'.tr, // 麦克风 - explanationText: 'voiceRecognitionPermissionText'.tr, // 语音识别功能需要访问麦克风。 - permanentDenialText: - 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 - settingsButtonText: 'goToSettings'.tr, // 去设置 - ); - if (!hasPermission) { - return; // 权限未获取,保持文本模式 - } - } - - isTextInputMode.toggle(); - - // 切换到语音模式时,启用按住说话模式 - // 切换到文本模式时,禁用按住说话模式并停止任何正在进行的语音输入 - if (isTextInputMode.value) { - // 切换到文本模式 - isTextInputMode.value = true; - // stopVoiceInput(); - } else { - // 切换到语音模式,启用按住说话模式 - isTextInputMode.value = false; - } - } - - // 开始语音输入 - Future startVoiceInput() async { - if (isListening.value) return; - Logger.i(TAG, '开始语音输入'); // 开始语音输入 - // final hasPermission = await PermissionUtil.instance.requestPermission( - // // context: Get.context!, - // permissionType: Permission.microphone, - // permissionName: 'microphonePermission'.tr, // 麦克风 - // explanationText: 'voiceTranslationPermissionText'.tr, // 语音识别与翻译功能需要访问麦克风。 - // permanentDenialText: - // 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 - // settingsButtonText: 'goToSettings'.tr, // 去设置 - // cancelButtonText: 'cancel'.tr, // 取消 - // ); - // if (!hasPermission) { - // isTextInputMode.value = true; // 如果没有权限,切回文本模式 - // return; - // } - - try { - await AgentService.stopConversation(); - - await AgentService.recognizeCallback(); - if (_bluetoothManager.currentDeviceRx.value != null) { - // await AgentService.restoreOriginalAudioState(); - } else { - // await AgentService.disableBluetoothAudio(); - } - } catch (e) { - Logger.e(TAG, '启动识别器失败: $e'); - } - } - - // 停止语音输入 - Future stopVoiceInput() async { - if (!isListening.value) return; - - try { - await AgentService.stopConversation(); - } catch (e) { - Logger.e(TAG, '停止语音输入失败: $e'); - } - } - - // 清除聊天历史 - Future clearChat() async { - try { - await AgentService.clearChatHistory(); - - // 清空本地消息列表 - messages.clear(); - - // 添加欢迎消息 - _addWelcomeMessage(); - - // 重置回复状态 - _isNewAssistantResponse = true; - } catch (e) { - Logger.e(TAG, '清除聊天历史失败: $e'); - } - } - - // 从相册选择图片 - Future pickImage() async { - try { - final ImagePicker picker = ImagePicker(); - final XFile? image = await picker.pickImage( - source: ImageSource.gallery, - imageQuality: 80, // 设置图片质量 - maxWidth: 1024, // 限制最大宽度 - maxHeight: 1024, // 限制最大高度 - ); - - if (image != null) { - // 设置已选择的图片 - selectedImagePath.value = image.path; - // 激活图片输入模式 - isImageInputActive.value = true; - } - } catch (e) { - Logger.e(TAG, '选择图片失败: $e'); - } - } - - // 拍照获取图片 - Future takePhoto() async { - try { - final ImagePicker picker = ImagePicker(); - final XFile? photo = await picker.pickImage( - source: ImageSource.camera, - imageQuality: 80, // 设置图片质量 - maxWidth: 1024, // 限制最大宽度 - maxHeight: 1024, // 限制最大高度 - ); - - if (photo != null) { - // 设置已选择的图片 - selectedImagePath.value = photo.path; - // 激活图片输入模式 - isImageInputActive.value = true; - } - } catch (e) { - Logger.e(TAG, '拍照失败: $e'); - } - } - - // 清除图片输入状态 - void clearImageInput() { - selectedImagePath.value = null; - isImageInputActive.value = false; - } - - // 切换'+'选项展开状态 - void togglePlusOptions() { - if (isPlusOptionsExpanded.value) { - // 如果当前已展开,则收缩 - collapsePlusOptions(); - _pendingPlusOptionsExpansion = false; // 取消待展开状态 - } else { - // 如果当前未展开,则展开并关闭键盘 - if (isKeyboardVisible.value) { - // 设置待展开标记 - _pendingPlusOptionsExpansion = true; - hideKeyboard(); - // 使用双重机制:轮询检测 + 键盘状态监听 - _waitForKeyboardToClose(); - } else { - // 键盘未显示,直接展开 - isPlusOptionsExpanded.value = true; - } - } - } - - // 等待键盘关闭的轮询机制 - void _waitForKeyboardToClose() { - int attempts = 0; - const maxAttempts = 30; // 最多尝试30次,总计3秒 - const checkInterval = Duration(milliseconds: 100); // 每100ms检查一次 - - Timer.periodic(checkInterval, (timer) { - attempts++; - - // 检查是否还需要展开(用户可能已经取消了) - if (!_pendingPlusOptionsExpansion) { - timer.cancel(); - return; - } - - if (!isKeyboardVisible.value) { - // 键盘已关闭,展开选项 - timer.cancel(); - _pendingPlusOptionsExpansion = false; - // 添加小延迟确保UI稳定 - Future.delayed(const Duration(milliseconds: 50), () { - if (!isKeyboardVisible.value) { - // 再次确认键盘状态 - isPlusOptionsExpanded.value = true; - } - }); - } else if (attempts >= maxAttempts) { - // 超时,强制展开(可能键盘检测有问题) - timer.cancel(); - _pendingPlusOptionsExpansion = false; - Logger.d('AgentController', '键盘关闭检测超时,强制展开选项'); - isPlusOptionsExpanded.value = true; - } - }); - } - - // 收缩'+'选项 - void collapsePlusOptions() { - isPlusOptionsExpanded.value = false; - } - - // 更新键盘可见状态 - void updateKeyboardVisibility(bool isVisible) { - final wasVisible = isKeyboardVisible.value; - isKeyboardVisible.value = isVisible; - - // 只有当键盘从隐藏变为显示时,才收缩'+'选项 - // 避免键盘关闭时误触发收缩逻辑 - if (!wasVisible && isVisible && isPlusOptionsExpanded.value) { - collapsePlusOptions(); - } - - // 如果键盘关闭了,并且有待展开的选项,则展开 - if (wasVisible && !isVisible && _pendingPlusOptionsExpansion) { - _pendingPlusOptionsExpansion = false; - Future.delayed(const Duration(milliseconds: 100), () { - if (!isKeyboardVisible.value) { - isPlusOptionsExpanded.value = true; - } - }); - } - } - - // 标记是否有待展开的'+'选项 - bool _pendingPlusOptionsExpansion = false; - - // 隐藏键盘 - void hideKeyboard() { - FocusManager.instance.primaryFocus?.unfocus(); - } - - // 使用预设提示词处理图片 - void useImagePrompt(String prompt) { - // 确保selectedImagePath.value不为null - if (selectedImagePath.value == null) return; - - // 填充文本框同时直接发送消息 - textController.text = prompt; - // 立即发送图片和提示词 - final imagePath = selectedImagePath.value!; - sendImageMessage(imagePath, text: prompt); - // 发送后清除图片输入状态 - clearImageInput(); - } - - // 预设提示词:总结图片内容 - void summarizeImage() { - useImagePrompt('请总结这张图片的内容'); - } - - // 预设提示词:抽取图像文字 - void extractTextFromImage() { - useImagePrompt('请提取这张图片中的所有文字'); - } - - // 预设提示词:翻译图像文字 - void translateImageText() { - useImagePrompt('请翻译这张图片中的文字'); - } - - // 预设提示词处理(文本输入模式) - void preSetPromptAction(String presetType) { - // 确保在文本输入模式下 - if (!isTextInputMode.value) { - // 如果不在文本模式,先切换到文本模式 - isTextInputMode.value = true; - } - - switch (presetType) { - case 'Music': - textController.text = 'playSomeMusic'.tr; - break; - case 'Navigation': - textController.text = 'navigationTo'.tr; - break; - case 'Weather': - textController.text = 'todayWeather'.tr; - break; - case 'News': - textController.text = 'todayNews'.tr; - break; - case 'Food': - textController.text = 'nearbyFood'.tr; - break; - default: - textController.text = presetType; - break; - } - - // 自动发送消息 - if (presetType != 'Navigation') { - sendMessage(); - } - } - - // 模式历史记录 - VoiceInputMode? _previousInputMode; - - // 保存当前输入模式 - void _saveCurrentInputMode() { - if (!isTextInputMode.value) { - _previousInputMode = VoiceInputMode.pushToTalk; - } else { - _previousInputMode = VoiceInputMode.text; - } - // else { - // _previousInputMode = null; - // } - } - - // 恢复之前的输入模式 - void _restorePreviousInputMode() { - switch (_previousInputMode) { - case VoiceInputMode.pushToTalk: - isTextInputMode.value = false; - //isPushToTalkMode.value = true; - break; - case VoiceInputMode.text: - isTextInputMode.value = true; - // isPushToTalkMode.value = false; - break; - default: - // 默认恢复到文字模式 - isTextInputMode.value = true; - //isPushToTalkMode.value = false; - break; - } - _previousInputMode = null; // 清除历史记录 - } - - // 开始打电话模式(支持耳机唤醒) - Future startPhoneCall({bool isHeadsetTriggered = false}) async { - Logger.i(TAG, '🔥 开始打电话模式 (耳机触发: $isHeadsetTriggered)'); - - // 保存当前输入模式,以便通话结束后恢复 - _saveCurrentInputMode(); - - // 收缩"+"选项 - collapsePlusOptions(); - - // 进入打电话模式 - isPhoneCallMode.value = true; - - // 如果当前是文本模式,切换到语音模式???? - // if (isListening.value) { - // 检查麦克风权限 - final hasPermission = await PermissionUtil.instance.requestPermission( - permissionType: Permission.microphone, - permissionName: 'microphonePermission'.tr, // 麦克风 - explanationText: 'phoneCallPermissionText'.tr, // 打电话功能需要访问麦克风进行语音通话。 - permanentDenialText: - 'permissionDeniedText'.tr, // 您已永久拒绝麦克风权限,请前往系统设置开启权限。 - settingsButtonText: 'goToSettings'.tr, // 去设置 - ); - - if (!hasPermission) { - // 权限未获取,退出打电话模式并恢复之前状态 - isPhoneCallMode.value = false; - _previousInputMode = null; // 清除历史记录 - return; - } - - // 打电话模式 - isTextInputMode.value = false; - Logger.i(TAG, '打电话模式已启动,开始语音输入'); - await AgentService.startConversation(mode: 'phone_call'); - } - - // 结束打电话模式 - void endPhoneCall() async { - Logger.i(TAG, '🔥 结束打电话模式'); - - try { - // 如果正在录音,先停止录音 - if (isListening.value) { - final result = await AgentService.stopConversation(); - - if (result) { - isListening.value = false; - } - } - - // 退出打电话模式 - isPhoneCallMode.value = false; - _restorePreviousInputMode(); - } catch (e) { - Logger.e(TAG, '结束打电话模式失败: $e'); - - // 即使出现异常,也要确保退出打电话模式 - isPhoneCallMode.value = false; - isListening.value = false; - _restorePreviousInputMode(); - } - } - - // 打电话模式专用:切换录音状态 - Future togglePhoneCallRecording() async { - Logger.i(TAG, '🎤 打电话模式切换录音状态'); - // 只有在打电话模式下才能使用此方法 - if (!isPhoneCallMode.value) { - Logger.w(TAG, '不在打电话模式,无法切换录音状态'); - return; - } - - try { - if (isListening.value) { - // 当前正在录音,停止录音 - Logger.i(TAG, '停止打电话模式录音'); - final result = await AgentService.stopConversation(); - - // 手动更新状态,确保UI同步 - if (result) { - isListening.value = false; - } - } else { - // 当前未录音,开始录音 - Logger.i(TAG, '开始打电话模式录音'); - final result = await AgentService.startConversation(mode: 'phone_call'); - - // 手动更新状态,确保UI同步 - if (result) { - isListening.value = true; - } - } - } catch (e) { - Logger.e(TAG, '打电话模式录音状态切换失败: $e'); - } - } - - // 耳机唤醒通话模式(专用入口) - Future startHeadsetCall() async { - Logger.i(TAG, '🎧 耳机唤醒通话模式'); - if (canStartHeadsetCall()) { - await startPhoneCall(isHeadsetTriggered: true); - } - } - - // ─── 设备通话模式(device_jieli AI 助手通路) ──────────────────── - // 走 device_jieli 的 AI 助手通路(speechStart/speechStop): - // 上行帧由 SpeechAudioEvent 派发,PCM_S16LE/16k/mono/20ms,直接灌给 ASR。 - // 与翻译 / 设备录音互斥;不影响已有 phone_call / push_to_talk 任何路径。 - - /// 启动设备通话模式 - Future startDeviceCall() async { - Logger.i(TAG, '🎧 开始设备通话模式(AI 助手通路)'); - if (isDeviceCallMode.value) { - Logger.w(TAG, '已处于设备通话模式,忽略'); - return; - } - - // 必须已连接杰理耳机;iOS / Android 都走 device_jieli 的 AI 助理通路 - final BleManager bleManager = Get.find(); - if (!bleManager.isConnected) { - // 用已有的 'tip'(全项目 30 处在用),不要写 'tips'——那个 key 一个语言 - // 文件里都没有,.tr 会原样显示 "tips"。 - Get.snackbar('tip'.tr, '请先连接耳机'); - return; - } - - // 收缩 + 号 - collapsePlusOptions(); - - isDeviceCallMode.value = true; - isDeviceCallMicMuted.value = false; - _deviceCallPushing = true; - - try { - // 先停之前可能存在的会话(不同模式之间切换保底) - await AgentService.stopConversation(); - - // 订阅 device_jieli 事件流,按 uplink 推给 ASR - await _deviceCallJieliSub?.cancel(); - _deviceCallJieliSub = - Jielihome.instance.events.listen(_onDeviceCallEvent); - // ignore: avoid_print - print( - '[DEVCALL] _deviceCallJieliSub 已订阅 sub=$_deviceCallJieliSub mode=${isDeviceCallMode.value} pushing=$_deviceCallPushing', - ); - - // 启动 AgentService 外部音频模式 + 电话通话模式(避免 push_to_talk 静默超时) - final started = await AgentService.startConversation( - mode: 'phone_call', - isExternalActive: true, - ); - if (!started) { - Logger.w(TAG, '设备通话:AgentService 启动失败'); - } - - // 启动 AI 助理通路(JieliAssistantPort):MODE_RECORD + DEVICE_ALWAYS_RECORDING, - // 耳机自主持续推 OPUS,由 Port 解码成 16k/16bit/mono/20ms PCM 帧 - final ok = await Jielihome.instance.assistantStart(); - if (!ok) { - Logger.w(TAG, '设备通话:assistantStart 启动失败'); - await _stopDeviceCallInternal(); - Get.snackbar('error'.tr, '设备通话启动失败'); - return; - } - Logger.i(TAG, '设备通话模式已启动(JieliAssistantPort)'); - } catch (e) { - Logger.e(TAG, '启动设备通话失败: $e'); - await _stopDeviceCallInternal(); - Get.snackbar('error'.tr, '设备通话启动失败'); - } - } - - /// 结束设备通话模式 - Future endDeviceCall() async { - Logger.i(TAG, '🎧 结束设备通话模式'); - await _stopDeviceCallInternal(); - } - - /// 设备通话模式下切换静音(丢帧) - void toggleDeviceCallMic() { - if (!isDeviceCallMode.value) return; - isDeviceCallMicMuted.value = !isDeviceCallMicMuted.value; - _deviceCallPushing = !isDeviceCallMicMuted.value; - Logger.i(TAG, '设备通话静音切换: muted=${isDeviceCallMicMuted.value}'); - } - - Future _stopDeviceCallInternal() async { - _deviceCallPushing = false; - try { - await _deviceCallJieliSub?.cancel(); - } catch (_) {} - _deviceCallJieliSub = null; - - try { - await Jielihome.instance.assistantStop(); - } catch (e) { - Logger.w(TAG, 'assistantStop 失败(忽略): $e'); - } - - try { - await AgentService.stopConversation(); - } catch (e) { - Logger.w(TAG, 'stopConversation 失败(忽略): $e'); - } - - isDeviceCallMicMuted.value = false; - isDeviceCallMode.value = false; - isListening.value = false; - } - - void _onDeviceCallEvent(JieliEvent event) { - // 入口诊断:每秒打一次"是否进入此函数",无条件,用 print 走 I/flutter 通道 - final now0 = DateTime.now().millisecondsSinceEpoch; - if (event is AssistantAudioEvent) { - _deviceCallPushedFrames += 1; - _deviceCallPushedBytes += event.pcm.length; - if (now0 - _deviceCallLastReportMs >= 1000) { - // ignore: avoid_print - print( - '[DEVCALL] _onDeviceCallEvent 入口 (last 1s): frames=$_deviceCallPushedFrames bytes=$_deviceCallPushedBytes mode=${isDeviceCallMode.value} pushing=$_deviceCallPushing seq=${event.sequence}', - ); - _deviceCallPushedFrames = 0; - _deviceCallPushedBytes = 0; - _deviceCallLastReportMs = now0; - } - } - if (!isDeviceCallMode.value) return; - if (event is AssistantAudioEvent) { - if (!_deviceCallPushing) return; - if (event.encoding != 'pcm16') return; - if (event.pcm.isEmpty) return; - // fire-and-forget 推送给 ASR - AgentService.pushAudioData(event.pcm).catchError((e) { - Logger.w(TAG, 'pushAudioData 失败: $e'); - return false; - }); - } else if (event is AssistantStartEvent) { - Logger.i(TAG, 'AI 助理通路已启动(端侧通知)sr=${event.sampleRate}'); - } else if (event is AssistantErrorEvent) { - Logger.e(TAG, 'AI 助理错误 code=${event.code} msg=${event.message ?? ''}'); - } else if (event is AssistantEndEvent) { - Logger.i(TAG, 'AI 助理通路已停止(端侧通知)'); - } - } - - // 检查当前是否可以启动耳机通话 - bool canStartHeadsetCall() { - // 如果已经在打电话模式,不允许重复启动 - if (isPhoneCallMode.value) { - Logger.w(TAG, '已在打电话模式,忽略耳机唤醒'); - return false; - } - - // 如果正在处理语音或TTS,暂时不允许启动 - if (isListening.value || isSpeaking.value || isProcessing.value) { - Logger.w(TAG, '当前正在处理语音,延迟耳机唤醒'); - return false; - } - - return true; - } - - // 验证语音输入状态一致性 - void _validateVoiceInputStates() { - // 检查状态冲突 - // if (isPhoneCallMode.value && isPushToTalkMode.value) { - // Logger.w(TAG, '状态冲突:同时启用打电话和按住说话模式,优先保持打电话模式'); - // isPushToTalkMode.value = false; - // } - - // // 检查文字模式与语音模式的一致性 - // if (isTextInputMode.value && - // (isPushToTalkMode.value || isPhoneCallMode.value)) { - // Logger.w(TAG, '状态冲突:文字模式与语音模式同时启用,优先保持语音模式'); - // isTextInputMode.value = false; - // } - - // // 检查按住说话状态的合理性 - // if (isPushToTalkActive.value && !isPushToTalkMode.value) { - // Logger.w(TAG, '状态冲突:按住说话激活但未启用按住说话模式'); - // isPushToTalkActive.value = false; - // } - } - - // 安全的状态切换方法 - void _safeStateTransition(VoiceInputMode targetMode) { - Logger.i(TAG, '安全状态切换到: $targetMode'); - - switch (targetMode) { - case VoiceInputMode.text: - isTextInputMode.value = true; - // isPushToTalkMode.value = false; - isPhoneCallMode.value = false; - isPushToTalkActive.value = false; - break; - - case VoiceInputMode.pushToTalk: - isTextInputMode.value = false; - // isPushToTalkMode.value = true; - isPhoneCallMode.value = false; - isPushToTalkActive.value = false; - break; - - case VoiceInputMode.phoneCall: - isTextInputMode.value = false; - // isPushToTalkMode.value = false; - isPhoneCallMode.value = true; - isPushToTalkActive.value = false; - break; - } - - // 验证状态一致性 - _validateVoiceInputStates(); - } - - // 开始按住说话 - void startPushToTalk() async { - print('liwei--------- 🎤 开始按住说话'); - - // 只有在语音模式且按住说话模式下才能按住说话 - if (isTextInputMode.value) return; - - // 如果已经在按住说话,不重复开始 - if (isPushToTalkActive.value) return; - - // 设置按住说话状态 - isPushToTalkActive.value = true; - //await AgentService.recognizeCallback(); - // 开始语音识别 - AgentService.startConversation(mode: 'push_to_talk'); - // await startVoiceInput(); - } - - // 结束按住说话 - void endPushToTalk() async { - print('liwei----------🎤 结束按住说话'); - - // 如果不在按住说话状态,直接返回 - if (!isPushToTalkActive.value) return; - - // 清除按住说话状态(但保持按住说话模式) - isPushToTalkActive.value = false; - - AgentService.stopConversation(); - // 停止语音识别 - // await stopVoiceInput(); - } - - // 获取音乐播放状态 - Future musicStatus() async { - try { - var status = await _musiceManager.getStatus(); - isPlaying.value = status['isPlaying'] ?? false; - currentId.value = status['id'] as int; - Logger.d( - TAG, '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}'); - Logger.d( - TAG, '音乐状态更新: playing=${isPlaying.value}, url=${currentId.value}'); - } catch (e) { - Logger.e(TAG, '获取音乐状态失败: $e'); - } - } - - // 播放音乐 - Future musicPlay(Map song) async { - try { - Logger.d(TAG, '开始播放音乐: ${song['title'] ?? song['name']}'); - - // 立即更新UI状态,提供即时反馈 - isPlaying.value = true; - // 修复:确保currentId接收int类型 - final songId = song['id']; - currentId.value = songId ?? 0; - - // 重置进度状态 - currentPosition.value = 0; - duration.value = 0; - progress.value = 0.0; - - // 转换为MusicManager期望的格式 - final musicData = { - 'id': song['id'], - 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', - 'artist': - song['artist']?.toString() ?? song['sgener']?.toString() ?? '', - 'coverUrl': - song['coverUrl']?.toString() ?? song['image']?.toString() ?? '', - }; - // 调用音乐管理器播放 - await _musiceManager.play(musicData); - // 更新媒体会话元数据和播放状态 - await _updateMediaSessionMetadata(musicData); - await _updateMediaSessionPlaybackState(true); - Logger.d(TAG, '音乐播放启动成功'); - } catch (e) { - Logger.e(TAG, '播放音乐失败: $e'); - // 发生错误时重置状态 - isPlaying.value = false; - currentId.value = 0; - } - } - - // 暂停音乐 - Future musicPause() async { - try { - Logger.d('AgentController', '暂停音乐播放'); - await _musiceManager.pause(); - isPlaying.value = false; - _stopProgressTimer(); - - // 更新媒体会话播放状态 - await _updateMediaSessionPlaybackState(false); - } catch (e) { - Logger.e('AgentController', '暂停音乐失败: $e'); - } - } - - // 恢复音乐播放 - Future musicResume() async { - try { - Logger.d('AgentController', '恢复播放音乐'); - await _musiceManager.resume(); - isPlaying.value = true; - - // 更新媒体会话播放状态 - await _updateMediaSessionPlaybackState(true); - - Logger.d('AgentController', '音乐恢复播放成功'); - } catch (e) { - Logger.e('AgentController', '恢复播放音乐失败: $e'); - isPlaying.value = false; - } - } - - // 停止进度更新定时器 - void _stopProgressTimer() { - if (_progressTimer != null) { - Logger.d('AgentController', '停止播放进度定时器'); - _progressTimer?.cancel(); - _progressTimer = null; - } - } - - // 获取播放进度 - Future updateMusicProgress(int currentIdInternal, int newPosition, - int newDuration, double newProgress) async { - try { - // 如果正在加载音乐,避免进度回调干扰状态更新 - if (isLoadingMusic.value) { - return; - } - - if (currentIdInternal != 0) { - isPlaying.value = true; - // return; // 如果没有在播放,直接返回 - } - - bool hasUpdate = false; - if (currentId.value != currentIdInternal) { - currentId.value = currentIdInternal; - hasUpdate = true; - } - // 只有当值发生变化时才更新,避免不必要的UI刷新 - if (currentPosition.value != newPosition) { - currentPosition.value = newPosition; - hasUpdate = true; - } - if (duration.value != newDuration) { - duration.value = newDuration; - hasUpdate = true; - } - if ((progress.value - newProgress).abs() > 0.001) { - // 避免浮点数精度问题 - progress.value = newProgress; - hasUpdate = true; - } - - // // 如果有进度更新,通知系统媒体会话更新进度信息 - // if (hasUpdate) { - // await _updateMediaSessionProgress(); - // } - } catch (e) { - Logger.e('AgentController', '获取播放进度失败: $e'); - } - } - - /// 更新系统媒体会话的进度信息 - Future _updateMediaSessionProgress() async { - try { - // 这里将调用music_service插件的方法来更新系统媒体会话进度 - // 具体实现将在music_service插件中添加 - await _musiceManager.updateMediaSessionProgress( - currentPosition.value, - duration.value, - progress.value, - ); - } catch (e) { - // 忽略错误,避免影响正常播放 - Logger.d('AgentController', '更新媒体会话进度失败: $e'); - } - } - - /// 更新系统媒体会话的元数据 - Future _updateMediaSessionMetadata(Map song) async { - try { - await _musiceManager.updateMediaSessionMetadata(song); - } catch (e) { - Logger.d('AgentController', '更新媒体会话元数据失败: $e'); - } - } - - /// 更新系统媒体会话的播放状态 - Future _updateMediaSessionPlaybackState(bool isPlaying) async { - try { - await _musiceManager.updateMediaSessionPlaybackState(isPlaying); - } catch (e) { - Logger.d('AgentController', '更新媒体会话播放状态失败: $e'); - } - } - - // 跳转到指定位置 - Future seekToPosition(int positionMs) async { - try { - await _musiceManager.seekTo(positionMs); - } catch (e) { - Logger.e('AgentController', '跳转播放位置失败: $e'); - } - } - - // 跳转到指定位置 - Future seekToProgress(int currentIdInternal, int newPosition, - int newDuration, double newProgress) async { - try { - // 立即更新进度状态 - await updateMusicProgress( - currentIdInternal, newPosition, newDuration, newProgress); - } catch (e) { - Logger.e('AgentController', '跳转播放位置失败: $e'); - } - } - - // 修改自动停止处理,遵循TTS设置 - void _handleAutoStop(String sessionid, int index) { - // 发送此消息到AI处理 - isProcessing.value = true; - // 标记为新的AI回复 - _isNewAssistantResponse = true; - currsessionid.value = sessionid; - AgentService.processTextInput(sessionid, messages[index].text, - speakResponse: isTtsEnabled.value); - } - - //去导航 - void openNavigation(bool isshow, String start, String destination, - String endlatitude, String endlongitude, String mode) { - print('liwei 导航:$destination $endlatitude $endlongitude $mode'); - if (isshow) { - // 弹出导航选择面板 - _showNavigationSelectionDialog( - start, destination, endlatitude, endlongitude, mode); - } else { - // 使用默认配置直接导航 - _navigateWithDefaultMode( - start, destination, endlatitude, endlongitude, mode); - } - } - - // 显示导航选择底部面板 - void _showNavigationSelectionDialog(String start, String destination, - String endlatitude, String endlongitude, String mode) { - Get.bottomSheet( - Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 顶部指示条 - Container( - margin: const EdgeInsets.only(top: 12), - width: 40, - height: 4, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(2), - ), - ), - // 标题 - Padding( - padding: const EdgeInsets.all(20), - child: Text( - 'selectNavigationApp'.tr, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - // 导航选项列表 - ListTile( - leading: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.map, color: Colors.blue), - ), - title: Text('amapMap'.tr), - subtitle: Text('useAmapNavigation'.tr), - onTap: () { - Get.back(); - openGaodeMap(destination, endlatitude, endlongitude, mode); - }, - ), - ListTile( - leading: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.green.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.map, color: Colors.green), - ), - title: Text('baiduMap'.tr), - subtitle: Text('useBaiduNavigation'.tr), - onTap: () { - Get.back(); - openBaiduMap(destination, endlatitude, endlongitude, mode); - }, - ), - ListTile( - leading: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.red.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.map, color: Colors.red), - ), - title: Text('googleMap'.tr), - subtitle: Text('useGoogleNavigation'.tr), - onTap: () { - Get.back(); - openGoogleMap(destination, mode); - }, - ), - // 苹果导航 - 仅在 iOS 平台显示 - if (Platform.isIOS) - ListTile( - leading: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.map, color: Colors.orange), - ), - title: Text('appleMap'.tr), - subtitle: Text('useAppleNavigation'.tr), - onTap: () { - Get.back(); - openAppleMap(destination, mode); - }, - ), - ListTile( - leading: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.navigation, color: Colors.grey), - ), - title: Text('builtInNavigation'.tr), - subtitle: Text('useBuiltInNavigation'.tr), - onTap: () { - Get.back(); - openInternalGaodeMap( - start, destination, endlatitude, endlongitude, mode); - }, - ), - // 底部安全区域 - SizedBox(height: MediaQuery.of(Get.context!).padding.bottom + 20), - ], - ), - ), - isScrollControlled: true, - isDismissible: true, // 点击外部区域可关闭 - enableDrag: true, // 支持拖拽关闭 - ); - } - - // 使用默认配置导航 - void _navigateWithDefaultMode(String start, String destination, - String endlatitude, String endlongitude, String mode) { - final navigationMode = - AppConfig.env('APP_NAVIGATION_MODE') ?? 'external_auto'; - if (navigationMode == 'internal_gaode') { - _navigationManager.open(start, "$endlongitude,$endlatitude", mode); - } else if (navigationMode == 'external_baidu') { - openBaiduMap(destination, endlatitude, endlongitude, mode); - } else if (navigationMode == 'external_gaode') { - openGaodeMap(destination, endlatitude, endlongitude, mode); - } else if (navigationMode == 'external_google') { - openGoogleMap(destination, mode); - } else { - // 自动选择导航应用,优先级:百度 > 高德 > 谷歌 - _autoSelectNavigation( - start, destination, endlatitude, endlongitude, mode); - } - } - - // 自动选择导航应用 - Future _autoSelectNavigation(String start, String destination, - String endlatitude, String endlongitude, String mode) async { - try { - // 1. 优先尝试百度地图 - final baiduUri = Uri( - scheme: 'baidumap', - host: 'map', - path: 'direction', - queryParameters: { - 'destination': destination, - 'coord_type': 'gcj02', - 'mode': mode == 'driving' ? 'driving' : 'transit', - 'src': 'deepvoice', - }, - ); - - if (await canLaunchUrl(baiduUri)) { - print('自动选择:使用百度地图'); - openBaiduMap(destination, endlatitude, endlongitude, mode); - return; - } - - // 2. 其次尝试高德地图 - final gaodeUri = Uri( - scheme: 'amapuri', - host: 'navi', - queryParameters: { - 'to': destination, - 'mode': mode.isNotEmpty ? mode : 'driving', - 'callnative': '1', - }, - ); - - if (await canLaunchUrl(gaodeUri)) { - print('自动选择:使用高德地图'); - openGaodeMap(destination, endlatitude, endlongitude, mode); - return; - } - - // 3. 然后尝试谷歌地图 - final googleUri = Uri( - scheme: 'comgooglemaps', - queryParameters: { - 'daddr': destination, - 'directionsmode': mode == 'driving' ? 'driving' : 'transit', - }, - ); - - if (await canLaunchUrl(googleUri)) { - print('自动选择:使用谷歌地图'); - openGoogleMap(destination, mode); - return; - } - - // 4. 在 iOS 平台尝试苹果地图 - if (Platform.isIOS) { - final appleUri = Uri( - scheme: 'maps', - queryParameters: { - 'daddr': destination, - 'dirflg': mode == 'driving' ? 'd' : 'r', - }, - ); - - if (await canLaunchUrl(appleUri)) { - print('自动选择:使用苹果地图'); - openAppleMap(destination, mode); - return; - } - } - // 5. 最后尝试内置高德导航 - _navigationManager.open(start, "$endlongitude,$endlatitude", mode); - // 5. 如果都没有安装,提示用户 - // print('未找到可用的导航应用'); - // final availableApps = - // Platform.isIOS ? '百度地图、高德地图、谷歌地图或苹果地图' : '百度地图、高德地图或谷歌地图'; - // Get.snackbar('提示', '未找到可用的导航应用,请安装$availableApps'); - } catch (e) { - print('自动选择导航失败: $e'); - Get.snackbar('error'.tr, 'navigationStartFailed'.tr); - } - } - - // 打开图片查看器 - void openImageViewer(BuildContext context, String imagePath) { - // 构建完整的图片路径 - String fullImagePath; - if (imagePath.startsWith('/')) { - // 如果是相对路径(以/开头),需要拼接AppDir.value - fullImagePath = '${AppDir.value}$imagePath'; - } else { - // 如果已经是完整路径,直接使用 - fullImagePath = imagePath; - } - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => Scaffold( - // appBar: AppBar( - // backgroundColor: Colors.black, - // leading: IconButton( - // icon: Icon(Icons.arrow_back_ios, color: Colors.white), - // onPressed: () => Navigator.of(context).pop(), - // ), - // iconTheme: IconThemeData(color: Colors.white), - // //title: Text('图片预览', style: TextStyle(color: Colors.white)), - // ), - body: Container( - color: Colors.black, - child: GestureDetector( - onTap: () => Navigator.of(context).pop(), // 点击退出 - - child: PhotoView( - imageProvider: FileImage(File(fullImagePath)), - minScale: PhotoViewComputedScale.contained, - maxScale: PhotoViewComputedScale.covered * 2, - backgroundDecoration: BoxDecoration(color: Colors.black), - // errorBuilder: (context, error, stackTrace) { - // return const Center( - // child: Column( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // Icon(Icons.error, color: Colors.white, size: 64), - // SizedBox(height: 16), - // Text('图片加载失败', style: TextStyle(color: Colors.white)), - // ], - // ), - // ); - // }, - ), - ), - ), - ), - ), - ); - } - - /// 播放音乐列表 - Future playPlaylist(List> playlist) async { - try { - if (playlist.isEmpty) { - Logger.e(TAG, '播放列表为空,无法播放'); - return; - } - - Logger.d(TAG, '开始播放音乐列表,共${playlist.length}首歌曲'); - - // 保存当前播放列表 - currentPlaylist.assignAll(playlist); - currentPlaylistIndex.value = 0; - - // 设置加载状态 - isLoadingMusic.value = true; - loadingMusicId.value = playlist[0]['id']; - - // 重置播放状态 - isPlaying.value = true; - currentPosition.value = 0; - duration.value = 0; - progress.value = 0.0; - - // 格式化歌曲列表数据 - final formattedPlaylist = playlist.map((song) { - return { - 'id': song['id'], - 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', - 'artist': - song['artist']?.toString() ?? song['sgener']?.toString() ?? '', - 'coverUrl': - song['coverUrl']?.toString() ?? song['image']?.toString() ?? '', - }; - }).toList(); - - // 调用音乐管理器播放列表 - await _musiceManager.playPlaylist(formattedPlaylist); - - // 更新当前播放歌曲ID - if (formattedPlaylist.isNotEmpty) { - currentId.value = formattedPlaylist[0]['id'] ?? 0; - } - - Logger.d(TAG, '音乐列表播放启动成功'); - } catch (e) { - Logger.e(TAG, '播放音乐列表失败: $e'); - isPlaying.value = false; - currentId.value = 0; - - // 显示错误提示 - Get.snackbar( - 'playFailed'.tr, - 'cannotPlayMusicListRetryLater'.tr, - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red[400], - colorText: Colors.white, - duration: const Duration(seconds: 2), - ); - } finally { - // 清除加载状态 - isLoadingMusic.value = false; - loadingMusicId.value = 0; - } - } - - /// 播放上一曲 - Future playPrevious() async { - if (currentPlaylist.isEmpty) { - Logger.w(TAG, '当前播放列表为空,无法播放上一曲'); - return; - } - - // 计算上一曲的索引 - int previousIndex = currentPlaylistIndex.value - 1; - if (previousIndex < 0) { - previousIndex = currentPlaylist.length - 1; // 循环到最后一首 - } - - currentPlaylistIndex.value = previousIndex; - final previousSong = currentPlaylist[previousIndex]; - - Logger.d(TAG, '播放上一曲: ${previousSong['name'] ?? previousSong['title']}'); - await musicPlay(previousSong); - } - - /// 播放下一曲 - Future playNext() async { - if (currentPlaylist.isEmpty) { - Logger.w(TAG, '当前播放列表为空,无法播放下一曲'); - return; - } - - // 计算下一曲的索引 - int nextIndex = currentPlaylistIndex.value + 1; - if (nextIndex >= currentPlaylist.length) { - nextIndex = 0; // 循环到第一首 - } - - currentPlaylistIndex.value = nextIndex; - final nextSong = currentPlaylist[nextIndex]; - - Logger.d(TAG, '播放下一曲: ${nextSong['name'] ?? nextSong['title']}'); - await musicPlay(nextSong); - } - - /// 更新当前播放列表和索引(当单独播放某首歌曲时调用) - void updateCurrentPlaylistContext( - List> playlist, int songId) { - currentPlaylist.assignAll(playlist); - - // 查找当前歌曲在列表中的索引 - int index = -1; - for (int i = 0; i < playlist.length; i++) { - if (playlist[i]['id'] == songId) { - index = i; - break; - } - } - - if (index != -1) { - currentPlaylistIndex.value = index; - Logger.d(TAG, '更新播放列表上下文,当前歌曲索引: $index'); - } - } - - /// 应用生命周期变化回调 - /// 目的: - /// - 当应用进入后台、失去焦点或被系统分离(inactive/paused/detached)时, - /// 兜底结束按住说话(调用 endPushToTalk),避免因 onPointerUp 未触发而卡在“按下”状态。 - /// 参数: - /// - state: 当前应用生命周期状态 - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.inactive || - state == AppLifecycleState.paused || - state == AppLifecycleState.detached) { - if (isPushToTalkActive.value) { - // 使用 microtask 避免阻塞当前生命周期回调 - Future.microtask(() { - endPushToTalk(); - }); - } - } - } - - // ========== 媒体会话回调处理方法 ========== - - /// 处理来自系统通知栏的播放操作 - Future onMediaSessionPlay() async { - Logger.d(TAG, '收到媒体会话播放回调'); - if (!isPlaying.value) { - await musicResume(); - } - } - - /// 处理来自系统通知栏的暂停操作 - Future onMediaSessionPause() async { - Logger.d(TAG, '收到媒体会话暂停回调'); - if (isPlaying.value) { - await musicPause(); - } - } - - /// 处理来自系统通知栏的下一曲操作 - Future onMediaSessionNext() async { - Logger.d(TAG, '收到媒体会话下一曲回调'); - await playNext(); - } - - /// 处理来自系统通知栏的上一曲操作 - Future onMediaSessionPrevious() async { - Logger.d(TAG, '收到媒体会话上一曲回调'); - await playPrevious(); - } - - /// 处理来自系统通知栏的跳转操作 - Future onMediaSessionSeekTo(int positionMs) async { - Logger.d(TAG, '收到媒体会话跳转回调: ${positionMs}ms'); - await seekToPosition(positionMs); - } - - /// 处理来自系统通知栏的停止操作 - Future onMediaSessionStop() async { - Logger.d(TAG, '收到媒体会话停止回调'); - await musicPause(); - // 重置播放状态 - currentPosition.value = 0; - progress.value = 0.0; - _stopProgressTimer(); - } - - /// 处理来自系统进度条 - Future onMediaSessionProgress(int currentIdInternal, int newPosition, - int newDuration, double newProgress) async { - // print( - // "onMediaSessionProgress currentIdInternal :$currentIdInternal ,newPosition:$newPosition newDuration:$newDuration newProgress:$newProgress"); - await seekToProgress( - currentIdInternal, newPosition, newDuration, newProgress); - } - - /// 处理来自系统通知栏的停止操作 - Future playQQMusic( - List songs, SongModel song, int index) async { - try { - if (_qqmusicManager.isPlaying.value) { - if (_qqmusicManager.currentSong.value?.mid == song.mid) { - // 当前歌曲正在播放,暂停它 - await _qqmusicManager.pauseMusic(); - } else { - final songMids = songs.map((s) => s.mid).toList(); - // 其他歌曲正在播放,播放这首歌 - final result = await _qqmusicManager.playSongs( - songMids: songMids, - index: index, - songList: [song], - currentSongModel: song, - ); - if (result['success'] == true) { - // 播放成功后,如果歌曲不在当前列表中,保持列表显示状态不变 - // Get.snackbar('开始播放', song.name); - _qqmusicManager.syncCurrentPlayInfo(); - } else { - Get.snackbar( - 'playFailed'.tr, result['error'] ?? 'cannotPlaySong'.tr); - } - return; - } - } else { - if (_qqmusicManager.currentSong.value?.mid == song.mid) { - // 当前歌曲已暂停,恢复播放 - await _qqmusicManager.resumeMusic(); - } else { - final songMids = songs.map((s) => s.mid).toList(); - // 播放新歌曲 - final result = await _qqmusicManager.playSongs( - songMids: songMids, - index: index, - songList: [song], - currentSongModel: song, - ); - if (result['success'] == true) { - // 播放成功后,如果歌曲不在当前列表中,保持列表显示状态不变 - // Get.snackbar('开始播放', song.name); - _qqmusicManager.syncCurrentPlayInfo(); - } else { - Get.snackbar( - 'playFailed'.tr, result['error'] ?? 'cannotPlaySong'.tr); - } - } - } - } catch (e) { - // 静默处理错误,避免影响用户体验 - print('播放控制失败: $e'); - } - } - - /// 打开内置高德地图进行导航 - Future openInternalGaodeMap( - String start, - String destination, - String endlatitude, - String endlongitude, - String mode // 'driving', 'transit', 'walking', 'cycling' - ) async { - try { - _navigationManager.open(start, "$endlongitude,$endlatitude", mode); - return true; - } catch (e) { - print('打开内置高德地图失败: $e'); - Get.snackbar('error'.tr, 'openMapError'.tr); - return false; - } - } - - /// 打开Google地图进行导航 - Future openGoogleMap(String daddr, String mode) async { - try { - // 方案1: 尝试使用Google Maps应用的URL scheme - // final Uri googleMapsUri = Uri.parse(url); - - // 方案1: Google Maps应用 URL scheme - final Uri appUri = Uri.parse( - 'comgooglemaps://?saddr=CurrentLocation&daddr=$daddr&directionsmode=$mode'); - if (await canLaunchUrl(appUri)) { - await launchUrl(appUri, mode: LaunchMode.externalApplication); - return true; - } else { - final Uri webUrl = Uri.parse( - "https://www.google.com/maps/dir/?api=1&origin=Current+Location&destination=$daddr&travelmode=$mode"); - if (await canLaunchUrl(webUrl)) { - await launchUrl(webUrl); - return true; - } - return false; - } - } catch (e) { - print('打开地图失败: $e'); - return false; - } - } - - /// 打开高德地图进行导航 - Future openGaodeMap( - String destination, - String lat, - String lon, - String mode, // 'driving', 'transit', 'walking', 'cycling' - ) async { - try { - // 获取应用包名 - final packageInfo = await PackageInfo.fromPlatform(); - - // 构建src参数(带平台前缀) - final String src = Platform.isAndroid - ? packageInfo.packageName - : "ios.${packageInfo.packageName}"; - - // 导航模式映射 - String gaodeMode; - switch (mode) { - case 'transit': - gaodeMode = '1'; // 公交 - break; - case 'walking': - gaodeMode = '2'; // 步行 - break; - case 'cycling': - gaodeMode = '3'; // 骑行 - break; - case 'driving': - default: - gaodeMode = '0'; // 驾车 - break; - } - - // 根据平台选择不同的 scheme 和参数 - final Uri uri; - if (Platform.isAndroid) { - uri = Uri( - scheme: 'androidamap', - host: 'route', - path: 'plan', - queryParameters: { - 'dlat': lat, - 'dlon': lon, - 'dname': destination, - 'dev': '0', // 0: gcj02 - 't': gaodeMode, - 'sourceApplication': src, - }, - ); - } else { - // iOS - uri = Uri( - scheme: 'amapuri', - host: 'route', - path: 'plan', - queryParameters: { - 'dlat': lat, - 'dlon': lon, - 'dname': destination, - 'dev': '0', // 0: gcj02 - 't': gaodeMode, - 'sourceApplication': src, - }, - ); - } - - print('高德导航链接: ${uri.toString()}'); - - if (await canLaunchUrl(uri)) { - final bool launched = await launchUrl( - uri, - mode: LaunchMode.externalApplication, - ); - return launched; - } else { - print('高德地图未安装'); - Get.snackbar('tip'.tr, 'pleaseInstallAmapApp'.tr); - return false; - } - } catch (e) { - print('打开高德地图失败: $e'); - Get.snackbar('error'.tr, 'openMapError'.tr); - return false; - } - } - - /// 打开Baidu地图进行导航 - Future openBaiduMap( - String destination, - String lat, - String lon, - String mode, // 'driving', 'transit', 'walking', 'riding' - ) async { - try { - // 获取应用包名 - final packageInfo = await PackageInfo.fromPlatform(); - final String src = - '${Platform.isAndroid ? '' : 'ios.'}${packageInfo.packageName}'; - - // 百度地图导航模式映射 - String baiduMode; - switch (mode) { - case 'transit': - baiduMode = 'transit'; // 公交 - break; - case 'walking': - baiduMode = 'walking'; // 步行 - break; - case 'cycling': - baiduMode = 'riding'; // 骑行 - break; - case 'driving': - default: - baiduMode = 'driving'; // 驾车 - break; - } - - final Uri uri = Uri( - scheme: 'baidumap', - host: 'map', - path: 'direction', - queryParameters: { - 'destination': 'name:$destination|latlng:$lat,$lon', - 'coord_type': 'gcj02', - 'mode': baiduMode, - 'src': src, - }, - ); - - print('百度导航链接: ${uri.toString()}'); - - if (await canLaunchUrl(uri)) { - final bool launched = await launchUrl( - uri, - mode: LaunchMode.externalApplication, - ); - return launched; - } else { - print('百度地图未安装'); - Get.snackbar('tip'.tr, 'pleaseInstallBaiduMapApp'.tr); - return false; - } - } catch (e) { - print('打开百度地图失败: $e'); - Get.snackbar('error'.tr, 'openMapError'.tr); - return false; - } - } - - /// 打开苹果地图进行导航 - Future openAppleMap(String destination, String mode) async { - try { - // 构建苹果地图导航链接 - final Uri uri = Uri( - scheme: 'maps', - queryParameters: { - 'daddr': destination, // 目的地地址 - 'dirflg': mode == 'driving' ? 'd' : 'r', // d=驾车, r=公交 - }, - ); - - print('苹果地图导航链接: ${uri.toString()}'); - - // 尝试打开苹果地图应用 - if (await canLaunchUrl(uri)) { - final bool launched = await launchUrl( - uri, - mode: LaunchMode.externalApplication, - ); - if (!launched) { - print('苹果地图启动失败'); - Get.snackbar('tip'.tr, 'appleMapStartFailed'.tr); - } - } else { - print('苹果地图不可用'); - Get.snackbar('tip'.tr, 'appleMapUnavailable'.tr); - } - } catch (e) { - print('打开苹果地图失败: $e'); - Get.snackbar('error'.tr, 'openAppleMapError'.tr); - } - } - - Future updateToolCallsExpanded( - String sessionid, - bool isExpanded, - ) async { - final message = messages.firstWhere( - (msg) => msg.sessionid == sessionid && msg.isUser == false, - ); - message.isToolCallsExpanded = isExpanded; - messages.refresh(); - } - - String getDeviceName() { - if (_bluetoothManager.currentDevice != null) { - return _bluetoothManager.currentDevice!.name; - } - return ""; - } -} diff --git a/apps/client/lib/modules/agent/controllers/message_action_controller.dart b/apps/client/lib/modules/agent/controllers/message_action_controller.dart deleted file mode 100644 index cd717ec1..00000000 --- a/apps/client/lib/modules/agent/controllers/message_action_controller.dart +++ /dev/null @@ -1,412 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:haptic_feedback/haptic_feedback.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../data/models/message_model.dart'; -import '../../../data/services/network/api.dart'; - -/// 消息操作控制器 -/// 负责处理消息长按、复制、举报等操作 -class MessageActionController extends GetxController { - // 当前举报的消息 - final reportingMessage = Rxn(); - - // 选择的举报原因 - List selectedReasons = []; - - // 举报描述 - final reportDescription = ''.obs; - - /// 处理消息长按事件 - void onLongPressMessage( - BuildContext context, - bool isDarkMode, - LongPressStartDetails details, - Message message, - ) { - // 触发触觉反馈 - Haptics.vibrate(HapticsType.success); - - // 计算弹出菜单位置 - RenderBox? renderBox = - Overlay.of(context).context.findRenderObject() as RenderBox; - final RelativeRect position = RelativeRect.fromRect( - Rect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx + 110, // 菜单显示位置X轴坐标 - details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标 - ), - Offset.zero & renderBox.size, - ); - - // 显示弹出菜单 - showMenu( - context: context, - position: position, - color: isDarkMode ? AppColors.cardBackground : Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.r), - ), - items: _buildMenuItems(context, isDarkMode, message), - ); - } - - /// 构建弹出菜单项 - List _buildMenuItems( - BuildContext context, - bool isDarkMode, - Message message, - ) { - final List items = [ - // 复制选项 - _buildPopupMenuItem( - Icon( - Icons.copy, - size: 20.w, - color: isDarkMode ? Colors.white : Colors.black87, - ), - 'copy'.tr, // 复制 - isDarkMode ? Colors.white : Colors.black87, - () => _copyMessage(message), - ), - ]; - - // 如果不是用户消息,添加举报选项 - if (!message.isUser) { - items.addAll([ - // 分隔线 - PopupMenuItem( - height: 1, - enabled: false, - child: Divider( - height: 1, - thickness: 0.8, - indent: 0, - endIndent: 0, - color: isDarkMode ? Colors.grey[700] : Colors.grey[300], - ), - ), - // 举报选项 - _buildPopupMenuItem( - Icon( - Icons.report, - size: 20.w, - color: isDarkMode ? Colors.red[700] : Colors.red, - ), - 'report'.tr, //举报 - isDarkMode ? Colors.red[700]! : Colors.red, - () => _showReportForm(context, isDarkMode, message), - ), - ]); - } - - return items; - } - - /// 构建弹出菜单项 - PopupMenuEntry _buildPopupMenuItem( - Icon icon, - String title, - Color color, - Function() onTap, - ) { - return PopupMenuItem( - onTap: onTap, - child: SizedBox( - width: 140.w, - child: Row( - children: [ - icon, - 8.horizontalSpace, - Text( - title, - style: TextStyle( - color: color, - fontSize: 14.sp, - ), - ), - ], - ), - ), - ); - } - - /// 复制消息内容 - void _copyMessage(Message message) { - Clipboard.setData(ClipboardData(text: message.text)).then((_) { - Get.snackbar( - 'success'.tr, - 'copySuccess'.tr, // 复制成功 - backgroundColor: Colors.blue.withOpacity(0.8), - colorText: Colors.white, - ); - }); - } - - /// 显示举报表单 - void _showReportForm(BuildContext context, bool isDarkMode, Message message) { - // 初始化举报状态 - initReport(message); - - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), - ), - builder: (BuildContext context) { - return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: _ReportForm(isDarkMode: isDarkMode), - ); - }, - ); - } - - /// 初始化举报状态 - void initReport(Message message) { - reportingMessage.value = message; - selectedReasons.clear(); - reportDescription.value = ''; - } - - /// 切换举报原因选择状态 - void toggleReason(String reason) { - if (selectedReasons.contains(reason)) { - selectedReasons.remove(reason); - } else { - selectedReasons.add(reason); - } - update(); - } - - /// 更新举报描述 - void updateDescription(String text) { - reportDescription.value = text; - } - - /// 提交举报 - Future submitReport() async { - if (reportingMessage.value == null) return; - - // 构建举报数据 - final reportData = { - 'describe': reportDescription.value, - 'message': [], - 'reportContext': reportingMessage.value!.text, - 'reporttype': selectedReasons.toString(), - }; - - try { - // 调用API提交举报 - var response = await Api.reportAiContent(reportData); - - if (response != null) { - print('提交举报响应: $response'); - } - - // 提交成功后重置状态 - selectedReasons.clear(); - reportDescription.value = ''; - reportingMessage.value = null; - - // 关闭举报表单 - Navigator.of(Get.context!).pop(); - - // 显示成功通知 - Get.snackbar( - 'submitSuccess'.tr, // 提交成功 - 'reportSubmitted'.tr, // 举报已提交,我们会尽快处理 - backgroundColor: Colors.green.withOpacity(0.8), - colorText: Colors.white, - duration: const Duration(seconds: 2), - ); - } catch (e) { - // 显示错误通知 - Get.snackbar( - 'submitFailed'.tr, // 提交失败 - 'networkError'.tr, // 网络错误,请稍后重试 - backgroundColor: Colors.red.withOpacity(0.8), - colorText: Colors.white, - duration: const Duration(seconds: 2), - ); - } - } -} - -/// 举报表单组件 -class _ReportForm extends StatelessWidget { - final bool isDarkMode; - final MessageActionController controller = - Get.find(); - - _ReportForm({required this.isDarkMode}); - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.all(16.w), - child: SingleChildScrollView( - child: GetBuilder( - builder: (_) => Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 标题栏 - Stack( - alignment: Alignment.center, - children: [ - Center( - child: Text( - 'report'.tr, // 举报 - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - Positioned( - right: 0, - child: IconButton( - icon: Icon( - Icons.close, - color: isDarkMode ? Colors.grey[400] : Colors.black54, - ), - onPressed: () => Navigator.pop(context), - ), - ), - ], - ), - - // 分隔线 - Container( - padding: EdgeInsets.symmetric(horizontal: 12.w), - margin: EdgeInsets.only(bottom: 10.w), - child: Divider( - height: 1, - color: Colors.grey[300], - ), - ), - - // 举报原因选择 - GetBuilder( - builder: (controller) { - return Wrap( - spacing: 8.w, - runSpacing: 8.h, - children: [ - _buildReportChip( - 'harmfulUnsafe'.tr, isDarkMode, controller), // 有害/不安全 - _buildReportChip('pornographicVulgar'.tr, isDarkMode, - controller), // 色情低俗 - _buildReportChip('misleadingAttack'.tr, isDarkMode, - controller), // 误导攻击 - _buildReportChip( - 'illegalCriminal'.tr, isDarkMode, controller), // 违法犯罪 - _buildReportChip( - 'privacyRelated'.tr, isDarkMode, controller), // 隐私相关 - _buildReportChip('copyrightViolation'.tr, isDarkMode, - controller), // 侵犯我的著作权、名誉权等 - _buildReportChip( - 'other'.tr, isDarkMode, controller), // 其他 - ], - ); - }, - ), - - SizedBox(height: 16.h), - - // 描述输入框 - Container( - padding: EdgeInsets.all(12.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.grey[200], - borderRadius: BorderRadius.circular(8.r), - ), - child: TextField( - maxLines: 3, - onChanged: controller.updateDescription, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - ), - decoration: InputDecoration( - hintText: - 'reportDescriptionHint'.tr, // 我们想知道你举报的原因,你可以描述你遇到的问题。 - hintStyle: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - fontSize: 14.sp, - ), - border: InputBorder.none, - ), - ), - ), - - SizedBox(height: 14.h), - - // 提交按钮 - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(vertical: 12.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - child: Text('submit'.tr), // 提交 - onPressed: () => controller.submitReport(), - ), - - SizedBox(height: 8.h), - ], - ), - ), - ), - ); - } - - /// 构建举报原因芯片 - Widget _buildReportChip( - String label, - bool isDarkMode, - MessageActionController controller, - ) { - final isSelected = controller.selectedReasons.contains(label); - - return GestureDetector( - onTap: () => controller.toggleReason(label), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), - decoration: BoxDecoration( - color: isSelected - ? Colors.red - : (isDarkMode ? Colors.grey[800] : Colors.grey[200]), - borderRadius: BorderRadius.circular(16.r), - border: Border.all( - color: isSelected - ? Colors.red[700]! - : (isDarkMode ? Colors.grey[700]! : Colors.grey[300]!), - width: isSelected ? 1.0 : 0.5, - ), - ), - child: Text( - label, - style: TextStyle( - color: isSelected - ? Colors.white - : (isDarkMode ? Colors.white : Colors.black87), - fontSize: 14.sp, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - ), - ), - ), - ); - } -} diff --git a/apps/client/lib/modules/agent/views/agent_view.dart b/apps/client/lib/modules/agent/views/agent_view.dart deleted file mode 100644 index 82016a48..00000000 --- a/apps/client/lib/modules/agent/views/agent_view.dart +++ /dev/null @@ -1,915 +0,0 @@ -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:flutter_intro/flutter_intro.dart'; -import 'package:get/get.dart'; -import '../../../core/widgets/intro_overlay_builder.dart'; -import '../../../data/models/channel_app_model.dart'; -import '../../../data/services/version_update_service.dart'; -import '../controllers/agent_controller.dart'; -import '../controllers/message_action_controller.dart'; -import 'message_bubble.dart'; -import '../widgets/sound_bar_widget.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class AgentView extends StatefulWidget { - const AgentView({Key? key}) : super(key: key); - - @override - State createState() => _AgentViewState(); -} - -class _AgentViewState extends State with WidgetsBindingObserver { - late AgentController controller; - late MessageActionController messageActionController; - double _keyboardHeight = 0; - - void _showTtsSpeedDialog() { - final primaryColor = Theme.of(context).primaryColor; - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - final RxInt tempRate = controller.ttsRatePercent.value.obs; - - Get.dialog( - AlertDialog( - backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.w), - ), - title: Text( - 'ttsSpeed'.tr, // 语速 - style: TextStyle(fontSize: 16.sp), - ), - content: Obx(() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - 'ttsSpeedDefaultHint'.tr, // 默认 +25% - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - ), - ), - SizedBox(height: 12.h), - Text( - '+${tempRate.value}%', - style: TextStyle( - fontSize: 20.sp, - fontWeight: FontWeight.w600, - color: primaryColor, - ), - ), - SizedBox(height: 8.h), - Slider( - value: tempRate.value.toDouble(), - min: 0, - max: 100, - divisions: 20, - label: '+${tempRate.value}%', - activeColor: primaryColor, - onChanged: (value) { - tempRate.value = value.round(); - }, - ), - ], - ); - }), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: Text('cancel'.tr), - ), - TextButton( - onPressed: () async { - Get.back(); - await controller.updateTtsRate(tempRate.value); - }, - child: Text('confirm'.tr), - ), - ], - ), - barrierDismissible: true, - ); - } - - @override - void initState() { - super.initState(); - controller = Get.find(); - messageActionController = Get.find(); - WidgetsBinding.instance.addObserver(this); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - @override - void didChangeMetrics() { - super.didChangeMetrics(); - final bottomInset = View.of(context).viewInsets.bottom; - final newKeyboardHeight = bottomInset / View.of(context).devicePixelRatio; - - if (_keyboardHeight != newKeyboardHeight) { - _keyboardHeight = newKeyboardHeight; - // 更新键盘可见状态 - controller.updateKeyboardVisibility(_keyboardHeight > 0); - } - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final primaryColor = theme.primaryColor; - final backgroundColor = theme.scaffoldBackgroundColor; - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - final isOfficialChannel = Get.isRegistered() && - VersionUpdateService.to.cachedChannel == UserChannel.OFFICIAL; - - return Scaffold( - appBar: AppBar( - elevation: 1, - backgroundColor: backgroundColor, - leading: IconButton( - icon: Icon(Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black87, size: 20.sp), - onPressed: () => Get.back(), - ), - titleSpacing: 0, - title: Row( - children: [ - Container( - width: 40.w, - height: 40.w, - decoration: BoxDecoration( - color: primaryColor, - borderRadius: BorderRadius.circular(20.w), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 3.w, - offset: const Offset(0, 1), - ), - ], - ), - child: Icon( - Icons.support_agent, - color: Colors.white, - size: 22.sp, - ), - ), - SizedBox(width: 12.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - // 机器人名字(品牌名,不做多语言翻译) - 'Emai', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - Row( - children: [ - Container( - width: 6.w, - height: 6.w, - decoration: const BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - ), - ), - SizedBox(width: 4.w), - Text( - 'online'.tr, - style: TextStyle( - fontSize: 12.sp, - color: Colors.green, - fontWeight: FontWeight.w500, - ), - ), - // Obx(() { - // final vipexptime = controller.vipExpiryTime.value; - // DateTime? expiry; - // if (vipexptime > 0) { - // expiry = vipexptime > 1000000000000 - // ? DateTime.fromMillisecondsSinceEpoch(vipexptime) - // : DateTime.fromMillisecondsSinceEpoch( - // vipexptime * 1000); - // } - // final text = expiry != null - // ? '${'expiryLabel'.tr}${DateFormat('yyyy-MM-dd').format(expiry)}' - // : 'notActivated'.tr; - // return Text( - // text, - // style: TextStyle( - // fontSize: 12.sp, - // color: Colors.green, - // fontWeight: FontWeight.w500, - // ), - // ); - // }), - ], - ), - ], - ), - ], - ), - actions: [ - // TTS开关 - Obx(() => IconButton( - icon: Icon( - controller.isTtsEnabled.value - ? Icons.volume_up - : Icons.volume_off, - size: 24.sp, - color: controller.isTtsEnabled.value - ? primaryColor - : isDarkMode - ? Colors.grey[400] - : Colors.grey, - ), - onPressed: () => controller.toggleTts(), - )), - - // 更多选项 - Container( - width: 34.w, - height: 34.w, - margin: EdgeInsets.only(right: 8.w), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.1) - : Colors.grey[100], - shape: BoxShape.circle, - boxShadow: [ - if (!isDarkMode) - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 2.w, - spreadRadius: 0.5.w, - ), - ], - ), - child: PopupMenuButton( - icon: Icon(Icons.more_vert, - color: isDarkMode ? Colors.white : Colors.black87, - size: 20.sp), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.w), - ), - color: isDarkMode ? Colors.grey[800] : Colors.white, - itemBuilder: (context) => [ - if (isOfficialChannel) - PopupMenuItem( - child: Obx(() => Row( - children: [ - Icon( - controller.isShowToolInterruptEnabled.value - ? Icons.build_circle - : Icons.build_circle_outlined, - color: controller.isShowToolInterruptEnabled.value - ? primaryColor - : (isDarkMode - ? Colors.grey[400] - : Colors.grey), - size: 20.sp, - ), - SizedBox(width: 8.w), - Text( - "显示工具", - style: TextStyle(fontSize: 14.sp), - ), - const Spacer(), - Switch( - value: - controller.isShowToolInterruptEnabled.value, - onChanged: (value) => - controller.toggleShowAIToolInterrupt(), - activeColor: primaryColor, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - ), - ], - )), - onTap: () => controller.toggleTtsInterrupt(), - ), - - // 播报打断开关选项 - PopupMenuItem( - child: Obx(() => Row( - children: [ - Icon( - controller.isTtsInterruptEnabled.value - ? Icons.voice_over_off - : Icons.voice_over_off_outlined, - color: controller.isTtsInterruptEnabled.value - ? primaryColor - : (isDarkMode ? Colors.grey[400] : Colors.grey), - size: 20.sp, - ), - SizedBox(width: 8.w), - Text( - //'ttsInterruptToggle'.tr, - "voice_interrupt".tr, // 语音打断 - style: TextStyle(fontSize: 14.sp), - ), - const Spacer(), - Switch( - value: controller.isTtsInterruptEnabled.value, - onChanged: (value) => - controller.toggleTtsInterrupt(), - activeColor: primaryColor, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - ), - ], - )), - onTap: () => controller.toggleTtsInterrupt(), - ), - - PopupMenuItem( - child: Row( - children: [ - Icon(Icons.speed, color: primaryColor, size: 20.sp), - SizedBox(width: 8.w), - Text('ttsSpeed'.tr, // 语速 - style: TextStyle(fontSize: 14.sp)), - ], - ), - onTap: () => Future.delayed( - const Duration(milliseconds: 100), - () => _showTtsSpeedDialog(), - ), - ), - - // 清除聊天记录选项 - PopupMenuItem( - child: Row( - children: [ - Icon(Icons.delete_outline, - color: primaryColor, size: 20.sp), - SizedBox(width: 8.w), - Text('clearChatHistory'.tr, - style: TextStyle(fontSize: 14.sp)), - ], - ), - onTap: () => Future.delayed( - const Duration(milliseconds: 100), - () => controller.clearChat(), - ), - ), - ], - ), - ), - ], - ), - body: SafeArea( - top: false, - child: Stack( - children: [ - IntroStepBuilder( - group: 'ai', - order: 3, - getOverlayPosition: IntroOverlayBuilder.overlayPosition, - overlayBuilder: (params) { - return Obx( - () => IntroOverlayBuilder.homeBuilder( - params, - false, - title: controller.newAIReplyCompleted.value - ? 'aiReplyCompleted'.tr - : 'aiThinking'.tr, - onNext: controller.newAIReplyCompleted.value ? () {} : null, - ), - ); - }, - builder: (BuildContext context, GlobalKey key) => SizedBox( - key: key, - ), - ), - // 主要聊天界面(占满整个 body,底部按钮浮在其上) - GestureDetector( - onTap: () { - // 点击聊天区域时,收起键盘并切换工具显示状态 - FocusScope.of(context).unfocus(); - if (controller.isPlusOptionsExpanded.value) { - controller.collapsePlusOptions(); - } - }, - child: Obx(() { - return Align( - alignment: Alignment.topCenter, - child: ListView.builder( - controller: controller.scrollController, - shrinkWrap: true, - reverse: true, - padding: EdgeInsets.only( - top: 16.h, - // 预留浮动通话/挂断按钮的高度,避免最后一条消息被遮挡 - bottom: 140.h, - left: 16.w, - right: 16.w, - ), - itemCount: controller.messages.length + - (controller.hasMoreMessages.value ? 1 : 0), - itemBuilder: (context, index) { - // 如果是最后一项且有更多消息,显示加载指示器 - if (index == controller.messages.length && - controller.hasMoreMessages.value) { - return _buildLoadMoreIndicator(); - } - - final message = controller - .messages[controller.messages.length - 1 - index]; - return index == 0 - ? IntroStepBuilder( - group: 'ai', - order: 4, - getOverlayPosition: - IntroOverlayBuilder.overlayPosition, - overlayBuilder: (params) { - return IntroOverlayBuilder.aiBuilder( - params, - '${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['endKey']}' - .tr, - onFinish: () async { - if (controller.noviceGuideService.aiStep < - controller.noviceGuideService - .aiStepsList.length - - 1) { - await Future.delayed( - const Duration(milliseconds: 300)); - controller.noviceGuideService.aiStep++; - Intro.of(Get.context!) - .start(group: 'ai', reset: true); - } - }, - onTap: () async { - params.onFinish(); - if (controller.noviceGuideService.aiStep < - controller.noviceGuideService - .aiStepsList.length - - 1) { - await Future.delayed( - const Duration(milliseconds: 300)); - controller.noviceGuideService.aiStep++; - Intro.of(Get.context!) - .start(group: 'ai', reset: true); - } - }, - ); - }, - builder: (BuildContext context, GlobalKey key) => - Padding( - key: key, - padding: EdgeInsets.only(bottom: 8.h), - child: GestureDetector( - onLongPressStart: - (LongPressStartDetails details) { - messageActionController.onLongPressMessage( - context, isDarkMode, details, message); - }, - child: MessageBubble( - sessionid: message.sessionid, - isUser: message.isUser, - message: message.text, - timestamp: message.timestamp, - isRecognizing: message.isRecognizing, - hasImage: message.hasImage, - imagePath: message.imagePath, - meta: message.meta, - functionCalls: message.functionCalls, - isToolCallsExpanded: - message.isToolCallsExpanded, - ), - ), - ), - ) - : Padding( - padding: EdgeInsets.only(bottom: 8.h), - child: GestureDetector( - onLongPressStart: - (LongPressStartDetails details) { - messageActionController.onLongPressMessage( - context, isDarkMode, details, message); - }, - child: MessageBubble( - sessionid: message.sessionid, - isUser: message.isUser, - message: message.text, - timestamp: message.timestamp, - isRecognizing: message.isRecognizing, - hasImage: message.hasImage, - imagePath: message.imagePath, - meta: message.meta, - functionCalls: message.functionCalls, - isToolCallsExpanded: - message.isToolCallsExpanded, - ), - ), - ); - }, - ), - ); - }), - ), - // 底部输入栏:仿微信「键盘/语音」双模式切换,只保留打字与语音输入 - Align( - alignment: Alignment.bottomCenter, - child: Obx(() { - if (controller.isDeviceCallMode.value) { - return const SizedBox.shrink(); - } - return _buildInputBar(context, isDarkMode); - }), - ), - - // 打电话模式覆盖层 - Obx(() { - if (controller.isPhoneCallMode.value) { - return _buildPhoneCallOverlay(context, isDarkMode); - } - return const SizedBox.shrink(); - }), - - // 设备通话模式覆盖层 - Obx(() { - if (controller.isDeviceCallMode.value) { - return _buildDeviceCallOverlay(context, isDarkMode); - } - return const SizedBox.shrink(); - }), - ], - ), - ), - ); - } - - // 底部输入栏:仿微信的「键盘 / 语音」双模式。 - // 左侧圆形按钮切换模式:文本模式显示麦克风图标(点它切到语音), - // 语音模式显示键盘图标(点它切回打字)。中间随模式变成输入框或「按住 说话」。 - // 只做打字 + 语音输入两件事,不含表情/图片/更多面板。 - Widget _buildInputBar(BuildContext context, bool isDarkMode) { - final Color barBg = isDarkMode ? const Color(0xFF1C1C1E) : Colors.white; - final Color fieldBg = - isDarkMode ? const Color(0xFF2C2C2E) : const Color(0xFFF2F3F5); - final Color iconColor = isDarkMode ? Colors.white70 : Colors.black54; - final Color textColor = isDarkMode ? Colors.white : Colors.black87; - - return Container( - decoration: BoxDecoration( - color: barBg, - border: Border( - top: BorderSide( - color: - isDarkMode ? const Color(0xFF2C2C2E) : const Color(0xFFE5E5E5), - width: 0.5, - ), - ), - ), - // 外层 body 已经包了 SafeArea(top:false),这里不再重复处理底部安全区 - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // 模式切换按钮 - GestureDetector( - onTap: controller.toggleInputMode, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: EdgeInsets.only(right: 8.w, bottom: 4.h), - child: Icon( - controller.isTextInputMode.value - ? Icons.keyboard_voice_outlined - : Icons.keyboard_outlined, - size: 28.sp, - color: iconColor, - ), - ), - ), - // 中间:输入框 / 按住说话 - Expanded( - child: controller.isTextInputMode.value - ? _buildTextField(fieldBg, textColor) - : _buildHoldToTalkButton(fieldBg, textColor), - ), - // 发送按钮:仅文本模式且有内容时出现 - if (controller.isTextInputMode.value && - controller.hasInputText.value) - Padding( - padding: EdgeInsets.only(left: 8.w, bottom: 2.h), - child: GestureDetector( - onTap: controller.sendMessage, - behavior: HitTestBehavior.opaque, - child: Container( - width: 40.h, - height: 40.h, - decoration: const BoxDecoration( - color: Color(0xFF07C160), // 微信绿 - shape: BoxShape.circle, - ), - child: Icon( - Icons.arrow_upward_rounded, - size: 22.sp, - color: Colors.white, - ), - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildTextField(Color fieldBg, Color textColor) { - return Container( - constraints: BoxConstraints(minHeight: 40.h, maxHeight: 120.h), - decoration: BoxDecoration( - color: fieldBg, - borderRadius: BorderRadius.circular(6.r), - ), - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), - child: TextField( - controller: controller.textController, - style: TextStyle(fontSize: 15.sp, color: textColor), - minLines: 1, - maxLines: 4, - textInputAction: TextInputAction.send, - onSubmitted: (_) => controller.sendMessage(), - decoration: const InputDecoration( - isDense: true, - border: InputBorder.none, - contentPadding: EdgeInsets.zero, - ), - ), - ); - } - - /// 按住说话:按下开始识别、松开结束。识别结果由 AgentService 事件回流成消息, - /// 与原来语音链路一致,这里只负责手势与视觉反馈。 - Widget _buildHoldToTalkButton(Color fieldBg, Color textColor) { - return GestureDetector( - onLongPressStart: (_) => controller.startVoiceInput(), - onLongPressEnd: (_) => controller.stopVoiceInput(), - onLongPressCancel: () => controller.stopVoiceInput(), - behavior: HitTestBehavior.opaque, - child: Obx(() { - final talking = controller.isListening.value; - return Container( - height: 40.h, - alignment: Alignment.center, - decoration: BoxDecoration( - color: talking ? const Color(0xFFD9D9D9) : fieldBg, - borderRadius: BorderRadius.circular(6.r), - ), - child: Text( - talking ? 'releaseToSend'.tr : 'holdToTalk'.tr, - style: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.w500, - color: talking ? Colors.black87 : textColor, - ), - ), - ); - }), - ); - } - - // 构建打电话模式覆盖层 - Widget _buildPhoneCallOverlay(BuildContext context, bool isDarkMode) { - return Container( - color: Colors.transparent, // 设置整个覆盖层为透明 - child: Column( - children: [ - // 中间空白区域 - 完全透明 - const Expanded( - child: SizedBox(), // 空的占位符,完全透明 - ), - - // 底部控制按钮 - 包含音频波纹 - Container( - color: isDarkMode ? Colors.black : Colors.white, - child: _buildPhoneCallControls(isDarkMode), - ), - ], - ), - ); - } - - // 构建打电话模式控制按钮 - Widget _buildPhoneCallControls(bool isDarkMode) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 24.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐 - children: [ - // 录音控制按钮(左侧) - 控制录音输入功能 - Obx(() => _buildPhoneControlButton( - icon: controller.isListening.value - ? Icons.mic // 录音中显示麦克风图标 - : Icons.mic_off, // 未录音显示麦克风关闭图标 - onTap: () => controller.togglePhoneCallRecording(), - isDarkMode: isDarkMode, - isActive: controller.isListening.value, - // backgroundColor: controller.isListening.value - // ? Colors.red // 录音中显示红色背景 - // : Theme.of(context).primaryColor, // 未录音显示主题色 - )), - - // 音频波纹组件(中间) - 仅显示动态波形效果,无其他UI元素 - Container( - width: 120.w, // 设置固定宽度 - height: 64.h, // 与按钮高度一致 - alignment: Alignment.center, // 确保波纹居中 - child: const AgentSoundBarsWidget(barCount: 12), // 适应宽度的波纹条数 - ), - - // 关闭打电话模式按钮(右侧) - 退出打电话模式 - _buildPhoneControlButton( - icon: Icons.close, - onTap: () => controller.endPhoneCall(), - isDarkMode: isDarkMode, - isActive: false, - //backgroundColor: Colors.red, - iconColor: Colors.redAccent, - ), - ], - ), - ); - } - - // 构建打电话控制按钮 - Widget _buildPhoneControlButton({ - required IconData icon, - required VoidCallback onTap, - required bool isDarkMode, - required bool isActive, - Color? backgroundColor, - Color? iconColor, - }) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 64.w, // 按钮尺寸 - height: 64.h, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: backgroundColor ?? - (isDarkMode ? Colors.grey[800] : Colors.grey[250]), - border: Border.all(color: Colors.blue, width: 2.w), - boxShadow: [ - BoxShadow( - color: (backgroundColor ?? Colors.grey).withValues(alpha: 0.3), - blurRadius: 8.w, - spreadRadius: 1.w, - offset: Offset(0, 2.h), - ), - ], - ), - child: Icon( - icon, - color: iconColor ?? (isDarkMode ? Colors.white : Colors.black), - size: 28.sp, // 图标尺寸 - ), - ), - ); - } - - // ─── 设备通话覆盖层(接听后仅一个挂断按钮,悬浮在聊天界面之上)───────────────────── - Widget _buildDeviceCallOverlay(BuildContext context, bool isDarkMode) { - return Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: EdgeInsets.only(bottom: 16.h), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onTap: () => controller.endDeviceCall(), - child: Container( - width: 68.w, - height: 68.w, - decoration: BoxDecoration( - color: const Color(0xFFEF4444), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: const Color(0xFFEF4444).withValues(alpha: 0.35), - blurRadius: 16.w, - offset: Offset(0, 6.h), - ), - ], - ), - child: Transform.rotate( - angle: 2.356, - child: Icon( - Icons.call_end, - color: Colors.white, - size: 30.sp, - ), - ), - ), - ), - SizedBox(height: 8.h), - Text( - '挂断', - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - ), - ), - ], - ), - ), - ); - } - - // 构建加载更多指示器 - Widget _buildLoadMoreIndicator() { - return Obx( - () { - if (controller.isLoadingMore.value) { - // 正在加载状态 - return Container( - padding: EdgeInsets.symmetric(vertical: 16.h), - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 16.w, - height: 16.h, - child: CircularProgressIndicator( - strokeWidth: 2.w, - ), - ), - SizedBox(width: 8.w), - Text( - 'loadMoreMessages'.tr, - style: TextStyle( - fontSize: 14.sp, - color: Colors.grey, - ), - ), - ], - ), - ), - ); - } else if (controller.isLoadingMoreError.value) { - // 加载失败状态 - return Container( - padding: EdgeInsets.symmetric(vertical: 16.h), - child: Center( - child: GestureDetector( - onTap: () => controller.loadMoreMessages(), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.refresh, - size: 16.sp, - color: Colors.grey, - ), - SizedBox(width: 8.w), - Text( - 'loadFailedClickRetry'.tr, - style: TextStyle( - fontSize: 14.sp, - color: Colors.grey, - ), - ), - ], - ), - ), - ), - ); - } else { - // 默认状态(有更多消息但未加载) - return Container( - padding: EdgeInsets.symmetric(vertical: 8.h), - child: Center( - child: Text( - 'swipeUpForMore'.tr, - style: TextStyle( - fontSize: 12.sp, - color: Colors.grey, - ), - ), - ), - ); - } - }, - ); - } -} diff --git a/apps/client/lib/modules/agent/views/google_navigation_card.dart b/apps/client/lib/modules/agent/views/google_navigation_card.dart deleted file mode 100644 index 177d6832..00000000 --- a/apps/client/lib/modules/agent/views/google_navigation_card.dart +++ /dev/null @@ -1,447 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import '../controllers/agent_controller.dart'; - -String formatDistance(String distanceStr) { - final distance = double.tryParse(distanceStr); - if (distance == null) return distanceStr; // 如果无法解析,则返回原始字符串 - - if (distance >= 1000) { - final km = distance / 1000; - return '${km.toStringAsFixed(1)} KM'; - } else { - return '${distance.toStringAsFixed(0)} M'; - } -} - -String formatDuration(String secondsStr) { - int totalSeconds = int.tryParse(secondsStr) ?? 0; - Duration duration = Duration(seconds: totalSeconds); - - int hours = duration.inHours; - int minutes = duration.inMinutes.remainder(60); - int seconds = duration.inSeconds.remainder(60); - - List parts = []; - if (hours > 0) parts.add('$hours${'hours'.tr}'); // 小时 - if (minutes > 0) parts.add('$minutes${'minutes'.tr}'); // 分钟 - //不需要秒 - //if (seconds > 0 || parts.isEmpty) parts.add('$seconds${'seconds'.tr}'); // 秒 - - return parts.join(''); -} - -/// 导航卡片组件 -/// -/// 各元素含义分析: -/// - [start]: 起点坐标 (经纬度格式) -/// - [end]: 终点坐标 (经纬度格式) -/// - [destination]: 目的地名称显示 -/// - [modes]: 导航方式列表,包含驾车、步行、公交等选项 -/// - [navigation]: 当前导航状态信息(可选) -/// - [height]: 卡片高度 -Widget googlenavigationCard( - BuildContext context, { - required String start, - required String end, - required String destination, - required List> modes, - Map? navigation, - double? height, // 改为可选参数,允许自适应高度 -}) { - int currentIndex = 0; - final controller = Get.find(); - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Container( - height: height?.h ?? 350.h, // 如果没有指定高度,使用350作为默认高度 - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16.r), - color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white, - boxShadow: [ - BoxShadow( - blurRadius: 12, - color: Colors.black.withValues(alpha: isDarkMode ? 0.4 : 0.08), - offset: const Offset(0, 2), - ), - ], - ), - child: StatefulBuilder( - builder: (context, setInnerState) { - return Column( - children: [ - // 顶部:路线预览图片区域 - Expanded( - flex: 4, // 增加图片区域的flex值,让图片占更多空间 - child: ClipRRect( - borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), - child: Stack( - children: [ - // 路线图片 - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: Image.network( - modes[currentIndex]["image"] as String, - key: ValueKey( - modes[currentIndex]["ntype"] as String), - height: double.infinity, - width: double.infinity, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - child: Icon( - Icons.map_outlined, - size: 48.sp, - color: isDarkMode - ? Colors.grey[600] - : Colors.grey[400], - ), - ); - }, - ), - ), - // 导航状态覆盖层 - if (navigation != null) - Positioned( - top: 12.h, - left: 12.w, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: - Theme.of(context).primaryColor.withOpacity(0.9), - borderRadius: BorderRadius.circular(12.r), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.navigation, - color: Colors.white, - size: 14.sp, - ), - SizedBox(width: 4.w), - Text( - 'navigating'.tr, // 导航中 - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - - // 中部:导航方式选择器 - Container( - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w), - child: Row( - children: List.generate(modes.length, (index) { - final mode = modes[index]; - final isSelected = index == currentIndex; - final isEnabled = mode['enable'] as bool; - - return Expanded( - child: GestureDetector( - onTap: isEnabled - ? () { - setInnerState(() => currentIndex = index); - } - : null, - child: Container( - margin: EdgeInsets.symmetric(horizontal: 4.w), - padding: EdgeInsets.symmetric( - vertical: 8.h, horizontal: 12.w), - decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).primaryColor.withOpacity(0.1) - : Colors.transparent, - borderRadius: BorderRadius.circular(8.r), - border: Border.all( - color: isSelected - ? Theme.of(context).primaryColor - : (isDarkMode - ? Colors.grey[700]! - : Colors.grey[300]!), - width: isSelected ? 2 : 1, - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - _getNavigationIcon(mode["ntype"] as String), - color: isEnabled - ? (isSelected - ? Theme.of(context).primaryColor - : (isDarkMode - ? Colors.white70 - : Colors.black87)) - : Colors.grey, - size: 20.sp, - ), - ], - ), - ), - ), - ); - }), - ), - ), - - // 底部:目的地信息和操作按钮 - Container( - padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 第一层:起点 → 目的地信息(独占整个宽度) - Row( - crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐 - children: [ - // 起点图标 - 统一图标容器宽度,确保对齐 - SizedBox( - width: 16.sp, // 固定图标容器宽度 - child: Icon( - Icons.my_location, - size: 15.sp, // 统一图标大小 - color: Theme.of(context).primaryColor, - ), - ), - SizedBox(width: 6.w), // 统一间距 - // 当前位置文本 - Text( - 'currentLocation'.tr, // 当前位置 - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white70 : Colors.black54, - height: 1.3, // 设置行高,提升多行文本可读性 - ), - softWrap: true, // 启用软换行 - overflow: TextOverflow.visible, // 确保文本完全显示 - ), - SizedBox(width: 8.w), - // 箭头图标 - Icon( - Icons.arrow_forward, - size: 15.sp, // 统一图标大小 - color: isDarkMode ? Colors.white54 : Colors.black45, - ), - SizedBox(width: 8.w), - // 目的地图标 - Icon( - Icons.location_on, - size: 15.sp, // 统一图标大小 - color: Colors.red, - ), - SizedBox(width: 6.w), // 统一间距 - // 目的地名称 - 使用Expanded确保自动换行 - Expanded( - child: Text( - destination, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black87, - height: 1.2, // 设置行高,提升多行文本可读性 - ), - softWrap: true, // 启用软换行 - overflow: TextOverflow.visible, // 确保文本完全显示 - ), - ), - ], - ), - - SizedBox(height: 4.h), // 第一层和第二层之间的间距 - - // 第二层:距离时间信息(左侧)+ 导航按钮(右侧) - Row( - crossAxisAlignment: CrossAxisAlignment.end, // 底部对齐 - children: [ - // 左侧:距离和时间信息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 距离信息 - Row( - crossAxisAlignment: - CrossAxisAlignment.center, // 确保垂直居中对齐 - children: [ - // 距离图标 - 统一图标容器宽度,确保与第一层对齐 - SizedBox( - width: 16.sp, // 与第一层图标容器宽度一致 - child: Icon( - Icons.straighten, - size: 15.sp, // 统一图标大小 - color: Theme.of(context).primaryColor, - ), - ), - SizedBox(width: 6.w), // 与第一层间距一致 - Flexible( - child: Text( - formatDistance( - modes[currentIndex]["distance"]), - style: TextStyle( - fontSize: 14.sp, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, - height: 1.3, // 设置行高,提升多行文本可读性 - ), - softWrap: true, // 启用软换行 - overflow: TextOverflow.visible, // 确保文本完全显示 - ), - ), - ], - ), - SizedBox(height: 4.h), - // 时间信息 - Row( - crossAxisAlignment: - CrossAxisAlignment.center, // 确保垂直居中对齐 - children: [ - // 时间图标 - 统一图标容器宽度,确保与第一层对齐 - SizedBox( - width: 16.sp, // 与第一层图标容器宽度一致 - child: Icon( - Icons.access_time, - size: 15.sp, // 统一图标大小 - color: Theme.of(context).primaryColor, - ), - ), - SizedBox(width: 6.w), // 与第一层间距一致 - Flexible( - child: Text( - '${'approximate'.tr}${formatDuration(modes[currentIndex]["duration"])}', // 约 - style: TextStyle( - fontSize: 14.sp, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, - height: 1.3, // 设置行高,提升多行文本可读性 - ), - softWrap: true, // 启用软换行 - overflow: TextOverflow.visible, // 确保文本完全显示 - ), - ), - ], - ), - // 导航状态信息 - if (navigation != null) ...[ - SizedBox(height: 6.h), - Container( - padding: EdgeInsets.symmetric( - horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: Theme.of(context) - .primaryColor - .withOpacity(0.1), - borderRadius: BorderRadius.circular(6.r), - ), - child: Text( - "${navigation["distanceText"]}${'enterRoad'.tr}${navigation["nextRoad"]}", // 进入 - style: TextStyle( - fontSize: 12.sp, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, - height: 1.3, // 设置行高,提升多行文本可读性 - ), - softWrap: true, // 启用软换行 - overflow: TextOverflow.visible, // 确保文本完全显示 - ), - ), - ], - ], - ), - ), - - SizedBox(width: 16.w), // 左右内容之间的间距 - - // 右侧:开始导航按钮 - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Theme.of(context).primaryColor, - Theme.of(context).primaryColor.withOpacity(0.8), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Theme.of(context) - .primaryColor - .withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(12.r), - onTap: () { - controller.openGoogleMap( - end, modes[currentIndex]["ntype"] as String); - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 12.h, - ), - child: Icon( - Icons.navigation, - color: Colors.white, - size: 20.sp, - ), - ), - ), - ), - ), - ], - ), - ], - ), - ), - ], - ); - }, - ), - ); -} - -/// 根据导航类型获取对应图标 -IconData _getNavigationIcon(String ntype) { - switch (ntype.toLowerCase()) { - case 'driving': - case '驾车': - return Icons.directions_car; - case 'walking': - case '步行': - return Icons.directions_walk; - case 'transit': - case 'tsransit': // 修正拼写错误 - case '公交': - return Icons.directions_transit; - case 'bicycling': // 修正为正确的英文值 - case 'cycling': - case '骑行': - return Icons.directions_bike; - default: - return Icons.navigation; - } -} diff --git a/apps/client/lib/modules/agent/views/message_bubble.dart b/apps/client/lib/modules/agent/views/message_bubble.dart deleted file mode 100644 index e23024f1..00000000 --- a/apps/client/lib/modules/agent/views/message_bubble.dart +++ /dev/null @@ -1,942 +0,0 @@ -import '/core/utils/logger.dart'; -import '/modules/agent/views/google_navigation_card.dart'; -import '/modules/agent/views/music_card.dart'; -import '/modules/agent/views/qqmusic_card.dart'; -import '/modules/agent/views/weather_baidu.dart'; -import '/modules/agent/views/weather_hefeng.dart'; - -import '../../../data/models/song_model.dart'; -import '../../../modules/agent/views/navigation_card.dart'; -import 'weather_card.dart'; -import 'travel_card.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; // 添加这个导入 -import 'package:get/get.dart'; -import 'package:intl/intl.dart'; -import 'dart:io'; -import 'package:flutter_markdown/flutter_markdown.dart'; - -import '../controllers/agent_controller.dart'; - -/// 消息气泡组件 -class MessageBubble extends GetView { - final String sessionid; - final bool isUser; - final String message; - final DateTime timestamp; - final bool isRecognizing; - final bool hasImage; - final String? imagePath; - final Map? meta; - final Map>? functionCalls; - final bool isToolCallsExpanded; - const MessageBubble({ - Key? key, - required this.sessionid, - required this.isUser, - required this.message, - required this.timestamp, - this.isRecognizing = false, - this.hasImage = false, - this.imagePath, - this.meta, - this.functionCalls, - this.isToolCallsExpanded = false, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: - isUser ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Column( - crossAxisAlignment: - isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, - children: [ - Stack( - children: [ - Container( - padding: (hasImage && message == '[图片]') - ? EdgeInsets.zero - : const EdgeInsets.all(12), - decoration: BoxDecoration( - color: _getBubbleColor(context), - borderRadius: BorderRadius.only( - topLeft: isUser - ? const Radius.circular(18) - : const Radius.circular(4), - topRight: isUser - ? const Radius.circular(4) - : const Radius.circular(18), - bottomLeft: const Radius.circular(18), - bottomRight: const Radius.circular(18), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Padding( - padding: (controller.isShowToolInterruptEnabled.value && - !isUser && - functionCalls != null && - functionCalls!.isNotEmpty) - ? const EdgeInsets.only(top: 30) - : EdgeInsets.zero, - child: _buildMessageContent(context), - ), - ), - //工具调用流程显示(仅在AI消息且有functionCalls时显示) - if (controller.isShowToolInterruptEnabled.value && - !isUser && - functionCalls != null && - functionCalls!.isNotEmpty) - _buildFunctionCallsWidget(context), - ], - ), - - const SizedBox(height: 4), - // 识别中的消息不显示时间 - if (!isRecognizing) - Row( - mainAxisAlignment: isUser - ? MainAxisAlignment.end - : MainAxisAlignment.spaceBetween, - children: [ - // 时间戳(左对齐或右对齐) - Text( - _formatTimestamp(timestamp), - style: TextStyle( - fontSize: 8, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[400] - : Colors.grey[500], - fontWeight: FontWeight.w400, - ), - ), - // AI标识(仅在AI消息时显示) - if (!isUser) - Padding( - padding: const EdgeInsets.only(left: 8), - child: Text( - "generatedByAi".tr, // 由AI生成 - style: TextStyle( - fontSize: 8, - color: Colors.blue[600], - fontWeight: FontWeight.bold, - fontStyle: FontStyle.italic, - ), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ); - } - - /// 获取气泡颜色 - Color _getBubbleColor(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - if (isUser) { - return isRecognizing - ? Color(0xFF3B82F6).withOpacity(0.8) // 识别中的用户消息使用半透明的蓝色 - : Color(0xFF3B82F6); // 使用更明亮的蓝色 - } else { - return isDarkMode - ? Color(0xFF2A2A2A) - : Color(0xFFF1F5F9); // 根据主题调整AI消息背景色 - } - } - - /// 构建消息内容 - Widget _buildMessageContent(BuildContext context) { - // 如果是识别中的消息,添加特殊样式 - if (isRecognizing) { - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - message, - style: const TextStyle( - color: Colors.white, - fontStyle: FontStyle.italic, - fontSize: 14, - height: 1.4, - ), - // 允许自动换行,不再强制单行 - softWrap: true, - maxLines: null, - ), - ), - const SizedBox(width: 8), - _buildTypingIndicator(), - ], - ); - } - - if (!isUser && - message.isEmpty && - (functionCalls == null || - !controller.isShowToolInterruptEnabled.value) && - meta == null) { - return _buildTypingIndicator(); - } - - // 如果只有工具调用而没有文本内容,显示一个占位符确保气泡有足够宽度 - if (!isUser && - message.isEmpty && - functionCalls != null && - controller.isShowToolInterruptEnabled.value && - meta == null) { - return Container( - constraints: const BoxConstraints( - minWidth: 200, // 确保最小宽度 - minHeight: 20, // 确保最小高度 - ), - child: const SizedBox.shrink(), // 透明占位符 - ); - } - - // 如果是图片消息,显示图片 - if (hasImage && imagePath != null) { - // 如果消息是默认的[图片]标记,只显示图片 - if (message == '[图片]') { - return Container( - padding: const EdgeInsets.all(8), - child: GestureDetector( - onTap: () { - // 点击图片时打开图片查看器 - Get.find() - .openImageViewer(context, imagePath ?? ''); - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.file( - File('${controller.AppDir.value}$imagePath'), - width: 200, - height: 200, - fit: BoxFit.cover, - ), - ), - ), - ); - } - - // 如果消息是自定义文本,显示文本+图片 - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - message, - style: TextStyle( - color: isUser ? Colors.white : Colors.black87, - fontSize: 14, - height: 1.4, - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(height: 8), - GestureDetector( - onTap: () { - // 点击图片时打开图片查看器 - Get.find() - .openImageViewer(context, imagePath ?? ''); - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.file( - File('${controller.AppDir.value}$imagePath'), - width: 200, - height: 200, - fit: BoxFit.cover, - ), - ), - ), - ], - ); - } - - // 判断消息内容,显示特殊卡片 - if (!isUser && meta != null) { - if (meta!.keys.contains('music_searchandplay_songs')) { - final music_search_songs = - meta!['music_searchandplay_songs'] as Map; - final data = music_search_songs['data']; - if (data != null && data is List) { - final rawList = data as List? ?? []; - final playlist = - rawList.map((e) => Map.from(e)).toList(); - //音乐播放列表 - return buildMusicContent(context, playlist); - } - } else if (meta!.keys.contains('qqmusic_search_songs')) { - final qqmusic_search_songs = - meta!['qqmusic_search_songs'] as Map; - final data = qqmusic_search_songs['data']; - if (data != null && data is List) { - //qq音乐 - final rawList = data as List? ?? []; - final songList = rawList - .map((item) { - try { - return SongModel.fromJson(item); - } catch (e) { - Logger.e('QQMusicPlaylist', '解析歌曲数据失败: $e, 数据: $item'); - return null; - } - }) - .where((song) => song != null) - .cast() - .toList(); - //音乐播放列表 - return buildQQMusicContent(context, songList); - } - } else if (meta!.keys.contains('hefeng_weather')) { - //高德的 - final hefeng_weather = meta!['hefeng_weather'] as Map; - final weather_city = hefeng_weather['city']; - final weather_day = hefeng_weather['day']; - final weather_data = hefeng_weather['data']; - if (weather_city != null) { - final rawList = weather_data as List? ?? []; - final daylist = - rawList.map((e) => Map.from(e)).toList(); - // final now = daylist.isNotEmpty ? daylist[0] : {}; - final now = daylist.firstWhere( - (day) => day['fxDate'] == weather_day, - orElse: () => daylist.isNotEmpty ? daylist[0] : {}, - ); - //天气 - return hefengWeatherContent( - context, - weather_city, - now, - daylist, - ); - } - } else if (meta!.keys.contains('maps_weather')) { - //高德的 - final maps_weather = meta!['maps_weather'] as Map; - final city = maps_weather['city']; - if (city != null) { - final rawList = maps_weather['forecasts'] as List? ?? []; - final daylist = - rawList.map((e) => Map.from(e)).toList(); - final now = daylist.isNotEmpty ? daylist[0] : {}; - //天气 - return buildWeatherContent( - context, - city, - now, - daylist, - ); - } - } else if (meta!.keys.contains('map_weather')) { - //百度的 - final map_weather = meta!['map_weather'] as Map; - final status = map_weather['status']; - if (status == 0) { - final result = map_weather['result'] as Map; - final city = result["location"]["city"] as String? ?? ''; - final rawList = result['forecasts'] as List? ?? []; - final daylist = - rawList.map((e) => Map.from(e)).toList(); - final now = daylist.isNotEmpty ? daylist[0] : {}; - //天气 - return buildWeatherBaiduContent( - context, - city, - now, - daylist, - ); - } - } else if (meta!.keys.contains('card_travel')) { - final rawList = meta!['card_travel']['items'] as List? ?? []; - final travellist = - rawList.map((e) => Map.from(e)).toList(); - // 旅游 - return buildTravelContent(context, travellist); - } else if (meta!.keys.contains('maps_route_navigation')) { - final maps_route_navigation = meta!['maps_route_navigation']; - if (maps_route_navigation is Map) { - final card_navigation = - maps_route_navigation["card_navigation"] as Map; - final start = card_navigation['start'] as String? ?? ''; - final end = card_navigation['end'] as String? ?? ''; - final end_longitude = card_navigation["end_longitude"] as String; - final end_latitude = card_navigation["end_latitude"] as String; - final destination = card_navigation['destination'] as String? ?? ''; - final rawList = card_navigation['items'] as List? ?? null; - if (rawList != null) { - final list = - rawList.map((e) => Map.from(e)).toList(); - final rawNavigation = card_navigation['navigation']; - final Map? navigation = - rawNavigation is Map ? rawNavigation : null; - // print("导航实时数据 ${rawNavigation}"); - // 导航 - return navigationCard(context, - start: start, - end: end, - endlatitude: end_latitude, - endlongitude: end_longitude, - destination: destination, - modes: list, - navigation: navigation); - } - } - } else if (meta!.keys.contains('maps_google_route_navigation')) { - final maps_google_route_navigation = - meta!['maps_google_route_navigation']; - if (maps_google_route_navigation is Map) { - final data = maps_google_route_navigation['data']; - if (data != null) { - final origin = data['origin'] as String? ?? ''; - final destination = data['destination'] as String? ?? ''; - final end = data['end'] as String? ?? ''; - final rawList = data['items'] as List? ?? null; - if (rawList != null) { - final list = - rawList.map((e) => Map.from(e)).toList(); - final rawNavigation = data['navigation']; - final Map? navigation = - rawNavigation is Map ? rawNavigation : null; - // print("导航实时数据 ${rawNavigation}"); - // 导航 - return googlenavigationCard(context, - start: origin, - end: destination, - destination: end, - modes: list, - navigation: navigation); - } - } - } - } - - // if (message.contains('播放列表') || message.contains('音乐')) { - // return _buildMusicContent(context); - // } else if (message.contains('景点') || message.contains('旅游')) { - // return _buildTravelContent(context); - // } else if (message.contains('天气') || - // message.contains('气温') || - // message.contains('预报')) { - // return _buildWeatherContent(context); - // } - } - - // 创建消息内容Widget - Widget messageContent; - - // 使用Markdown渲染AI消息,用户消息仍然使用普通文本 - if (!isUser) { - messageContent = MarkdownBody( - data: message, - styleSheet: MarkdownStyleSheet( - p: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.9) - : Colors.black87, - fontSize: 14, - height: 1.4, - fontWeight: FontWeight.w400, - ), - h1: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - h2: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - h3: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, - fontSize: 15, - fontWeight: FontWeight.bold, - ), - code: TextStyle( - backgroundColor: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[800] - : Colors.grey[200], - fontFamily: 'monospace', - fontSize: 13, - ), - codeblockDecoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: BorderRadius.circular(4), - ), - blockquote: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[400] - : Colors.grey[700], - fontSize: 14, - fontStyle: FontStyle.italic, - ), - blockquoteDecoration: BoxDecoration( - border: Border( - left: BorderSide( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[600]! - : Colors.grey[400]!, - width: 4, - ), - ), - ), - listBullet: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.9) - : Colors.black87, - fontSize: 14, - ), - ), - //selectable: true, - ); - } else { - // 用户消息仍然使用普通文本 - messageContent = Text( - message, - style: TextStyle( - color: isUser - ? Colors.white - : (Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.9) - : Colors.black87), - fontSize: 14, - height: 1.4, - fontWeight: FontWeight.w400, - ), - ); - } - - // 如果是AI消息且有工具调用,确保最小宽度以防止工具调用列表越界 - if (!isUser && functionCalls != null && functionCalls!.isNotEmpty) { - return Container( - constraints: const BoxConstraints( - minWidth: 200, // 确保最小宽度,防止工具调用列表越界 - ), - child: messageContent, - ); - } - - return messageContent; - } - - /// 构建输入中的指示器 - Widget _buildTypingIndicator() { - return SizedBox( - width: 40, - height: 20, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildDot(100), - _buildDot(300), - _buildDot(500), - ], - ), - ); - } - - /// 构建动画点 - Widget _buildDot(int milliseconds) { - return _AnimatedDot(delayMilliseconds: milliseconds); - } - - /// 智能格式化时间戳 - String _formatTimestamp(DateTime messageTime) { - final now = DateTime.now(); - final today = DateTime(now.year, now.month, now.day); - final yesterday = today.subtract(const Duration(days: 1)); - final messageDate = - DateTime(messageTime.year, messageTime.month, messageTime.day); - - // 计算时间差 - final difference = now.difference(messageTime); - - // 1分钟内显示"刚刚" - if (difference.inMinutes < 1) { - return 'justNow'.tr; // 刚刚 - } - - // 1小时内显示"X分钟前" - if (difference.inHours < 1) { - return 'minutesAgo' - .trParams({'minutes': difference.inMinutes.toString()}); // X分钟前 - } - - // 今天的消息显示时间 - if (messageDate == today) { - return DateFormat('HH:mm').format(messageTime); - } - - // 昨天的消息显示"昨天 HH:mm" - if (messageDate == yesterday) { - return 'yesterday'.tr + - ' ' + - DateFormat('HH:mm').format(messageTime); // 昨天 HH:mm - } - - // 本周内的消息显示"星期X HH:mm" - if (difference.inDays < 7) { - final weekday = _getWeekdayName(messageTime.weekday); - return weekday + ' ' + DateFormat('HH:mm').format(messageTime); - } - - // 今年的消息显示"MM-dd HH:mm" - if (messageTime.year == now.year) { - return DateFormat('MM-dd HH:mm').format(messageTime); - } - - // 其他情况显示完整日期"yyyy-MM-dd HH:mm" - return DateFormat('yyyy-MM-dd HH:mm').format(messageTime); - } - - /// 获取星期名称 - String _getWeekdayName(int weekday) { - switch (weekday) { - case 1: - return 'monday'.tr; // 星期一 - case 2: - return 'tuesday'.tr; // 星期二 - case 3: - return 'wednesday'.tr; // 星期三 - case 4: - return 'thursday'.tr; // 星期四 - case 5: - return 'friday'.tr; // 星期五 - case 6: - return 'saturday'.tr; // 星期六 - case 7: - return 'sunday'.tr; // 星期日 - default: - return ''; - } - } - - /// 构建工具调用流程组件 - Widget _buildFunctionCallsWidget(BuildContext context) { - return Positioned( - top: 0, - left: 0, - right: 0, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 0, vertical: 0), // 减少垂直padding - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 标题行 - Container( - height: 40, // 增加固定高度,给字体更多空间 - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? const Color.fromARGB(255, 71, 68, 68)?.withOpacity(0.8) - : Colors.grey[100]?.withOpacity(0.9), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(6), - topRight: Radius.circular(6), - ), - border: Border.all( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.1) - : Colors.black.withOpacity(0.1), - width: 0.5, - ), - ), - child: Row( - children: [ - Icon( - Icons.build, // 工具icon - size: 20, // 减小图标大小 - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.7) - : Colors.black54, - ), - const SizedBox(width: 4), - // 显示最后一个工具名称 - 可点击展开 - Expanded( - child: GestureDetector( - onTap: () async { - await controller.updateToolCallsExpanded( - sessionid, - !isToolCallsExpanded, - ); - }, - child: Text( - functionCalls != null && functionCalls!.isNotEmpty - ? functionCalls!.keys.last - : 'toolCall'.tr, - style: TextStyle( - fontSize: 16, // 适当增大字体 - color: - Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.7) - : Colors.black54, - fontWeight: FontWeight.w500, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - const SizedBox(width: 4), - // 拷贝按钮 - GestureDetector( - onTap: () { - _copyFunctionCallsToClipboard(); - }, - child: Container( - padding: const EdgeInsets.all(2), - child: Icon( - Icons.copy, - size: 20, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.7) - : Colors.black54, - ), - ), - ), - const SizedBox(width: 4), - // 展开按钮 - GestureDetector( - onTap: () async { - await controller.updateToolCallsExpanded( - sessionid, - !isToolCallsExpanded, - ); - }, - child: Container( - padding: const EdgeInsets.all(2), // 增加 padding 从 2 到 8 - child: Icon( - isToolCallsExpanded - ? Icons.keyboard_arrow_down // 展开时显示向上箭头 - : Icons.keyboard_arrow_up, // 收起时显示向下箭头 - size: 20, // 增加图标大小从 14 到 16 - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.7) - : Colors.black54, - ), - ), - ), - ], - ), - ), - // 展开的工具调用列表 - if (isToolCallsExpanded && functionCalls != null) - Container( - margin: const EdgeInsets.only(top: 2), // 减少顶部间距 - padding: const EdgeInsets.all(6), // 减少内边距 - constraints: const BoxConstraints( - maxHeight: 120, // 限制最大高度,避免超界 - ), - decoration: BoxDecoration( - // 添加更强的背景色来遮挡下层UI - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[900]?.withOpacity(0.95) // 深色模式下使用深灰色背景 - : Colors.white.withOpacity(0.95), // 浅色模式下使用白色背景 - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(6), - bottomRight: Radius.circular(6), - ), - border: Border.all( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.2) - : Colors.black.withOpacity(0.2), - width: 0.5, - ), - // 添加阴影增强遮挡效果 - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: SingleChildScrollView( - // 添加滚动,防止超界 - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: functionCalls!.entries.map((entry) { - String toolName = entry.key; - dynamic toolResult = entry.value; - return Padding( - padding: const EdgeInsets.only(bottom: 8), // 减少间距 - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 工具名称 - 固定宽度 - SizedBox( - width: 80, // 固定宽度 - child: Text( - toolName, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).brightness == - Brightness.dark - ? Colors.white.withOpacity(0.6) - : Colors.black45, - fontWeight: FontWeight.w500, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 12), // 增加间距 - // 工具结果 - 剩余空间 - Expanded( - child: Text( - toolResult?.toString() ?? 'noResult'.tr, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).brightness == - Brightness.dark - ? Colors.white.withOpacity(0.8) - : Colors.black87, - height: 1.1, // 减小行高 - ), - ), - ), - ], - ), - ); - }).toList(), - ), - ), - ), - ], - ), - )); - } - - /// 拷贝工具调用内容到剪贴板 - void _copyFunctionCallsToClipboard() { - if (functionCalls == null || functionCalls!.isEmpty) { - Get.snackbar( - 'tip'.tr, - 'noToolCallContentToCopy'.tr, - snackPosition: SnackPosition.BOTTOM, - duration: const Duration(seconds: 2), - ); - return; - } - - StringBuffer buffer = StringBuffer(); - buffer.writeln('toolCallDetails'.tr); - buffer.writeln('=' * 30); - - functionCalls!.forEach((toolName, toolData) { - buffer.writeln('🔧 工具名称: $toolName'); - if (toolData is Map) { - toolData.forEach((key, value) { - buffer.writeln(' $key: $value'); - }); - } else { - buffer.writeln(' 结果: $toolData'); - } - buffer.writeln('-' * 20); - }); - - Clipboard.setData(ClipboardData(text: buffer.toString())); - - Get.snackbar( - 'success'.tr, - 'toolCallContentCopied'.tr, - snackPosition: SnackPosition.BOTTOM, - duration: const Duration(seconds: 2), - ); - } -} - -/// 动画点组件 -class _AnimatedDot extends StatefulWidget { - final int delayMilliseconds; - - const _AnimatedDot({required this.delayMilliseconds}); - - @override - _AnimatedDotState createState() => _AnimatedDotState(); -} - -class _AnimatedDotState extends State<_AnimatedDot> - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _animation; - - @override - void initState() { - super.initState(); - - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1500), - ); - - _animation = Tween(begin: 0, end: 6).animate( - CurvedAnimation( - parent: _controller, - curve: Interval(0.0, 0.5, curve: Curves.easeOut), - reverseCurve: Interval(0.5, 1.0, curve: Curves.easeIn), - ), - )..addListener(() { - setState(() {}); - }); - - Future.delayed(Duration(milliseconds: widget.delayMilliseconds), () { - if (mounted) { - _controller.repeat(); - } - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - width: 6, - height: 6, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(3), - boxShadow: [ - BoxShadow( - color: Colors.white.withOpacity(0.5), - blurRadius: _animation.value, - spreadRadius: _animation.value / 2, - ), - ], - ), - ); - } -} diff --git a/apps/client/lib/modules/agent/views/music_card.dart b/apps/client/lib/modules/agent/views/music_card.dart deleted file mode 100644 index b60025ee..00000000 --- a/apps/client/lib/modules/agent/views/music_card.dart +++ /dev/null @@ -1,275 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import '../../../routes/app_routes.dart'; -import '../controllers/agent_controller.dart'; -import '../widgets/circular_progress_button.dart'; - -/// 构建音乐项目 -Widget buildMusicItem( - BuildContext context, int id, String name, String singer, String image) { - final controller = Get.find(); // 获取控制器 - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - Map song = { - 'id': id, - 'title': name, - 'artist': singer, - 'coverUrl': image, - }; - - return Container( - padding: EdgeInsets.all(14.w), - decoration: BoxDecoration( - color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03), - blurRadius: 3, - spreadRadius: 1, - offset: const Offset(0, 1), - ), - ], - ), - child: Row( - children: [ - Container( - width: 50.w, - height: 50.h, - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.grey[100], - borderRadius: BorderRadius.circular(8.r), - ), - child: Stack( - children: [ - // 主图片 - Container( - width: 50.w, - height: 50.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - image: image.isNotEmpty - ? DecorationImage( - image: NetworkImage(image), - fit: BoxFit.cover, - ) - : null, - ), - child: image.isEmpty - ? Icon( - Icons.music_note, - size: 18.sp, - color: Theme.of(context).primaryColor, - ) - : null, - ), - // 右下角音乐图标 - if (image.isNotEmpty) - Positioned( - right: 2.w, - bottom: 2.w, - child: Container( - width: 16.w, - height: 16.w, - // decoration: BoxDecoration( - // color: Colors.white, - // borderRadius: BorderRadius.circular(2.r), - // boxShadow: [ - // BoxShadow( - // color: Colors.black.withOpacity(0.2), - // blurRadius: 1, - // offset: const Offset(0, 0.5), - // ), - // ], - // ), - child: ClipRRect( - borderRadius: BorderRadius.circular(2.r), - child: Image.asset( - 'assets/images/qqmusic.png', - fit: BoxFit.contain, - ), - ), - ), - ), - ], - ), - ), - SizedBox(width: 12.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - style: TextStyle( - fontSize: 13.sp, - fontWeight: FontWeight.w500, - color: isDarkMode - ? Colors.white.withValues(alpha: 0.9) - : Colors.grey[800], - ), - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 2.h), - Text( - singer, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - Obx(() { - final isThisPlaying = - controller.isPlaying.value && controller.currentId.value == id; - - // 只有当前播放的歌曲才显示进度 - final currentProgress = - isThisPlaying ? controller.progress.value : 0.0; - - return CircularProgressButton( - isPlaying: isThisPlaying, - progress: currentProgress, - primaryColor: Theme.of(context).primaryColor, - backgroundColor: Colors.grey, - size: 36.0, - strokeWidth: 2.0, - onPlayPause: () async { - try { - if (controller.isPlaying.value) { - if (controller.currentId.value == id) { - // 当前歌曲正在播放,暂停它 - await controller.musicPause(); - } else { - // 其他歌曲正在播放,播放这首歌 - await controller.musicPlay(song); - } - } else { - if (controller.currentId.value == id) { - // 当前歌曲已暂停,恢复播放 - await controller.musicResume(); - } else { - // 播放新歌曲 - await controller.musicPlay(song); - } - } - } catch (e) { - // 静默处理错误,避免影响用户体验 - } - }, - onSeek: isThisPlaying - ? (seekProgress) { - // 只有当前播放的歌曲才允许跳转 - final targetPosition = - (controller.duration.value * seekProgress).round(); - controller.seekToPosition(targetPosition); - } - : null, - ); - }), - ], - ), - ); -} - -/// 构建音乐内容卡片 -Widget buildMusicContent( - BuildContext context, List> playlist) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - final controller = Get.find(); // 获取控制器 - controller.updateCurrentPlaylistContext(playlist, controller.currentId.value); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'recommendedMusic'.tr, //以下是一些推荐的音乐: - style: TextStyle( - color: - isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.black87, - fontSize: 14, - height: 1.4, - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(height: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - width: 18.w, - height: 18.w, - child: Image.asset( - 'assets/images/qqmusic.png', - fit: BoxFit.contain, - ), - ), - const SizedBox(width: 6), - Text( - 'playList'.tr, //播放列表 - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[300] - : Colors.grey[700], - ), - ), - ], - ), - const SizedBox(height: 10), - - // 动态生成音乐条目 - ...playlist.asMap().entries.take(4).map((entry) { - final index = entry.key; - final item = entry.value; - final id = item["id"]; - final name = item["name"] ?? "未知歌曲"; - final singer = item["singer"] ?? "未知歌手"; - final image = item["image"] ?? ""; - return Column( - children: [ - buildMusicItem(context, id, name, singer, image), - if (index != playlist.length - 1) - Divider( - height: 1, - thickness: 1, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[800] - : Colors.grey[100], - ), - ], - ); - }).toList(), - const SizedBox(height: 8), - Center( - child: TextButton( - onPressed: () { - // 跳转到完整歌单页面,传递完整的歌单数据 - Get.toNamed(Routes.musicPlaylist, arguments: playlist); - }, - style: TextButton.styleFrom( - minimumSize: Size.zero, - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Text( - 'moreMusic'.tr, //更多音乐, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ], - ), - ], - ); -} diff --git a/apps/client/lib/modules/agent/views/navigation_card.dart b/apps/client/lib/modules/agent/views/navigation_card.dart deleted file mode 100644 index 31b900a5..00000000 --- a/apps/client/lib/modules/agent/views/navigation_card.dart +++ /dev/null @@ -1,472 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import '../controllers/agent_controller.dart'; - -String formatDistance(String distanceStr) { - final distance = double.tryParse(distanceStr); - if (distance == null) return distanceStr; // 如果无法解析,则返回原始字符串 - - if (distance >= 1000) { - final km = distance / 1000; - return '${km.toStringAsFixed(1)} KM'; - } else { - return '${distance.toStringAsFixed(0)} M'; - } -} - -String formatDuration(String secondsStr) { - int totalSeconds = int.tryParse(secondsStr) ?? 0; - Duration duration = Duration(seconds: totalSeconds); - - int hours = duration.inHours; - int minutes = duration.inMinutes.remainder(60); - int seconds = duration.inSeconds.remainder(60); - - List parts = []; - if (hours > 0) parts.add('$hours${'hours'.tr}'); // 小时 - if (minutes > 0) parts.add('$minutes${'minutes'.tr}'); // 分钟 - //不需要秒 - //if (seconds > 0 || parts.isEmpty) parts.add('$seconds${'seconds'.tr}'); // 秒 - - return parts.join(''); -} - -/// 导航卡片组件 -/// -/// 各元素含义分析: -/// - [start]: 起点坐标 (经纬度格式) -/// - [end]: 终点坐标 (经纬度格式) -/// - [destination]: 目的地名称显示 -/// - [modes]: 导航方式列表,包含驾车、步行、公交等选项 -/// - [navigation]: 当前导航状态信息(可选) -/// - [height]: 卡片高度 -Widget navigationCard( - BuildContext context, { - required String start, - required String end, - required String endlatitude, - required String endlongitude, - required String destination, - required List> modes, - Map? navigation, - double? height, // 改为可选参数,允许自适应高度 -}) { - int currentIndex = 0; - final controller = Get.find(); - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Container( - height: height?.h ?? 350.h, // 如果没有指定高度,使用350作为默认高度 - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16.r), - color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white, - boxShadow: [ - BoxShadow( - blurRadius: 12, - color: Colors.black.withValues(alpha: isDarkMode ? 0.4 : 0.08), - offset: const Offset(0, 2), - ), - ], - ), - child: StatefulBuilder( - builder: (context, setInnerState) { - return Column( - children: [ - // 顶部:路线预览图片区域 - Expanded( - flex: 4, // 增加图片区域的flex值,让图片占更多空间 - child: ClipRRect( - borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), - child: Stack( - children: [ - // 路线图片 - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: Image.network( - modes[currentIndex]["image"] as String, - key: ValueKey( - modes[currentIndex]["ntype"] as String), - height: double.infinity, - width: double.infinity, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - child: Icon( - Icons.map_outlined, - size: 48.sp, - color: isDarkMode - ? Colors.grey[600] - : Colors.grey[400], - ), - ); - }, - ), - ), - // 导航状态覆盖层 - if (navigation != null) - Positioned( - top: 12.h, - left: 12.w, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: - Theme.of(context).primaryColor.withOpacity(0.9), - borderRadius: BorderRadius.circular(12.r), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.navigation, - color: Colors.white, - size: 14.sp, - ), - SizedBox(width: 4.w), - Text( - 'navigating'.tr, // 导航中 - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - - // 中部:导航方式选择器 - Container( - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w), - child: Row( - children: List.generate(modes.length, (index) { - final mode = modes[index]; - final isSelected = index == currentIndex; - final isEnabled = mode['enable'] as bool; - - return Expanded( - child: GestureDetector( - onTap: isEnabled - ? () { - setInnerState(() => currentIndex = index); - } - : null, - child: Container( - margin: EdgeInsets.symmetric(horizontal: 4.w), - padding: EdgeInsets.symmetric( - vertical: 8.h, horizontal: 12.w), - decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).primaryColor.withOpacity(0.1) - : Colors.transparent, - borderRadius: BorderRadius.circular(8.r), - border: Border.all( - color: isSelected - ? Theme.of(context).primaryColor - : (isDarkMode - ? Colors.grey[700]! - : Colors.grey[300]!), - width: isSelected ? 2 : 1, - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - _getNavigationIcon(mode["ntype"] as String), - color: isEnabled - ? (isSelected - ? Theme.of(context).primaryColor - : (isDarkMode - ? Colors.white70 - : Colors.black87)) - : Colors.grey, - size: 20.sp, - ), - // SizedBox(height: 4.h), - // Text( - // (mode["ntype"] as String).tr, - // textAlign: TextAlign.center, - // style: TextStyle( - // color: isEnabled - // ? (isSelected - // ? Theme.of(context).primaryColor - // : (isDarkMode - // ? Colors.white70 - // : Colors.black87)) - // : Colors.grey, - // fontSize: 11.sp, - // fontWeight: isSelected - // ? FontWeight.w600 - // : FontWeight.w400, - // ), - // ), - ], - ), - ), - ), - ); - }), - ), - ), - - // 底部:目的地信息和操作按钮 - Container( - padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 第一层:起点 → 目的地信息(独占整个宽度) - Row( - crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐 - children: [ - // 起点图标 - 统一图标容器宽度,确保对齐 - SizedBox( - width: 16.sp, // 固定图标容器宽度 - child: Icon( - Icons.my_location, - size: 15.sp, // 统一图标大小 - color: Theme.of(context).primaryColor, - ), - ), - SizedBox(width: 6.w), // 统一间距 - // 当前位置文本 - Text( - 'currentLocation'.tr, // 当前位置 - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white70 : Colors.black54, - height: 1.3, // 设置行高,提升多行文本可读性 - ), - softWrap: true, // 启用软换行 - overflow: TextOverflow.visible, // 确保文本完全显示 - ), - SizedBox(width: 8.w), - // 箭头图标 - Icon( - Icons.arrow_forward, - size: 15.sp, // 统一图标大小 - color: isDarkMode ? Colors.white54 : Colors.black45, - ), - SizedBox(width: 8.w), - // 目的地图标 - Icon( - Icons.location_on, - size: 15.sp, // 统一图标大小 - color: Colors.red, - ), - SizedBox(width: 6.w), // 统一间距 - // 目的地名称 - 使用Expanded确保自动换行 - Expanded( - child: Text( - destination, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black87, - height: 1.2, // 设置行高,提升多行文本可读性 - ), - softWrap: true, // 启用软换行 - overflow: TextOverflow.visible, // 确保文本完全显示 - ), - ), - ], - ), - - SizedBox(height: 4.h), // 第一层和第二层之间的间距 - - // 第二层:距离时间信息(左侧)+ 导航按钮(右侧) - Row( - crossAxisAlignment: CrossAxisAlignment.end, // 底部对齐 - children: [ - // 左侧:距离和时间信息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 距离信息 - Row( - crossAxisAlignment: - CrossAxisAlignment.center, // 确保垂直居中对齐 - children: [ - // 距离图标 - 统一图标容器宽度,确保与第一层对齐 - SizedBox( - width: 16.sp, // 与第一层图标容器宽度一致 - child: Icon( - Icons.straighten, - size: 15.sp, // 统一图标大小 - color: Theme.of(context).primaryColor, - ), - ), - SizedBox(width: 6.w), // 与第一层间距一致 - Flexible( - child: Text( - formatDistance( - modes[currentIndex]["distance"]), - style: TextStyle( - fontSize: 14.sp, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, - height: 1.3, // 设置行高,提升多行文本可读性 - ), - softWrap: true, // 启用软换行 - overflow: TextOverflow.visible, // 确保文本完全显示 - ), - ), - ], - ), - SizedBox(height: 4.h), - // 时间信息 - Row( - crossAxisAlignment: - CrossAxisAlignment.center, // 确保垂直居中对齐 - children: [ - // 时间图标 - 统一图标容器宽度,确保与第一层对齐 - SizedBox( - width: 16.sp, // 与第一层图标容器宽度一致 - child: Icon( - Icons.access_time, - size: 15.sp, // 统一图标大小 - color: Theme.of(context).primaryColor, - ), - ), - SizedBox(width: 6.w), // 与第一层间距一致 - Flexible( - child: Text( - '${'approximate'.tr}${formatDuration(modes[currentIndex]["duration"])}', // 约 - style: TextStyle( - fontSize: 14.sp, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, - height: 1.3, // 设置行高,提升多行文本可读性 - ), - softWrap: true, // 启用软换行 - overflow: TextOverflow.visible, // 确保文本完全显示 - ), - ), - ], - ), - // 导航状态信息 - if (navigation != null) ...[ - SizedBox(height: 6.h), - Container( - padding: EdgeInsets.symmetric( - horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: Theme.of(context) - .primaryColor - .withOpacity(0.1), - borderRadius: BorderRadius.circular(6.r), - ), - child: Text( - "${navigation["distanceText"]}${'enterRoad'.tr}${navigation["nextRoad"]}", // 进入 - style: TextStyle( - fontSize: 12.sp, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, - height: 1.3, // 设置行高,提升多行文本可读性 - ), - softWrap: true, // 启用软换行 - overflow: TextOverflow.visible, // 确保文本完全显示 - ), - ), - ], - ], - ), - ), - - SizedBox(width: 16.w), // 左右内容之间的间距 - - // 右侧:开始导航按钮 - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Theme.of(context).primaryColor, - Theme.of(context).primaryColor.withOpacity(0.8), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Theme.of(context) - .primaryColor - .withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(12.r), - onTap: () { - controller.openNavigation( - true, - start, - destination, - endlatitude, - endlongitude, - modes[currentIndex]["ntype"] as String); - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 12.h, - ), - child: Icon( - Icons.navigation, - color: Colors.white, - size: 20.sp, - ), - ), - ), - ), - ), - ], - ), - ], - ), - ), - ], - ); - }, - ), - ); -} - -/// 根据导航类型获取对应图标 -IconData _getNavigationIcon(String ntype) { - switch (ntype.toLowerCase()) { - case 'driving': - case '驾车': - return Icons.directions_car; - case 'walking': - case '步行': - return Icons.directions_walk; - case 'transit': - case 'tsransit': // 修正拼写错误 - case '公交': - return Icons.directions_transit; - case 'bicycling': // 修正为正确的英文值 - case 'cycling': - case '骑行': - return Icons.directions_bike; - default: - return Icons.navigation; - } -} diff --git a/apps/client/lib/modules/agent/views/qqmusic_card.dart b/apps/client/lib/modules/agent/views/qqmusic_card.dart deleted file mode 100644 index 5e4086c9..00000000 --- a/apps/client/lib/modules/agent/views/qqmusic_card.dart +++ /dev/null @@ -1,265 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import '/modules/agent/widgets/circular_progress_button.dart'; -import '../../../routes/app_routes.dart'; -import '../../../data/services/qq_music.dart'; -import '../../../data/models/song_model.dart'; - -/// 构建音乐项目 -Widget buildQQMusicItem( - BuildContext context, List songs, SongModel song, int index) { - final qqMusicService = QqMusicService.to; // 使用QQ音乐全局服务 - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Container( - padding: EdgeInsets.all(14.w), - decoration: BoxDecoration( - color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03), - blurRadius: 3, - spreadRadius: 1, - offset: const Offset(0, 1), - ), - ], - ), - child: Row( - children: [ - Container( - width: 50.w, - height: 50.h, - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.grey[100], - borderRadius: BorderRadius.circular(8.r), - ), - child: Stack( - children: [ - // 主图片 - Container( - width: 50.w, - height: 50.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - image: song.albumPic!.isNotEmpty - ? DecorationImage( - image: NetworkImage(song.albumPic!), - fit: BoxFit.cover, - ) - : null, - ), - child: song.albumPic!.isEmpty - ? Icon( - Icons.music_note, - size: 18.sp, - color: Theme.of(context).primaryColor, - ) - : null, - ), - // 右下角音乐图标 - if (song.albumPic!.isNotEmpty) - Positioned( - right: 0.w, - bottom: 0.w, - child: Container( - width: 12.w, - height: 12.w, - child: Image.asset( - 'assets/images/qqmusic.png', - fit: BoxFit.contain, - ), - ), - ), - ], - ), - ), - SizedBox(width: 12.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - song.name, - style: TextStyle( - fontSize: 13.sp, - fontWeight: FontWeight.w500, - color: isDarkMode - ? Colors.white.withValues(alpha: 0.9) - : Colors.grey[800], - ), - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 2.h), - Text( - song.artist, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - Obx(() { - // 使用QQ音乐服务的全局状态 - final isThisPlaying = qqMusicService.isPlaying.value && - qqMusicService.currentSong.value?.mid == song.mid; - - // 计算播放进度 - final currentProgress = - isThisPlaying && qqMusicService.totalDuration.value > 0 - ? qqMusicService.currentPosition.value / - qqMusicService.totalDuration.value - : 0.0; - - return CircularProgressButton( - isPlaying: isThisPlaying, - progress: currentProgress, - primaryColor: Theme.of(context).primaryColor, - backgroundColor: Colors.grey, - size: 36.0, - strokeWidth: 2.0, - onPlayPause: () { - try { - if (qqMusicService.isPlaying.value) { - if (qqMusicService.currentSong.value?.mid == song.mid) { - // 当前歌曲正在播放,暂停它 - qqMusicService.pauseMusic(); - } else { - final songMids = songs.map((s) => s.mid).toList(); - // 其他歌曲正在播放,播放这首歌 - qqMusicService.playSongs( - songMids: songMids, - index: index, - songList: [song], - currentSongModel: song, - ); - } - } else { - if (qqMusicService.currentSong.value?.mid == song.mid) { - // 当前歌曲已暂停,恢复播放 - qqMusicService.resumeMusic(); - } else { - final songMids = songs.map((s) => s.mid).toList(); - // 播放新歌曲 - qqMusicService.playSongs( - songMids: songMids, - index: index, - songList: [song], - currentSongModel: song, - ); - } - } - } catch (e) { - // 静默处理错误,避免影响用户体验 - print('播放控制失败: $e'); - } - }, - onSeek: isThisPlaying - ? (seekProgress) { - // QQ音乐暂不支持拖拽进度,可以在这里添加相关逻辑 - // 或者显示提示信息 - print('QQ音乐暂不支持拖拽进度'); - } - : null, - ); - }), - ], - ), - ); -} - -/// 构建音乐内容卡片 -Widget buildQQMusicContent(BuildContext context, List songs) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'recommendedMusic'.tr, //以下是一些推荐的音乐: - style: TextStyle( - color: - isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.black87, - fontSize: 14, - height: 1.4, - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(height: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - width: 18.w, - height: 18.w, - child: Image.asset( - 'assets/images/qqmusic.png', - fit: BoxFit.contain, - ), - ), - const SizedBox(width: 6), - Text( - 'playList'.tr, //播放列表 - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[300] - : Colors.grey[700], - ), - ), - ], - ), - const SizedBox(height: 10), - - // 动态生成音乐条目 - ...songs.asMap().entries.take(4).map((entry) { - final index = entry.key; - final item = entry.value; - return Column( - children: [ - buildQQMusicItem(context, songs, item, index), - if (index != songs.length - 1) - Divider( - height: 1, - thickness: 1, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[800] - : Colors.grey[100], - ), - ], - ); - }).toList(), - const SizedBox(height: 8), - Center( - child: TextButton( - onPressed: () { - // 跳转到完整歌单页面,传递完整的歌单数据 - Get.toNamed(Routes.qqmusic, arguments: {"playlist": songs}); - }, - style: TextButton.styleFrom( - minimumSize: Size.zero, - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Text( - 'moreMusic'.tr, //更多音乐, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ], - ), - ], - ); -} diff --git a/apps/client/lib/modules/agent/views/travel_card.dart b/apps/client/lib/modules/agent/views/travel_card.dart deleted file mode 100644 index be62ccda..00000000 --- a/apps/client/lib/modules/agent/views/travel_card.dart +++ /dev/null @@ -1,223 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -/// 构建旅游内容卡片 -Widget buildTravelContent( - BuildContext context, List> travellist) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '以下是一些推荐搜索结果:', - style: TextStyle( - color: isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.black87, - fontSize: 14.sp, - height: 1.4, - fontWeight: FontWeight.w400, - ), - ), - SizedBox(height: 12.h), - Container( - padding: EdgeInsets.all(12.w), - decoration: BoxDecoration( - color: isDarkMode ? const Color(0xFF1E1E1E) : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03), - blurRadius: 3, - spreadRadius: 1, - offset: const Offset(0, 1), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Row( - // children: [ - // Icon( - // Icons.place, - // size: 16, - // color: Color(0xFFE11D48), - // ), - // const SizedBox(width: 6), - // Text( - // '热门景点 · 东京', - // style: TextStyle( - // fontSize: 13, - // fontWeight: FontWeight.w600, - // color: Colors.grey[700], - // ), - // ), - // ], - // ), - // const SizedBox(height: 10), - - // 动态生成旅游景点条目 - ...travellist.asMap().entries.map((entry) { - final index = entry.key; - final item = entry.value; - final name = item["name"] ?? "未知景点"; //名字 - final addr = item["addr"] ?? ""; //地址 - // final tag = item["tag"] ?? "未知标签"; //标签 - // final rating = item["rating"] ?? ""; //评分 - // final distance = item["distance"] ?? ""; //距离 - // final images = item["images"] ?? []; // 图片列表 - return Column( - children: [ - buildLocationItem( - context, - name, - addr, - ), - if (index != travellist.length - 1) - Divider( - height: 1.h, - thickness: 1, - color: isDarkMode ? Colors.grey[700] : Colors.grey[100] - ), - ], - ); - }).toList(), - // // 景点列表 - // _buildLocationItem('东京塔', '标志性建筑,可俯瞰东京全景', context), - // Divider(height: 1, thickness: 1, color: Colors.grey[100]), - // _buildLocationItem('涩谷十字路口', '世界上最繁忙的十字路口之一', context), - // Divider(height: 1, thickness: 1, color: Colors.grey[100]), - // _buildLocationItem('浅草寺', '东京最古老的寺庙', context), - - SizedBox(height: 8.h), - Center( - child: TextButton( - onPressed: () {}, - style: TextButton.styleFrom( - minimumSize: Size.zero, - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Text( - '查看更多景点', - style: TextStyle( - fontSize: 12.sp, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ], - ), - ), - SizedBox(height: 10.h), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: () {}, - icon: Icon(Icons.map, size: 16.sp), - label: const Text('查看地图'), - style: OutlinedButton.styleFrom( - foregroundColor: Theme.of(context).primaryColor, - side: BorderSide( - color: Theme.of(context).primaryColor.withValues(alpha: 0.5)), - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), - ), - textStyle: TextStyle(fontSize: 12.sp), - ), - ), - ), - SizedBox(width: 8.w), - Expanded( - child: OutlinedButton.icon( - onPressed: () {}, - icon: Icon(Icons.bookmark_border, size: 16.sp), - label: const Text('保存到收藏'), - style: OutlinedButton.styleFrom( - foregroundColor: isDarkMode ? Colors.grey[400] : Colors.grey[700], - side: BorderSide(color: isDarkMode ? Colors.grey[600]! : Colors.grey[400]!), - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), - ), - textStyle: TextStyle(fontSize: 12.sp), - ), - ), - ), - ], - ), - ], - ); -} - -/// 构建地点项目 -Widget buildLocationItem( - BuildContext context, String name, String description) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Padding( - padding: EdgeInsets.symmetric(vertical: 8.h), - child: Row( - children: [ - Container( - width: 38.w, - height: 38.h, - decoration: BoxDecoration( - color: isDarkMode - ? const Color(0xFF2A2A2A) - : Theme.of(context).primaryColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8.r), - ), - child: Icon( - Icons.place, - size: 18.sp, - color: Theme.of(context).primaryColor, - ), - ), - SizedBox(width: 12.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - style: TextStyle( - fontSize: 13.sp, - fontWeight: FontWeight.w500, - color: isDarkMode - ? Colors.white.withValues(alpha: 0.9) - : Colors.grey[800], - ), - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 2.h), - Text( - description, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - IconButton( - onPressed: () {}, - icon: Icon( - Icons.arrow_forward, - color: isDarkMode ? Colors.grey[500] : Colors.grey[400] - ), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - iconSize: 20.sp, - ), - ], - ), - ); -} diff --git a/apps/client/lib/modules/agent/views/weather_baidu.dart b/apps/client/lib/modules/agent/views/weather_baidu.dart deleted file mode 100644 index 585eae20..00000000 --- a/apps/client/lib/modules/agent/views/weather_baidu.dart +++ /dev/null @@ -1,562 +0,0 @@ -import 'package:common_utils/common_utils.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -/// 构建天气内容卡片 -Widget buildWeatherBaiduContent(BuildContext context, String city, - Map now, List> days) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'weatherQuery'.tr, // 翻译键代表的中文文本内容:查询天气情况: - style: TextStyle( - color: - isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.black87, - fontSize: 14.sp, - height: 1.4, - fontWeight: FontWeight.w400, - ), - ), - SizedBox(height: 12.h), - Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - gradient: _getWeatherGradient(now['text_day'] ?? '', isDarkMode), - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: isDarkMode ? 0.3 : 0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${city} ${now["date"]}", - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - SizedBox(height: 4.w), - Text( - _getWeatherDescriptionTranslation(now["text_day"]), - style: TextStyle( - fontSize: 14.sp, - color: Colors.white.withValues(alpha: 0.9), - ), - ), - SizedBox(height: 8.h), - Row( - children: [ - Text( - "${now["high"]}°", - style: TextStyle( - fontSize: 36.sp, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - SizedBox(width: 8.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${'highest'.tr}: ${now["high"]}°', // 翻译键代表的中文文本内容:最高 - style: TextStyle( - fontSize: 12.sp, - color: Colors.white.withValues(alpha: 0.9), - ), - ), - Text( - '${'lowest'.tr}: ${now["low"]}°', // 翻译键代表的中文文本内容:最低 - style: TextStyle( - fontSize: 12.sp, - color: Colors.white.withValues(alpha: 0.9), - ), - ), - ], - ), - ], - ), - ], - ), - Icon( - getWeatherIcon(now['text_day']), - size: 48.sp, - color: Colors.white, - ), - ], - ), - SizedBox(height: 16.h), - Container( - height: 1, - color: Colors.white.withValues(alpha: isDarkMode ? 0.15 : 0.2), - ), - SizedBox(height: 16.h), - Text( - 'futureForecast'.tr, // 翻译键代表的中文文本内容:未来预报 - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - SizedBox(height: 8.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate( - days.length, - (index) { - String dateStr = days[index]['date']; - DateTime? date = DateUtil.getDateTime(dateStr); - IconData weatherIcon = - getWeatherIcon(days[index]['text_day']); - - // 获取星期几的翻译键 - String weekdayKey = _getWeekdayTranslationKey(date); - String weekday = weekdayKey.tr; // 翻译键代表的中文文本内容:对应的星期几 - - String temp = "${days[index]['high']}°"; - return _buildForecastDay( - weekday, - weatherIcon, - temp, - ); - }, - ), - ), - SizedBox(height: 16.h), - _buildWeatherTip(context, now, isDarkMode), - ], - ), - ), - ], - ); -} - -/// 获取天气描述的翻译 -String _getWeatherDescriptionTranslation(String weatherDescription) { - final description = weatherDescription.trim(); - - // 天气描述映射表 - final Map weatherMap = { - // 晴天类 - '晴': 'sunny'.tr, // 翻译键代表的中文文本内容:晴 - '少云': 'partlyCloudy'.tr, // 翻译键代表的中文文本内容:少云 - '晴间多云': 'partlySunny'.tr, // 翻译键代表的中文文本内容:晴间多云 - - // 多云类 - '多云': 'cloudy'.tr, // 翻译键代表的中文文本内容:多云 - '阴': 'overcast'.tr, // 翻译键代表的中文文本内容:阴 - - // 雨天类 - '阵雨': 'shower'.tr, // 翻译键代表的中文文本内容:阵雨 - '雷阵雨': 'thundershower'.tr, // 翻译键代表的中文文本内容:雷阵雨 - '小雨': 'lightRain'.tr, // 翻译键代表的中文文本内容:小雨 - '中雨': 'moderateRain'.tr, // 翻译键代表的中文文本内容:中雨 - '大雨': 'heavyRain'.tr, // 翻译键代表的中文文本内容:大雨 - '暴雨': 'rainstorm'.tr, // 翻译键代表的中文文本内容:暴雨 - - // 雪天类 - '阵雪': 'snowShower'.tr, // 翻译键代表的中文文本内容:阵雪 - '小雪': 'lightSnow'.tr, // 翻译键代表的中文文本内容:小雪 - '中雪': 'moderateSnow'.tr, // 翻译键代表的中文文本内容:中雪 - '大雪': 'heavySnow'.tr, // 翻译键代表的中文文本内容:大雪 - '暴雪': 'blizzard'.tr, // 翻译键代表的中文文本内容:暴雪 - - // 雾霾类 - '雾': 'fog'.tr, // 翻译键代表的中文文本内容:雾 - '霾': 'haze'.tr, // 翻译键代表的中文文本内容:霾 - - // 风类 - '大风': 'strongWind'.tr, // 翻译键代表的中文文本内容:大风 - }; - - // 返回翻译后的描述,如果没有找到对应翻译则返回原文 - return weatherMap[description] ?? description; -} - -/// 获取星期几的翻译键 -String _getWeekdayTranslationKey(DateTime? date) { - if (date == null) return 'unknown'; - - switch (date.weekday) { - case 1: - return 'monday'; // 翻译键代表的中文文本内容:周一 - case 2: - return 'tuesday'; // 翻译键代表的中文文本内容:周二 - case 3: - return 'wednesday'; // 翻译键代表的中文文本内容:周三 - case 4: - return 'thursday'; // 翻译键代表的中文文本内容:周四 - case 5: - return 'friday'; // 翻译键代表的中文文本内容:周五 - case 6: - return 'saturday'; // 翻译键代表的中文文本内容:周六 - case 7: - return 'sunday'; // 翻译键代表的中文文本内容:周日 - default: - return 'unknown'; - } -} - -/// 构建智能天气提示 -Widget _buildWeatherTip( - BuildContext context, Map now, bool isDarkMode) { - final weatherTip = _generateWeatherTip(now); - - return Container( - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 12.w), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.15) - : Colors.white.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(8.r), - ), - child: Row( - children: [ - Icon( - weatherTip.icon, - size: 16.sp, - color: Colors.white, - ), - SizedBox(width: 8.w), - Flexible( - child: Text( - weatherTip.message, - style: TextStyle( - fontSize: 12.sp, - color: Colors.white, - ), - ), - ), - ], - ), - ); -} - -/// 天气提示数据类 -class WeatherTip { - final IconData icon; - final String message; - - const WeatherTip({required this.icon, required this.message}); -} - -/// 根据天气数据生成智能提示 -WeatherTip _generateWeatherTip(Map now) { - final dayWeather = (now['dayweather'] as String? ?? '').trim(); - final dayTemp = int.tryParse(now['daytemp']?.toString() ?? '0') ?? 0; - final nightTemp = int.tryParse(now['nighttemp']?.toString() ?? '0') ?? 0; - - // 高温提示 (>= 35°C) - if (dayTemp >= 35) { - return WeatherTip( - icon: Icons.local_fire_department, - message: 'hotWeatherTip'.tr, // 翻译键代表的中文文本内容:今日高温炎热,请注意防暑降温,多喝水避免中暑 - ); - } - - // 低温提示 (<= 5°C) - if (dayTemp <= 5) { - return WeatherTip( - icon: Icons.ac_unit, - message: 'coldWeatherTip'.tr, // 翻译键代表的中文文本内容:今日气温较低,外出请注意保暖,小心感冒 - ); - } - - // 雨天提示 - if (dayWeather.contains('雨') || - ['阵雨', '雷阵雨', '小雨', '中雨', '大雨', '暴雨'] - .any((rain) => dayWeather.contains(rain))) { - return WeatherTip( - icon: Icons.umbrella, - message: 'rainyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日有雨,外出请携带雨具,注意出行安全 - ); - } - - // 雪天提示 - if (dayWeather.contains('雪') || - ['阵雪', '小雪', '中雪', '大雪', '暴雪'].any((snow) => dayWeather.contains(snow))) { - return WeatherTip( - icon: Icons.ac_unit, - message: 'snowyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日有雪,路面湿滑请注意安全,做好保暖措施 - ); - } - - // 雾霾提示 - if (dayWeather.contains('雾') || - dayWeather.contains('霾') || - ['雾', '浓雾', '霾', '中度霾', '重度霾'].any((fog) => dayWeather.contains(fog))) { - return WeatherTip( - icon: Icons.blur_on, - message: 'foggyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日有雾霾,能见度较低,外出请佩戴口罩 - ); - } - - // 大风提示 - if (dayWeather.contains('风') || - ['大风', '强风', '疾风', '烈风'].any((wind) => dayWeather.contains(wind))) { - return WeatherTip( - icon: Icons.air, - message: 'windyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日风力较大,外出请注意安全,固定好物品 - ); - } - - // 晴天紫外线提示 (温度 >= 25°C) - if (['晴', '少云', '晴间多云'].any((sunny) => dayWeather.contains(sunny)) && - dayTemp >= 25) { - return WeatherTip( - icon: Icons.wb_sunny, - message: 'sunnyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日阳光充足紫外线较强,外出请做好防晒 - ); - } - - // 温差大提示 (>= 10°C) - if ((dayTemp - nightTemp).abs() >= 10) { - return WeatherTip( - icon: Icons.thermostat, - message: 'temperatureDifferenceTip'.tr, // 翻译键代表的中文文本内容:今日昼夜温差较大,请适时增减衣物 - ); - } - - // 默认舒适天气提示 - return WeatherTip( - icon: Icons.wb_sunny_outlined, - message: 'comfortableWeatherTip'.tr, // 翻译键代表的中文文本内容:今日天气适宜,是外出活动的好时光 - ); -} - -/// 构建单日天气预报 -Widget _buildForecastDay(String day, IconData icon, String temp) { - return Column( - children: [ - Text( - day, - style: TextStyle( - fontSize: 12.sp, - color: Colors.white.withValues(alpha: 0.9), - ), - ), - SizedBox(height: 4.h), - Icon( - icon, - size: 20.sp, - color: Colors.white, - ), - SizedBox(height: 4.h), - Text( - temp, - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w500, - color: Colors.white, - ), - ), - ], - ); -} - -/// 获取天气图标 -IconData getWeatherIcon(String description) { - description = description.trim(); - - if (["晴", "少云", "晴间多云"].contains(description)) { - return Icons.wb_sunny; - } - - if (["多云", "阴"].contains(description)) { - return Icons.cloud; - } - - if (description.contains("雾") || - ["雾", "浓雾", "强浓雾", "轻雾", "大雾", "特强浓雾", "霾", "中度霾", "重度霾", "严重霾"] - .contains(description)) { - return Icons.blur_on; - } - - if (description.contains("风") || - [ - "有风", - "平静", - "微风", - "和风", - "清风", - "强风/劲风", - "疾风", - "大风", - "烈风", - "风暴", - "狂爆风", - "飓风", - "热带风暴" - ].contains(description)) { - return Icons.air; - } - - if (description.contains("雨") || - [ - "阵雨", - "雷阵雨", - "雷阵雨并伴有冰雹", - "小雨", - "中雨", - "大雨", - "暴雨", - "大暴雨", - "特大暴雨", - "强阵雨", - "强雷阵雨", - "极端降雨", - "毛毛雨/细雨", - "雨", - "小雨-中雨", - "中雨-大雨", - "大雨-暴雨", - "暴雨-大暴雨", - "大暴雨-特大暴雨" - ].contains(description)) { - return Icons.grain; // 或使用 community icon 包:weather_icons 中的 rain - } - - if (description.contains("雪") || - [ - "雨雪天气", - "雨夹雪", - "阵雨夹雪", - "冻雨", - "雪", - "阵雪", - "小雪", - "中雪", - "大雪", - "暴雪", - "小雪-中雪", - "中雪-大雪", - "大雪-暴雪" - ].contains(description)) { - return Icons.ac_unit; - } - - if (["浮尘", "扬沙", "沙尘暴", "强沙尘暴"].contains(description)) { - return Icons.filter_drama; - } - - if (["龙卷风"].contains(description)) { - return Icons.waves; - } - - if (["热"].contains(description)) { - return Icons.local_fire_department; - } - - if (["冷"].contains(description)) { - return Icons.ac_unit; - } - - // 默认或未知天气 - return Icons.help_outline; -} - -/// 根据天气类型获取背景渐变颜色 -LinearGradient _getWeatherGradient(String weather, bool isDarkMode) { - final weatherType = weather.trim(); - - // 晴天 - 温暖的橙黄色 - if (['晴', '少云', '晴间多云'].any((sunny) => weatherType.contains(sunny))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFFFF8A00), Color(0xFFE65100)] // 深色模式:深橙色 - : [Color(0xFFFFB74D), Color(0xFFFF9800)], // 浅色模式:亮橙色 - ); - } - - // 多云/阴天 - 灰蓝色 - if (['多云', '阴'].any((cloudy) => weatherType.contains(cloudy))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF455A64), Color(0xFF37474F)] // 深色模式:深灰蓝 - : [Color(0xFF78909C), Color(0xFF607D8B)], // 浅色模式:灰蓝色 - ); - } - - // 雨天 - 深蓝色 - if (weatherType.contains('雨') || - ['阵雨', '雷阵雨', '小雨', '中雨', '大雨', '暴雨'] - .any((rain) => weatherType.contains(rain))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF1565C0), Color(0xFF0D47A1)] // 深色模式:深蓝色 - : [Color(0xFF42A5F5), Color(0xFF1976D2)], // 浅色模式:亮蓝色 - ); - } - - // 雪天 - 冷色调蓝白色 - if (weatherType.contains('雪') || - ['阵雪', '小雪', '中雪', '大雪', '暴雪'] - .any((snow) => weatherType.contains(snow))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF0277BD), Color(0xFF01579B)] // 深色模式:深冷蓝 - : [Color(0xFF81D4FA), Color(0xFF29B6F6)], // 浅色模式:浅蓝色 - ); - } - - // 雾霾天 - 暗黄灰色 - if (weatherType.contains('雾') || - weatherType.contains('霾') || - ['雾', '浓雾', '霾', '中度霾', '重度霾'].any((fog) => weatherType.contains(fog))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF5D4037), Color(0xFF3E2723)] // 深色模式:深棕色 - : [Color(0xFF8D6E63), Color(0xFF6D4C41)], // 浅色模式:棕灰色 - ); - } - - // 大风天 - 青绿色 - if (weatherType.contains('风') || - ['大风', '强风', '疾风', '烈风'].any((wind) => weatherType.contains(wind))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF00695C), Color(0xFF004D40)] // 深色模式:深青色 - : [Color(0xFF26A69A), Color(0xFF00796B)], // 浅色模式:青绿色 - ); - } - - // 默认 - 原来的蓝色 - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF1E40AF), Color(0xFF1E3A8A)] - : [Color(0xFF60A5FA), Color(0xFF3B82F6)], - ); -} diff --git a/apps/client/lib/modules/agent/views/weather_card.dart b/apps/client/lib/modules/agent/views/weather_card.dart deleted file mode 100644 index ce502699..00000000 --- a/apps/client/lib/modules/agent/views/weather_card.dart +++ /dev/null @@ -1,562 +0,0 @@ -import 'package:common_utils/common_utils.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -/// 构建天气内容卡片 -Widget buildWeatherContent(BuildContext context, String city, - Map now, List> days) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'weatherQuery'.tr, // 翻译键代表的中文文本内容:查询天气情况: - style: TextStyle( - color: - isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.black87, - fontSize: 14.sp, - height: 1.4, - fontWeight: FontWeight.w400, - ), - ), - SizedBox(height: 12.h), - Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - gradient: _getWeatherGradient(now['dayweather'] ?? '', isDarkMode), - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: isDarkMode ? 0.3 : 0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${city} ${now["date"]}", - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - SizedBox(height: 4.w), - Text( - _getWeatherDescriptionTranslation(now["dayweather"]), - style: TextStyle( - fontSize: 14.sp, - color: Colors.white.withValues(alpha: 0.9), - ), - ), - SizedBox(height: 8.h), - Row( - children: [ - Text( - "${now["daytemp"]}°", - style: TextStyle( - fontSize: 36.sp, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - SizedBox(width: 8.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${'highest'.tr}: ${now["daytemp"]}°', // 翻译键代表的中文文本内容:最高 - style: TextStyle( - fontSize: 12.sp, - color: Colors.white.withValues(alpha: 0.9), - ), - ), - Text( - '${'lowest'.tr}: ${now["nighttemp"]}°', // 翻译键代表的中文文本内容:最低 - style: TextStyle( - fontSize: 12.sp, - color: Colors.white.withValues(alpha: 0.9), - ), - ), - ], - ), - ], - ), - ], - ), - Icon( - getWeatherIcon(now['dayweather']), - size: 48.sp, - color: Colors.white, - ), - ], - ), - SizedBox(height: 16.h), - Container( - height: 1, - color: Colors.white.withValues(alpha: isDarkMode ? 0.15 : 0.2), - ), - SizedBox(height: 16.h), - Text( - 'futureForecast'.tr, // 翻译键代表的中文文本内容:未来预报 - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - SizedBox(height: 8.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate( - days.length, - (index) { - String dateStr = days[index]['date']; - DateTime? date = DateUtil.getDateTime(dateStr); - IconData weatherIcon = - getWeatherIcon(days[index]['dayweather']); - - // 获取星期几的翻译键 - String weekdayKey = _getWeekdayTranslationKey(date); - String weekday = weekdayKey.tr; // 翻译键代表的中文文本内容:对应的星期几 - - String temp = "${days[index]['daytemp']}°"; - return _buildForecastDay( - weekday, - weatherIcon, - temp, - ); - }, - ), - ), - SizedBox(height: 16.h), - _buildWeatherTip(context, now, isDarkMode), - ], - ), - ), - ], - ); -} - -/// 获取天气描述的翻译 -String _getWeatherDescriptionTranslation(String weatherDescription) { - final description = weatherDescription.trim(); - - // 天气描述映射表 - final Map weatherMap = { - // 晴天类 - '晴': 'sunny'.tr, // 翻译键代表的中文文本内容:晴 - '少云': 'partlyCloudy'.tr, // 翻译键代表的中文文本内容:少云 - '晴间多云': 'partlySunny'.tr, // 翻译键代表的中文文本内容:晴间多云 - - // 多云类 - '多云': 'cloudy'.tr, // 翻译键代表的中文文本内容:多云 - '阴': 'overcast'.tr, // 翻译键代表的中文文本内容:阴 - - // 雨天类 - '阵雨': 'shower'.tr, // 翻译键代表的中文文本内容:阵雨 - '雷阵雨': 'thundershower'.tr, // 翻译键代表的中文文本内容:雷阵雨 - '小雨': 'lightRain'.tr, // 翻译键代表的中文文本内容:小雨 - '中雨': 'moderateRain'.tr, // 翻译键代表的中文文本内容:中雨 - '大雨': 'heavyRain'.tr, // 翻译键代表的中文文本内容:大雨 - '暴雨': 'rainstorm'.tr, // 翻译键代表的中文文本内容:暴雨 - - // 雪天类 - '阵雪': 'snowShower'.tr, // 翻译键代表的中文文本内容:阵雪 - '小雪': 'lightSnow'.tr, // 翻译键代表的中文文本内容:小雪 - '中雪': 'moderateSnow'.tr, // 翻译键代表的中文文本内容:中雪 - '大雪': 'heavySnow'.tr, // 翻译键代表的中文文本内容:大雪 - '暴雪': 'blizzard'.tr, // 翻译键代表的中文文本内容:暴雪 - - // 雾霾类 - '雾': 'fog'.tr, // 翻译键代表的中文文本内容:雾 - '霾': 'haze'.tr, // 翻译键代表的中文文本内容:霾 - - // 风类 - '大风': 'strongWind'.tr, // 翻译键代表的中文文本内容:大风 - }; - - // 返回翻译后的描述,如果没有找到对应翻译则返回原文 - return weatherMap[description] ?? description; -} - -/// 获取星期几的翻译键 -String _getWeekdayTranslationKey(DateTime? date) { - if (date == null) return 'unknown'; - - switch (date.weekday) { - case 1: - return 'monday'; // 翻译键代表的中文文本内容:周一 - case 2: - return 'tuesday'; // 翻译键代表的中文文本内容:周二 - case 3: - return 'wednesday'; // 翻译键代表的中文文本内容:周三 - case 4: - return 'thursday'; // 翻译键代表的中文文本内容:周四 - case 5: - return 'friday'; // 翻译键代表的中文文本内容:周五 - case 6: - return 'saturday'; // 翻译键代表的中文文本内容:周六 - case 7: - return 'sunday'; // 翻译键代表的中文文本内容:周日 - default: - return 'unknown'; - } -} - -/// 构建智能天气提示 -Widget _buildWeatherTip( - BuildContext context, Map now, bool isDarkMode) { - final weatherTip = _generateWeatherTip(now); - - return Container( - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 12.w), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.15) - : Colors.white.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(8.r), - ), - child: Row( - children: [ - Icon( - weatherTip.icon, - size: 16.sp, - color: Colors.white, - ), - SizedBox(width: 8.w), - Flexible( - child: Text( - weatherTip.message, - style: TextStyle( - fontSize: 12.sp, - color: Colors.white, - ), - ), - ), - ], - ), - ); -} - -/// 天气提示数据类 -class WeatherTip { - final IconData icon; - final String message; - - const WeatherTip({required this.icon, required this.message}); -} - -/// 根据天气数据生成智能提示 -WeatherTip _generateWeatherTip(Map now) { - final dayWeather = (now['dayweather'] as String? ?? '').trim(); - final dayTemp = int.tryParse(now['daytemp']?.toString() ?? '0') ?? 0; - final nightTemp = int.tryParse(now['nighttemp']?.toString() ?? '0') ?? 0; - - // 高温提示 (>= 35°C) - if (dayTemp >= 35) { - return WeatherTip( - icon: Icons.local_fire_department, - message: 'hotWeatherTip'.tr, // 翻译键代表的中文文本内容:今日高温炎热,请注意防暑降温,多喝水避免中暑 - ); - } - - // 低温提示 (<= 5°C) - if (dayTemp <= 5) { - return WeatherTip( - icon: Icons.ac_unit, - message: 'coldWeatherTip'.tr, // 翻译键代表的中文文本内容:今日气温较低,外出请注意保暖,小心感冒 - ); - } - - // 雨天提示 - if (dayWeather.contains('雨') || - ['阵雨', '雷阵雨', '小雨', '中雨', '大雨', '暴雨'] - .any((rain) => dayWeather.contains(rain))) { - return WeatherTip( - icon: Icons.umbrella, - message: 'rainyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日有雨,外出请携带雨具,注意出行安全 - ); - } - - // 雪天提示 - if (dayWeather.contains('雪') || - ['阵雪', '小雪', '中雪', '大雪', '暴雪'].any((snow) => dayWeather.contains(snow))) { - return WeatherTip( - icon: Icons.ac_unit, - message: 'snowyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日有雪,路面湿滑请注意安全,做好保暖措施 - ); - } - - // 雾霾提示 - if (dayWeather.contains('雾') || - dayWeather.contains('霾') || - ['雾', '浓雾', '霾', '中度霾', '重度霾'].any((fog) => dayWeather.contains(fog))) { - return WeatherTip( - icon: Icons.blur_on, - message: 'foggyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日有雾霾,能见度较低,外出请佩戴口罩 - ); - } - - // 大风提示 - if (dayWeather.contains('风') || - ['大风', '强风', '疾风', '烈风'].any((wind) => dayWeather.contains(wind))) { - return WeatherTip( - icon: Icons.air, - message: 'windyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日风力较大,外出请注意安全,固定好物品 - ); - } - - // 晴天紫外线提示 (温度 >= 25°C) - if (['晴', '少云', '晴间多云'].any((sunny) => dayWeather.contains(sunny)) && - dayTemp >= 25) { - return WeatherTip( - icon: Icons.wb_sunny, - message: 'sunnyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日阳光充足紫外线较强,外出请做好防晒 - ); - } - - // 温差大提示 (>= 10°C) - if ((dayTemp - nightTemp).abs() >= 10) { - return WeatherTip( - icon: Icons.thermostat, - message: 'temperatureDifferenceTip'.tr, // 翻译键代表的中文文本内容:今日昼夜温差较大,请适时增减衣物 - ); - } - - // 默认舒适天气提示 - return WeatherTip( - icon: Icons.wb_sunny_outlined, - message: 'comfortableWeatherTip'.tr, // 翻译键代表的中文文本内容:今日天气适宜,是外出活动的好时光 - ); -} - -/// 构建单日天气预报 -Widget _buildForecastDay(String day, IconData icon, String temp) { - return Column( - children: [ - Text( - day, - style: TextStyle( - fontSize: 12.sp, - color: Colors.white.withValues(alpha: 0.9), - ), - ), - SizedBox(height: 4.h), - Icon( - icon, - size: 20.sp, - color: Colors.white, - ), - SizedBox(height: 4.h), - Text( - temp, - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w500, - color: Colors.white, - ), - ), - ], - ); -} - -/// 获取天气图标 -IconData getWeatherIcon(String description) { - description = description.trim(); - - if (["晴", "少云", "晴间多云"].contains(description)) { - return Icons.wb_sunny; - } - - if (["多云", "阴"].contains(description)) { - return Icons.cloud; - } - - if (description.contains("雾") || - ["雾", "浓雾", "强浓雾", "轻雾", "大雾", "特强浓雾", "霾", "中度霾", "重度霾", "严重霾"] - .contains(description)) { - return Icons.blur_on; - } - - if (description.contains("风") || - [ - "有风", - "平静", - "微风", - "和风", - "清风", - "强风/劲风", - "疾风", - "大风", - "烈风", - "风暴", - "狂爆风", - "飓风", - "热带风暴" - ].contains(description)) { - return Icons.air; - } - - if (description.contains("雨") || - [ - "阵雨", - "雷阵雨", - "雷阵雨并伴有冰雹", - "小雨", - "中雨", - "大雨", - "暴雨", - "大暴雨", - "特大暴雨", - "强阵雨", - "强雷阵雨", - "极端降雨", - "毛毛雨/细雨", - "雨", - "小雨-中雨", - "中雨-大雨", - "大雨-暴雨", - "暴雨-大暴雨", - "大暴雨-特大暴雨" - ].contains(description)) { - return Icons.grain; // 或使用 community icon 包:weather_icons 中的 rain - } - - if (description.contains("雪") || - [ - "雨雪天气", - "雨夹雪", - "阵雨夹雪", - "冻雨", - "雪", - "阵雪", - "小雪", - "中雪", - "大雪", - "暴雪", - "小雪-中雪", - "中雪-大雪", - "大雪-暴雪" - ].contains(description)) { - return Icons.ac_unit; - } - - if (["浮尘", "扬沙", "沙尘暴", "强沙尘暴"].contains(description)) { - return Icons.filter_drama; - } - - if (["龙卷风"].contains(description)) { - return Icons.waves; - } - - if (["热"].contains(description)) { - return Icons.local_fire_department; - } - - if (["冷"].contains(description)) { - return Icons.ac_unit; - } - - // 默认或未知天气 - return Icons.help_outline; -} - -/// 根据天气类型获取背景渐变颜色 -LinearGradient _getWeatherGradient(String weather, bool isDarkMode) { - final weatherType = weather.trim(); - - // 晴天 - 温暖的橙黄色 - if (['晴', '少云', '晴间多云'].any((sunny) => weatherType.contains(sunny))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFFFF8A00), Color(0xFFE65100)] // 深色模式:深橙色 - : [Color(0xFFFFB74D), Color(0xFFFF9800)], // 浅色模式:亮橙色 - ); - } - - // 多云/阴天 - 灰蓝色 - if (['多云', '阴'].any((cloudy) => weatherType.contains(cloudy))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF455A64), Color(0xFF37474F)] // 深色模式:深灰蓝 - : [Color(0xFF78909C), Color(0xFF607D8B)], // 浅色模式:灰蓝色 - ); - } - - // 雨天 - 深蓝色 - if (weatherType.contains('雨') || - ['阵雨', '雷阵雨', '小雨', '中雨', '大雨', '暴雨'] - .any((rain) => weatherType.contains(rain))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF1565C0), Color(0xFF0D47A1)] // 深色模式:深蓝色 - : [Color(0xFF42A5F5), Color(0xFF1976D2)], // 浅色模式:亮蓝色 - ); - } - - // 雪天 - 冷色调蓝白色 - if (weatherType.contains('雪') || - ['阵雪', '小雪', '中雪', '大雪', '暴雪'] - .any((snow) => weatherType.contains(snow))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF0277BD), Color(0xFF01579B)] // 深色模式:深冷蓝 - : [Color(0xFF81D4FA), Color(0xFF29B6F6)], // 浅色模式:浅蓝色 - ); - } - - // 雾霾天 - 暗黄灰色 - if (weatherType.contains('雾') || - weatherType.contains('霾') || - ['雾', '浓雾', '霾', '中度霾', '重度霾'].any((fog) => weatherType.contains(fog))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF5D4037), Color(0xFF3E2723)] // 深色模式:深棕色 - : [Color(0xFF8D6E63), Color(0xFF6D4C41)], // 浅色模式:棕灰色 - ); - } - - // 大风天 - 青绿色 - if (weatherType.contains('风') || - ['大风', '强风', '疾风', '烈风'].any((wind) => weatherType.contains(wind))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF00695C), Color(0xFF004D40)] // 深色模式:深青色 - : [Color(0xFF26A69A), Color(0xFF00796B)], // 浅色模式:青绿色 - ); - } - - // 默认 - 原来的蓝色 - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF1E40AF), Color(0xFF1E3A8A)] - : [Color(0xFF60A5FA), Color(0xFF3B82F6)], - ); -} diff --git a/apps/client/lib/modules/agent/views/weather_hefeng.dart b/apps/client/lib/modules/agent/views/weather_hefeng.dart deleted file mode 100644 index 537e715f..00000000 --- a/apps/client/lib/modules/agent/views/weather_hefeng.dart +++ /dev/null @@ -1,481 +0,0 @@ -import 'package:common_utils/common_utils.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; - -/// 构建天气内容卡片 -Widget hefengWeatherContent(BuildContext context, String city, - Map now, List> days) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'weatherQuery'.tr, // 翻译键代表的中文文本内容:查询天气情况: - style: TextStyle( - color: - isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.black87, - fontSize: 14.sp, - height: 1.4, - fontWeight: FontWeight.w400, - ), - ), - SizedBox(height: 12.h), - Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - gradient: _getWeatherGradient(now['textDay'] ?? '', isDarkMode), - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: isDarkMode ? 0.3 : 0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${city} ${now["fxDate"]}", - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - SizedBox(height: 4.w), - Text( - _getWeatherDescriptionTranslation(now["textDay"]), - style: TextStyle( - fontSize: 14.sp, - color: Colors.white.withValues(alpha: 0.9), - ), - ), - SizedBox(height: 8.h), - Row( - children: [ - Text( - "${now["tempMax"]}°", - style: TextStyle( - fontSize: 36.sp, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - SizedBox(width: 8.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${'highest'.tr}: ${now["tempMax"]}°', // 翻译键代表的中文文本内容:最高 - style: TextStyle( - fontSize: 12.sp, - color: Colors.white.withValues(alpha: 0.9), - ), - ), - Text( - '${'lowest'.tr}: ${now["tempMin"]}°', // 翻译键代表的中文文本内容:最低 - style: TextStyle( - fontSize: 12.sp, - color: Colors.white.withValues(alpha: 0.9), - ), - ), - ], - ), - ], - ), - ], - ), - getWeatherSvgIcon( - now['iconDay'], - size: 48.sp, - ), - ], - ), - SizedBox(height: 16.h), - Container( - height: 1, - color: Colors.white.withValues(alpha: isDarkMode ? 0.15 : 0.2), - ), - SizedBox(height: 16.h), - Text( - 'futureForecast'.tr, // 翻译键代表的中文文本内容:未来预报 - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - SizedBox(height: 8.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate( - days.length, - (index) { - String dateStr = days[index]['fxDate']; - DateTime? date = DateUtil.getDateTime(dateStr); - Widget weatherIcon = - getWeatherSvgIcon(days[index]['iconDay'], size: 20.sp); - - // 获取星期几的翻译键 - String weekdayKey = _getWeekdayTranslationKey(date); - String weekday = weekdayKey.tr; // 翻译键代表的中文文本内容:对应的星期几 - - String temp = "${days[index]['tempMax']}°"; - return _buildForecastDay( - weekday, - weatherIcon, - temp, - ); - }, - ), - ), - SizedBox(height: 16.h), - _hefengWeatherTip(context, now, isDarkMode), - ], - ), - ), - ], - ); -} - -/// 获取天气描述的翻译 -String _getWeatherDescriptionTranslation(String weatherDescription) { - final description = weatherDescription.trim(); - - // 天气描述映射表 - final Map weatherMap = { - // 晴天类 - '晴': 'sunny'.tr, // 翻译键代表的中文文本内容:晴 - '少云': 'partlyCloudy'.tr, // 翻译键代表的中文文本内容:少云 - '晴间多云': 'partlySunny'.tr, // 翻译键代表的中文文本内容:晴间多云 - - // 多云类 - '多云': 'cloudy'.tr, // 翻译键代表的中文文本内容:多云 - '阴': 'overcast'.tr, // 翻译键代表的中文文本内容:阴 - - // 雨天类 - '阵雨': 'shower'.tr, // 翻译键代表的中文文本内容:阵雨 - '雷阵雨': 'thundershower'.tr, // 翻译键代表的中文文本内容:雷阵雨 - '小雨': 'lightRain'.tr, // 翻译键代表的中文文本内容:小雨 - '中雨': 'moderateRain'.tr, // 翻译键代表的中文文本内容:中雨 - '大雨': 'heavyRain'.tr, // 翻译键代表的中文文本内容:大雨 - '暴雨': 'rainstorm'.tr, // 翻译键代表的中文文本内容:暴雨 - - // 雪天类 - '阵雪': 'snowShower'.tr, // 翻译键代表的中文文本内容:阵雪 - '小雪': 'lightSnow'.tr, // 翻译键代表的中文文本内容:小雪 - '中雪': 'moderateSnow'.tr, // 翻译键代表的中文文本内容:中雪 - '大雪': 'heavySnow'.tr, // 翻译键代表的中文文本内容:大雪 - '暴雪': 'blizzard'.tr, // 翻译键代表的中文文本内容:暴雪 - - // 雾霾类 - '雾': 'fog'.tr, // 翻译键代表的中文文本内容:雾 - '霾': 'haze'.tr, // 翻译键代表的中文文本内容:霾 - - // 风类 - '大风': 'strongWind'.tr, // 翻译键代表的中文文本内容:大风 - }; - - // 返回翻译后的描述,如果没有找到对应翻译则返回原文 - return weatherMap[description] ?? description; -} - -/// 获取星期几的翻译键 -String _getWeekdayTranslationKey(DateTime? date) { - if (date == null) return 'unknown'; - - switch (date.weekday) { - case 1: - return 'monday'; // 翻译键代表的中文文本内容:周一 - case 2: - return 'tuesday'; // 翻译键代表的中文文本内容:周二 - case 3: - return 'wednesday'; // 翻译键代表的中文文本内容:周三 - case 4: - return 'thursday'; // 翻译键代表的中文文本内容:周四 - case 5: - return 'friday'; // 翻译键代表的中文文本内容:周五 - case 6: - return 'saturday'; // 翻译键代表的中文文本内容:周六 - case 7: - return 'sunday'; // 翻译键代表的中文文本内容:周日 - default: - return 'unknown'; - } -} - -/// 构建智能天气提示 -Widget _hefengWeatherTip( - BuildContext context, Map now, bool isDarkMode) { - final weatherTip = _generateWeatherTip(now); - - return Container( - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 12.w), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.15) - : Colors.white.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(8.r), - ), - child: Row( - children: [ - Icon( - weatherTip.icon, - size: 16.sp, - color: Colors.white, - ), - SizedBox(width: 8.w), - Flexible( - child: Text( - weatherTip.message, - style: TextStyle( - fontSize: 12.sp, - color: Colors.white, - ), - ), - ), - ], - ), - ); -} - -/// 天气提示数据类 -class WeatherTip { - final IconData icon; - final String message; - - const WeatherTip({required this.icon, required this.message}); -} - -/// 根据天气数据生成智能提示 -WeatherTip _generateWeatherTip(Map now) { - final dayWeather = (now['textDay'] as String? ?? '').trim(); - final dayTemp = int.tryParse(now['tempMax']?.toString() ?? '0') ?? 0; - final nightTemp = int.tryParse(now['tempMin']?.toString() ?? '0') ?? 0; - - // 高温提示 (>= 35°C) - if (dayTemp >= 35) { - return WeatherTip( - icon: Icons.local_fire_department, - message: 'hotWeatherTip'.tr, // 翻译键代表的中文文本内容:今日高温炎热,请注意防暑降温,多喝水避免中暑 - ); - } - - // 低温提示 (<= 5°C) - if (dayTemp <= 5) { - return WeatherTip( - icon: Icons.ac_unit, - message: 'coldWeatherTip'.tr, // 翻译键代表的中文文本内容:今日气温较低,外出请注意保暖,小心感冒 - ); - } - - // 雨天提示 - if (dayWeather.contains('雨') || - ['阵雨', '雷阵雨', '小雨', '中雨', '大雨', '暴雨'] - .any((rain) => dayWeather.contains(rain))) { - return WeatherTip( - icon: Icons.umbrella, - message: 'rainyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日有雨,外出请携带雨具,注意出行安全 - ); - } - - // 雪天提示 - if (dayWeather.contains('雪') || - ['阵雪', '小雪', '中雪', '大雪', '暴雪'].any((snow) => dayWeather.contains(snow))) { - return WeatherTip( - icon: Icons.ac_unit, - message: 'snowyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日有雪,路面湿滑请注意安全,做好保暖措施 - ); - } - - // 雾霾提示 - if (dayWeather.contains('雾') || - dayWeather.contains('霾') || - ['雾', '浓雾', '霾', '中度霾', '重度霾'].any((fog) => dayWeather.contains(fog))) { - return WeatherTip( - icon: Icons.blur_on, - message: 'foggyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日有雾霾,能见度较低,外出请佩戴口罩 - ); - } - - // 大风提示 - if (dayWeather.contains('风') || - ['大风', '强风', '疾风', '烈风'].any((wind) => dayWeather.contains(wind))) { - return WeatherTip( - icon: Icons.air, - message: 'windyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日风力较大,外出请注意安全,固定好物品 - ); - } - - // 晴天紫外线提示 (温度 >= 25°C) - if (['晴', '少云', '晴间多云'].any((sunny) => dayWeather.contains(sunny)) && - dayTemp >= 25) { - return WeatherTip( - icon: Icons.wb_sunny, - message: 'sunnyWeatherTip'.tr, // 翻译键代表的中文文本内容:今日阳光充足紫外线较强,外出请做好防晒 - ); - } - - // 温差大提示 (>= 10°C) - if ((dayTemp - nightTemp).abs() >= 10) { - return WeatherTip( - icon: Icons.thermostat, - message: 'temperatureDifferenceTip'.tr, // 翻译键代表的中文文本内容:今日昼夜温差较大,请适时增减衣物 - ); - } - - // 默认舒适天气提示 - return WeatherTip( - icon: Icons.wb_sunny_outlined, - message: 'comfortableWeatherTip'.tr, // 翻译键代表的中文文本内容:今日天气适宜,是外出活动的好时光 - ); -} - -/// 构建单日天气预报 -Widget _buildForecastDay(String day, Widget icon, String temp) { - return Column( - children: [ - Text( - day, - style: TextStyle( - fontSize: 12.sp, - color: Colors.white.withValues(alpha: 0.9), - ), - ), - SizedBox(height: 4.h), - icon, - SizedBox(height: 4.h), - Text( - temp, - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w500, - color: Colors.white, - ), - ), - ], - ); -} - -/// 获取天气 SVG 图标 Widget -Widget getWeatherSvgIcon(String mappedCode, {double size = 24.0}) { - return _getWeatherSvgByCode(mappedCode, size: size); -} - -/// 根据和风天气代码获取 SVG 图标 -Widget _getWeatherSvgByCode(String code, {double size = 24.0}) { - // 检查 SVG 文件是否存在,如果不存在则使用默认图标 - final svgPath = 'assets/weather/$code.svg'; - - return SvgPicture.asset( - svgPath, - width: size, - height: size, - fit: BoxFit.contain, - colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - // 如果 SVG 加载失败,显示默认图标 - placeholderBuilder: (context) => Icon( - Icons.help_outline, - size: size, - ), - ); -} - -/// 根据天气类型获取背景渐变颜色 -LinearGradient _getWeatherGradient(String weather, bool isDarkMode) { - final weatherType = weather.trim(); - - // 晴天 - 温暖的橙黄色 - if (['晴', '少云', '晴间多云'].any((sunny) => weatherType.contains(sunny))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFFFF8A00), Color(0xFFE65100)] // 深色模式:深橙色 - : [Color(0xFFFFB74D), Color(0xFFFF9800)], // 浅色模式:亮橙色 - ); - } - - // 多云/阴天 - 灰蓝色 - if (['多云', '阴'].any((cloudy) => weatherType.contains(cloudy))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF455A64), Color(0xFF37474F)] // 深色模式:深灰蓝 - : [Color(0xFF78909C), Color(0xFF607D8B)], // 浅色模式:灰蓝色 - ); - } - - // 雨天 - 深蓝色 - if (weatherType.contains('雨') || - ['阵雨', '雷阵雨', '小雨', '中雨', '大雨', '暴雨'] - .any((rain) => weatherType.contains(rain))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF1565C0), Color(0xFF0D47A1)] // 深色模式:深蓝色 - : [Color(0xFF42A5F5), Color(0xFF1976D2)], // 浅色模式:亮蓝色 - ); - } - - // 雪天 - 冷色调蓝白色 - if (weatherType.contains('雪') || - ['阵雪', '小雪', '中雪', '大雪', '暴雪'] - .any((snow) => weatherType.contains(snow))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF0277BD), Color(0xFF01579B)] // 深色模式:深冷蓝 - : [Color(0xFF81D4FA), Color(0xFF29B6F6)], // 浅色模式:浅蓝色 - ); - } - - // 雾霾天 - 暗黄灰色 - if (weatherType.contains('雾') || - weatherType.contains('霾') || - ['雾', '浓雾', '霾', '中度霾', '重度霾'].any((fog) => weatherType.contains(fog))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF5D4037), Color(0xFF3E2723)] // 深色模式:深棕色 - : [Color(0xFF8D6E63), Color(0xFF6D4C41)], // 浅色模式:棕灰色 - ); - } - - // 大风天 - 青绿色 - if (weatherType.contains('风') || - ['大风', '强风', '疾风', '烈风'].any((wind) => weatherType.contains(wind))) { - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF00695C), Color(0xFF004D40)] // 深色模式:深青色 - : [Color(0xFF26A69A), Color(0xFF00796B)], // 浅色模式:青绿色 - ); - } - - // 默认 - 原来的蓝色 - return LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [Color(0xFF1E40AF), Color(0xFF1E3A8A)] - : [Color(0xFF60A5FA), Color(0xFF3B82F6)], - ); -} diff --git a/apps/client/lib/modules/agent/widgets/circular_progress_button.dart b/apps/client/lib/modules/agent/widgets/circular_progress_button.dart deleted file mode 100644 index 28729d23..00000000 --- a/apps/client/lib/modules/agent/widgets/circular_progress_button.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:flutter/material.dart'; -import 'dart:math' as math; - -/// 环形进度条播放按钮组件 -class CircularProgressButton extends StatelessWidget { - final bool isPlaying; - final double progress; // 0.0 - 1.0 - final VoidCallback onPlayPause; - final Function(double)? onSeek; // 点击进度条跳转回调 - final double size; - final Color primaryColor; - final Color backgroundColor; - final double strokeWidth; - - const CircularProgressButton({ - Key? key, - required this.isPlaying, - required this.progress, - required this.onPlayPause, - this.onSeek, - this.size = 36.0, - required this.primaryColor, - this.backgroundColor = Colors.grey, - this.strokeWidth = 2.0, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - // 只保留播放/暂停功能,移除进度条点击交互 - onPlayPause(); - }, - child: SizedBox( - width: size, - height: size, - child: Stack( - alignment: Alignment.center, - children: [ - // 背景圆环 - CustomPaint( - size: Size(size, size), - painter: _CircularProgressPainter( - progress: 1.0, - color: backgroundColor.withValues(alpha: 0.3), - strokeWidth: strokeWidth, - ), - ), - // 进度圆环 - CustomPaint( - size: Size(size, size), - painter: _CircularProgressPainter( - progress: progress, - color: primaryColor, - strokeWidth: strokeWidth, - ), - ), - // 播放/暂停按钮 - Icon( - isPlaying ? Icons.pause : Icons.play_arrow, - color: primaryColor, - size: size * 0.5, - ), - ], - ), - ), - ); - } -} - -/// 环形进度条绘制器 -class _CircularProgressPainter extends CustomPainter { - final double progress; - final Color color; - final double strokeWidth; - - _CircularProgressPainter({ - required this.progress, - required this.color, - required this.strokeWidth, - }); - - @override - void paint(Canvas canvas, Size size) { - final center = Offset(size.width / 2, size.height / 2); - final radius = (size.width / 2) - strokeWidth / 2; - - final paint = Paint() - ..color = color - ..strokeWidth = strokeWidth - ..style = PaintingStyle.stroke - ..strokeCap = StrokeCap.round; - - // 绘制圆弧,从顶部开始 (-π/2),顺时针绘制 - const startAngle = -math.pi / 2; - final sweepAngle = 2 * math.pi * progress; - - canvas.drawArc( - Rect.fromCircle(center: center, radius: radius), - startAngle, - sweepAngle, - false, - paint, - ); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return oldDelegate is! _CircularProgressPainter || - oldDelegate.progress != progress || - oldDelegate.color != color || - oldDelegate.strokeWidth != strokeWidth; - } -} diff --git a/apps/client/lib/modules/agent/widgets/full_screen_editor.dart b/apps/client/lib/modules/agent/widgets/full_screen_editor.dart deleted file mode 100644 index 94c2bbd0..00000000 --- a/apps/client/lib/modules/agent/widgets/full_screen_editor.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import '../controllers/agent_controller.dart'; - -/// 底部弹出式文本编辑器组件 -class FullScreenEditor extends StatefulWidget { - const FullScreenEditor({super.key}); - - @override - State createState() => _FullScreenEditorState(); -} - -class _FullScreenEditorState extends State { - late TextEditingController _textController; - late FocusNode _focusNode; - final AgentController controller = AgentController.to; - - @override - void initState() { - super.initState(); - // 使用与主界面相同的文本控制器,保持内容同步 - _textController = controller.textController; - _focusNode = FocusNode(); - - // 延迟聚焦,确保界面完全加载后再聚焦 - WidgetsBinding.instance.addPostFrameCallback((_) { - _focusNode.requestFocus(); - }); - } - - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - final primaryColor = Theme.of(context).primaryColor; - final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; // 获取键盘高度 - - return Container( - height: MediaQuery.of(context).size.height * 0.95, // 占屏幕高度的90% - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20.r), - topRight: Radius.circular(20.r), - ), - ), - child: Stack( - children: [ - // 主要内容区域 - Column( - children: [ - // 主要编辑区域 - Expanded( - child: Container( - padding: EdgeInsets.fromLTRB(16.w, 60.w, 16.w, 16.w), - child: TextField( - controller: _textController, - focusNode: _focusNode, - maxLines: null, - expands: true, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.newline, - style: TextStyle( - fontSize: 16.sp, - color: isDarkMode ? Colors.white : Colors.black87, - height: 1.5, - ), - decoration: InputDecoration( - hintText: 'fullScreenEditorHint'.tr, // 在这里输入你的消息... - hintStyle: TextStyle( - color: isDarkMode ? Colors.white54 : Colors.grey[500], - fontSize: 16.sp, - ), - border: InputBorder.none, - contentPadding: EdgeInsets.zero, - ), - ), - ), - ), - - // 底部操作区域 - Container( - margin: - EdgeInsets.only(bottom: keyboardHeight), // 关键修改:添加键盘高度的边距 - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.grey[50], - border: Border( - top: BorderSide( - color: isDarkMode ? Colors.grey[700]! : Colors.grey[200]!, - width: 1, - ), - ), - ), - child: Row( - children: [ - // 字符计数 - Expanded( - child: ValueListenableBuilder( - valueListenable: _textController, - builder: (context, value, child) { - return Text( - 'characterCount'.tr.replaceAll('{count}', - '${value.text.length}'), // ${value.text.length} 字符 - style: TextStyle( - color: isDarkMode - ? Colors.white60 - : Colors.grey[600], - fontSize: 14.sp, - ), - ); - }, - ), - ), - - // 发送按钮 - 保持与主界面一致的样式 - Obx(() { - final hasText = controller.hasTextInput.value; - return GestureDetector( - onTap: hasText - ? () async { - await controller.sendMessageFromFullScreen(); - if (context.mounted) { - Navigator.of(context).pop(); - } - } - : null, - child: Container( - width: 44.w, - height: 44.h, - decoration: BoxDecoration( - color: hasText - ? primaryColor - : (isDarkMode - ? Colors.grey[800] - : Colors.grey[300]), - borderRadius: BorderRadius.circular(22.r), - ), - child: Icon( - Icons.send, - color: hasText - ? Colors.white - : (isDarkMode - ? Colors.grey[600] - : Colors.grey[500]), - size: 22.sp, - ), - ), - ); - }), - ], - ), - ), - ], - ), - - // 右上角关闭按钮 - Positioned( - top: 16.h, - right: 16.w, - child: GestureDetector( - onTap: () { - controller.closeFullScreenEdit(); - Navigator.of(context).pop(); - }, - child: Container( - width: 32.w, - height: 32.h, - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.grey[200], - borderRadius: BorderRadius.circular(16.r), - ), - child: Icon( - Icons.close, - color: isDarkMode ? Colors.white : Colors.black87, - size: 20.sp, - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/apps/client/lib/modules/agent/widgets/sound_bar_widget.dart b/apps/client/lib/modules/agent/widgets/sound_bar_widget.dart deleted file mode 100644 index 9028f713..00000000 --- a/apps/client/lib/modules/agent/widgets/sound_bar_widget.dart +++ /dev/null @@ -1,155 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../controllers/agent_controller.dart'; - -/// 声音柱动画组件 (已优化) -/// 用于显示语音输入时的音频波形效果 -class SoundBarWidget extends StatefulWidget { - final double delay; - final bool isActive; - - const SoundBarWidget({ - Key? key, - required this.delay, - required this.isActive, - }) : super(key: key); - - @override - State createState() => _SoundBarWidgetState(); -} - -class _SoundBarWidgetState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _animation; - - @override - void initState() { - super.initState(); - - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 500), - ); - - _animation = Tween(begin: 3, end: 18).animate( - CurvedAnimation( - parent: _controller, - curve: Curves.easeInOut, - ), - ); - // 移除了 .addListener(() { setState(() {}); }) - - _startOrStopAnimation(); - } - - @override - void didUpdateWidget(SoundBarWidget oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.isActive != oldWidget.isActive) { - _startOrStopAnimation(); - } - } - - /// 开始或停止动画 - void _startOrStopAnimation() { - // 确保组件仍然挂载在树上 - if (!mounted) return; - - if (widget.isActive) { - Future.delayed(Duration(milliseconds: (widget.delay * 1000).toInt()), () { - if (mounted && widget.isActive) { - _controller.repeat(reverse: true); - } - }); - } else { - // 停止并平滑地过渡到初始状态 - _controller.stop(); - _controller.animateTo(0.0, - duration: const Duration(milliseconds: 200), curve: Curves.easeOut); - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final primaryColor = Theme.of(context).primaryColor; - - // 使用 AnimatedBuilder 来优化性能 - return AnimatedBuilder( - animation: _animation, - builder: (context, child) { - return Container( - width: 2.5, - // 当动画不活跃时,高度固定为3;活跃时使用动画值 - height: widget.isActive ? _animation.value : 3, - margin: const EdgeInsets.symmetric(horizontal: 1), - decoration: BoxDecoration( - color: widget.isActive - ? primaryColor.withOpacity(0.8) - : Colors.grey.withOpacity(0.4), - borderRadius: BorderRadius.circular(1.5), - ), - ); - }, - ); - } -} - -/// 声音柱组合组件 (无需改动) -/// 包含多个声音柱的组合显示 -class SoundBarsWidget extends StatelessWidget { - final bool isActive; - final int barCount; - - const SoundBarsWidget({ - Key? key, - required this.isActive, - this.barCount = 10, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(barCount, (index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 1), - child: SoundBarWidget( - delay: index * 0.1, - isActive: isActive, - ), - ); - }), - ); - } -} - -/// 带控制器集成的声音柱组件 (无需改动) -/// 自动从AgentController获取状态 -class AgentSoundBarsWidget extends StatelessWidget { - final int barCount; - - const AgentSoundBarsWidget({ - Key? key, - this.barCount = 10, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Obx(() { - final controller = Get.find(); - final shouldShowAnimation = - controller.isListening.value || controller.isPushToTalkActive.value; - return SoundBarsWidget( - isActive: shouldShowAnimation, - barCount: barCount, - ); - }); - } -} diff --git a/apps/client/lib/modules/ble/bindings/ble_binding.dart b/apps/client/lib/modules/ble/bindings/ble_binding.dart deleted file mode 100644 index 4fc2cc5c..00000000 --- a/apps/client/lib/modules/ble/bindings/ble_binding.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/ble_controller.dart'; - -class BleBinding extends Bindings { - @override - void dependencies() { - Get.put(BleController()); - } -} \ No newline at end of file diff --git a/apps/client/lib/modules/ble/controllers/ble_controller.dart b/apps/client/lib/modules/ble/controllers/ble_controller.dart deleted file mode 100644 index 3af4089b..00000000 --- a/apps/client/lib/modules/ble/controllers/ble_controller.dart +++ /dev/null @@ -1,267 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:flutter_blue_plus/flutter_blue_plus.dart' as fbp; -import 'package:permission_handler/permission_handler.dart'; -import '../../../core/utils/permission_util.dart'; - -class BleController extends GetxController { - // 扫描状态 - final RxBool isScanning = false.obs; - final RxBool isBluetoothOn = false.obs; - final RxBool hasPermissions = false.obs; - - // 发现的设备列表 - final RxList scanResults = [].obs; - - // 连接的设备 - final Rx connectedDevice = - Rx(null); - - // 错误信息 - final RxString errorMessage = ''.obs; - - @override - void onInit() { - super.onInit(); - _initBluetooth(); - } - - @override - void onClose() { - stopScan(); - super.onClose(); - } - - // 初始化蓝牙 - Future _initBluetooth() async { - try { - // 先检查权限 - await _checkPermissions(); - - // 检查蓝牙适配器状态 - fbp.FlutterBluePlus.adapterState.listen((state) { - isBluetoothOn.value = state == fbp.BluetoothAdapterState.on; - if (state == fbp.BluetoothAdapterState.on) { - errorMessage.value = ''; - } else { - errorMessage.value = 'pleaseEnableBluetooth'.tr; - stopScan(); - } - }); - - // 监听扫描结果 - fbp.FlutterBluePlus.scanResults.listen((results) { - scanResults.value = results; - }); - - // 监听扫描状态 - fbp.FlutterBluePlus.isScanning.listen((scanning) { - isScanning.value = scanning; - }); - } catch (e) { - errorMessage.value = 'bleInitFailedWithError'.trParams({'error': e.toString()}); - } - } - - // 检查权限(只查状态,绝不发起申请) - // - // 合规(场景 1/2):进页面就自动弹权限框是明确禁止的。这里原来虽然叫 - // "不请求",实际却调了 .request(),用户一进 BLE 页就被弹蓝牙+定位框, - // 拒绝之后每次重进页面还会再弹。现在只读状态,申请交给用户点"扫描"时触发。 - Future _checkPermissions() async { - try { - bool allGranted = true; - for (final p in _blePermissions) { - if (!await p.status.isGranted) { - allGranted = false; - break; - } - } - hasPermissions.value = allGranted; - - if (!allGranted) { - errorMessage.value = 'needBluetoothAndLocationPermission'.tr; - } - } catch (e) { - print('检查权限失败: $e'); - hasPermissions.value = false; - } - } - - // 扫描需要的权限(成套,缺一个都扫不出设备) - static const List _blePermissions = [ - Permission.bluetoothScan, - Permission.bluetoothConnect, - Permission.location, - ]; - - // 请求权限(仅由用户点击"扫描"触发) - // - // 合规(场景 1):拒绝之后不再弹任何框、不跳设置页——原来这里在检测到 - // permanentlyDenied 时会立刻弹一个带"前往设置"的自定义框,正是要禁掉的行为。 - // 现在统一走 PermissionUtil,拒绝即进入 48h 冷却,返回 false 后由 UI 置灰扫描按钮。 - Future _requestPermissions() async { - try { - final allGranted = await PermissionUtil.instance.requestPermissions( - permissions: _blePermissions, - permissionName: 'bluetooth'.tr, - explanationText: 'needBluetoothAndLocationPermission'.tr, - ); - hasPermissions.value = allGranted; - - if (!allGranted) { - // 只做静态提示,不引导、不跳转 - errorMessage.value = 'needBluetoothAndLocationPermission'.tr; - } - - return allGranted; - } catch (e) { - print('请求权限失败: $e'); - errorMessage.value = 'permissionRequestFailedWithError'.trParams({'error': e.toString()}); - return false; - } - } - - /// 权限被拒且仍在冷却期 —— 视图据此把扫描入口置灰 - bool get isPermissionCoolingDown => _blePermissions - .any((p) => PermissionUtil.instance.isDenialCoolingDown(p)); - - // 显示蓝牙开启对话框 - void _showBluetoothDialog() { - Get.dialog( - AlertDialog( - title: Text('needEnableBluetooth'.tr), - content: Text('enableBluetoothDialogContent'.tr), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: Text('cancel'.tr), - ), - TextButton( - onPressed: () { - Get.back(); - // 尝试开启蓝牙 - fbp.FlutterBluePlus.turnOn(); - }, - child: Text('enableBluetooth'.tr), - ), - ], - ), - ); - } - - // 开始扫描 - Future startScan() async { - try { - errorMessage.value = ''; - - // 检查权限 - if (!hasPermissions.value) { - if (!await _requestPermissions()) { - return; - } - } - - // 检查蓝牙状态 - if (!isBluetoothOn.value) { - _showBluetoothDialog(); - return; - } - - // 清空之前的结果 - scanResults.clear(); - - // 开始扫描 - await fbp.FlutterBluePlus.startScan( - timeout: const Duration(seconds: 30), - androidUsesFineLocation: true, - ); - } catch (e) { - errorMessage.value = 'bleScanFailedWithError'.trParams({'error': e.toString()}); - } - } - - // 停止扫描 - Future stopScan() async { - try { - await fbp.FlutterBluePlus.stopScan(); - } catch (e) { - print('停止扫描失败: $e'); - } - } - - // 连接设备 - Future connectToDevice(fbp.BluetoothDevice device) async { - try { - errorMessage.value = ''; - - // 停止扫描 - await stopScan(); - - // 连接设备 - await device.connect(timeout: const Duration(seconds: 15)); - connectedDevice.value = device; - - Get.snackbar( - 'bleConnectionSuccess'.tr, - 'bleConnectedToDevice'.trParams({'device': device.platformName.isNotEmpty ? device.platformName : 'unknownEquipment'.tr}), - backgroundColor: Colors.green, - colorText: Colors.white, - ); - } catch (e) { - errorMessage.value = 'bleConnectionFailedWithError'.trParams({'error': e.toString()}); - Get.snackbar( - 'connectionFailed'.tr, - e.toString(), - backgroundColor: Colors.red, - colorText: Colors.white, - ); - } - } - - // 断开连接 - Future disconnectDevice() async { - try { - if (connectedDevice.value != null) { - await connectedDevice.value!.disconnect(); - connectedDevice.value = null; - - Get.snackbar( - 'bleDisconnected'.tr, - 'bleDeviceDisconnected'.tr, - backgroundColor: Colors.orange, - colorText: Colors.white, - ); - } - } catch (e) { - errorMessage.value = 'bleDisconnectFailedWithError'.trParams({'error': e.toString()}); - } - } - - // 获取设备名称 - String getDeviceName(fbp.ScanResult result) { - if (result.device.platformName.isNotEmpty) { - return result.device.platformName; - } - if (result.advertisementData.localName.isNotEmpty) { - return result.advertisementData.localName; - } - return 'unknownEquipment'.tr; - } - - // 获取信号强度描述 - String getSignalStrengthText(int rssi) { - if (rssi >= -50) return 'signalStrong'.tr; - if (rssi >= -70) return 'signalMedium'.tr; - if (rssi >= -90) return 'signalWeak'.tr; - return 'signalVeryWeak'.tr; - } - - // 获取信号强度图标 - IconData getSignalStrengthIcon(int rssi) { - if (rssi >= -50) return Icons.signal_wifi_4_bar; - if (rssi >= -70) return Icons.signal_wifi_4_bar; - if (rssi >= -90) return Icons.signal_wifi_4_bar; - return Icons.signal_wifi_4_bar; - } -} diff --git a/apps/client/lib/modules/ble/views/ble_view.dart b/apps/client/lib/modules/ble/views/ble_view.dart deleted file mode 100644 index 1b8d7d3c..00000000 --- a/apps/client/lib/modules/ble/views/ble_view.dart +++ /dev/null @@ -1,374 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_blue_plus/flutter_blue_plus.dart' as fbp; -import '../controllers/ble_controller.dart'; - -class BleView extends GetView { - const BleView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.white, - appBar: AppBar( - title: Text( - 'bleScanTitle'.tr, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 18.sp, - fontWeight: FontWeight.bold, - ), - ), - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, - elevation: 0, - iconTheme: IconThemeData( - color: isDarkMode ? Colors.white : Colors.black, - ), - actions: [ - // 合规(场景 1/2):用户拒绝过蓝牙/定位权限且仍在 48h 冷却期内时, - // 扫描按钮置灰——既不能再弹权限框,也不该让用户点了毫无反应。 - Obx(() { - // 读一下 hasPermissions 让 Obx 建立订阅:isPermissionCoolingDown - // 读的是 GetStorage 不是 Rx,单靠它不会触发重建 - controller.hasPermissions.value; - final blocked = controller.isPermissionCoolingDown; - return IconButton( - onPressed: blocked - ? null - : (controller.isScanning.value - ? controller.stopScan - : controller.startScan), - icon: Icon( - controller.isScanning.value ? Icons.stop : Icons.refresh, - color: blocked - ? Colors.grey - : (controller.isScanning.value - ? Colors.red - : (isDarkMode ? Colors.white : Colors.black)), - ), - ); - }), - ], - ), - body: SafeArea( - child: Column( - children: [ - // 状态栏 - _buildStatusBar(isDarkMode), - - // 连接的设备信息 - _buildConnectedDevice(isDarkMode), - - // 设备列表 - Expanded( - child: _buildDeviceList(isDarkMode), - ), - ], - ), - ), - floatingActionButton: Obx(() { - controller.hasPermissions.value; // 同上:建立订阅,权限申请结束后重建 - final blocked = controller.isPermissionCoolingDown; - return FloatingActionButton( - onPressed: blocked - ? null - : (controller.isScanning.value - ? controller.stopScan - : controller.startScan), - backgroundColor: blocked - ? Colors.grey - : (controller.isScanning.value ? Colors.red : Colors.blue), - child: Icon( - controller.isScanning.value - ? Icons.stop - : Icons.bluetooth_searching, - color: Colors.white, - ), - ); - }), - ); - } - - // 状态栏 - Widget _buildStatusBar(bool isDarkMode) { - return Obx(() => Container( - width: double.infinity, - padding: EdgeInsets.all(16.w), - margin: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.grey[100], - borderRadius: BorderRadius.circular(12.r), - ), - child: Column( - children: [ - Row( - children: [ - Icon( - controller.isBluetoothOn.value - ? Icons.bluetooth - : Icons.bluetooth_disabled, - color: controller.isBluetoothOn.value - ? Colors.blue - : Colors.red, - size: 24.w, - ), - SizedBox(width: 8.w), - Text( - controller.isBluetoothOn.value ? 'bluetoothOn'.tr : '蓝牙已关闭', - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 16.sp, - fontWeight: FontWeight.w500, - ), - ), - Spacer(), - if (controller.isScanning.value) - SizedBox( - width: 20.w, - height: 20.w, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.blue), - ), - ), - ], - ), - if (controller.errorMessage.value.isNotEmpty) ...[ - SizedBox(height: 8.h), - Text( - controller.errorMessage.value, - style: TextStyle( - color: Colors.red, - fontSize: 14.sp, - ), - ), - ], - if (controller.isScanning.value) ...[ - SizedBox(height: 8.h), - Text( - 'bleScanning'.tr, - style: TextStyle( - color: Colors.blue, - fontSize: 14.sp, - ), - ), - ], - ], - ), - )); - } - - // 已连接设备信息 - Widget _buildConnectedDevice(bool isDarkMode) { - return Obx(() { - if (controller.connectedDevice.value == null) { - return SizedBox.shrink(); - } - - final device = controller.connectedDevice.value!; - return Container( - width: double.infinity, - padding: EdgeInsets.all(16.w), - margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), - decoration: BoxDecoration( - color: Colors.green.withOpacity(0.1), - border: Border.all(color: Colors.green, width: 1), - borderRadius: BorderRadius.circular(12.r), - ), - child: Row( - children: [ - Icon( - Icons.bluetooth_connected, - color: Colors.green, - size: 24.w, - ), - SizedBox(width: 12.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'bleConnectedDevices'.tr, - style: TextStyle( - color: Colors.green, - fontSize: 12.sp, - fontWeight: FontWeight.w500, - ), - ), - Text( - device.platformName.isNotEmpty - ? device.platformName - : 'bleUnknownDevice'.tr, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 16.sp, - fontWeight: FontWeight.bold, - ), - ), - Text( - device.remoteId.toString(), - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - fontSize: 12.sp, - ), - ), - ], - ), - ), - IconButton( - onPressed: controller.disconnectDevice, - icon: Icon( - Icons.close, - color: Colors.red, - size: 20.w, - ), - ), - ], - ), - ); - }); - } - - // 设备列表 - Widget _buildDeviceList(bool isDarkMode) { - return Obx(() { - if (controller.scanResults.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.bluetooth_searching, - size: 64.w, - color: isDarkMode ? Colors.white54 : Colors.black54, - ), - SizedBox(height: 16.h), - Text( - controller.isScanning.value ? '正在搜索设备...' : 'bleTapToScan'.tr, - style: TextStyle( - color: isDarkMode ? Colors.white54 : Colors.black54, - fontSize: 16.sp, - ), - textAlign: TextAlign.center, - ), - ], - ), - ); - } - - return ListView.builder( - padding: EdgeInsets.symmetric(horizontal: 16.w), - itemCount: controller.scanResults.length, - itemBuilder: (context, index) { - final result = controller.scanResults[index]; - return _buildDeviceItem(result, isDarkMode); - }, - ); - }); - } - - // 设备项 - Widget _buildDeviceItem(fbp.ScanResult result, bool isDarkMode) { - final deviceName = controller.getDeviceName(result); - final isConnected = - controller.connectedDevice.value?.remoteId == result.device.remoteId; - - return Container( - margin: EdgeInsets.only(bottom: 12.h), - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.white, - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: isConnected - ? Colors.green - : (isDarkMode ? Colors.grey[700]! : Colors.grey[300]!), - width: isConnected ? 2 : 1, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 8, - offset: Offset(0, 2), - ), - ], - ), - child: ListTile( - contentPadding: EdgeInsets.all(16.w), - leading: Container( - width: 48.w, - height: 48.w, - decoration: BoxDecoration( - color: isConnected - ? Colors.green.withOpacity(0.1) - : Colors.blue.withOpacity(0.1), - borderRadius: BorderRadius.circular(24.r), - ), - child: Icon( - isConnected ? Icons.bluetooth_connected : Icons.bluetooth, - color: isConnected ? Colors.green : Colors.blue, - size: 24.w, - ), - ), - title: Text( - deviceName, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - fontSize: 16.sp, - fontWeight: FontWeight.bold, - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 4.h), - Text( - result.device.remoteId.toString(), - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - fontSize: 12.sp, - ), - ), - SizedBox(height: 4.h), - Row( - children: [ - Icon( - controller.getSignalStrengthIcon(result.rssi), - size: 16.w, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - SizedBox(width: 4.w), - Text( - '${result.rssi} dBm (${controller.getSignalStrengthText(result.rssi)})', - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - fontSize: 12.sp, - ), - ), - ], - ), - ], - ), - trailing: isConnected - ? Icon( - Icons.check_circle, - color: Colors.green, - size: 24.w, - ) - : IconButton( - onPressed: () => controller.connectToDevice(result.device), - icon: Icon( - Icons.connect_without_contact, - color: Colors.blue, - size: 24.w, - ), - ), - onTap: isConnected - ? null - : () => controller.connectToDevice(result.device), - ), - ); - } -} diff --git a/apps/client/lib/modules/devices/bindings/devices_binding.dart b/apps/client/lib/modules/devices/bindings/devices_binding.dart deleted file mode 100644 index 3eb4575c..00000000 --- a/apps/client/lib/modules/devices/bindings/devices_binding.dart +++ /dev/null @@ -1,11 +0,0 @@ -import '/modules/devices/controllers/devices_controller.dart'; -import 'package:get/get.dart'; - -class DevicesBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => DevicesController(), - ); - } -} diff --git a/apps/client/lib/modules/devices/controllers/devices_controller.dart b/apps/client/lib/modules/devices/controllers/devices_controller.dart deleted file mode 100644 index 97e1eb03..00000000 --- a/apps/client/lib/modules/devices/controllers/devices_controller.dart +++ /dev/null @@ -1,360 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:device_jieli/device_jieli.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import 'package:permission_handler/permission_handler.dart'; - -import '/core/utils/logger.dart'; -import '/core/utils/permission_util.dart'; -import '/data/models/device_category.dart'; -import '/data/services/agent_module_defaults.dart'; -import '/data/models/user_Info.dart'; -import '/data/services/ble_manager.dart'; -import '/data/services/network/api.dart'; -import '/data/utils/jieli_adv_parser.dart'; -import '/routes/app_routes.dart'; - -/// 设备页控制器:扫描杰理设备、绑定(首次)、连接、断开。 -class DevicesController extends GetxController { - static const String _tag = 'DevicesController'; - - late final BleManager _bleManager; - - RxList get scannedDevices => _bleManager.scannedDevices; - RxBool get isScanning => _bleManager.isScanning; - - RxInt get connectionStateRx => _bleManager.connectionStateRx; - String? get currentDeviceAddress => _bleManager.currentDeviceAddress; - - /// 正在连接的目标地址(用于 tile UI 反馈) - final RxnString connectingAddress = RxnString(); - - /// 正在调用绑定接口的目标地址(用于 tile UI 反馈) - final RxnString bindingAddress = RxnString(); - - /// 当 [User.instance.devices] 变化时触发 UI 重建。 - /// 直接读 user 对象不响应式,所以加一个 tick。 - final RxInt boundDevicesTick = 0.obs; - - @override - void onInit() { - super.onInit(); - _bleManager = Get.find(); - Future.microtask(startScan); - } - - @override - void onClose() { - _bleManager.stopScan(); - super.onClose(); - } - - Future startScan() async { - Logger.i(_tag, 'startScan 被调用'); - final granted = await _ensureBluetoothPermission(); - if (!granted) { - Get.snackbar('tip'.tr, 'pleaseGrantBluetoothPermission'.tr); - return; - } - // 从「选择设备」页带过来的品类:只扫该品类产品的 scanuuid; - // 没带品类(或品类圈不出产品)时退回扫描全部 UUID - final String? categoryId = Get.arguments is Map - ? (Get.arguments as Map)['categoryId'] as String? - : null; - _categoryId = categoryId; - final uuids = DeviceCategories.scanUuidsFor(categoryId); - Logger.i(_tag, - '扫描品类=${categoryId ?? "全部"}, uuid 数=${uuids.length}'); - final ok = await _bleManager.startScan(uuids: uuids); - if (!ok) { - final err = _bleManager.lastScanError ?? 'unknown'; - Logger.w(_tag, '扫描启动失败: $err'); - Get.snackbar('error'.tr, '${'startScanFailed'.tr}: $err'); - } - } - - Future stopScan() async { - await _bleManager.stopScan(); - } - - /// 从「选择设备」页带过来的品类 id,连接成功后按它铺 AI 页默认功能 - String? _categoryId; - - /// 当前用户已绑定的设备里是否包含这个 BLE 设备(按 MAC 匹配)。 - bool isDeviceBound(JieliDevice device) { - if (!User.isLoggedIn()) return false; - return _findBoundDevice(device) != null; - } - - UserDevice? _findBoundDevice(JieliDevice device) { - final addr = device.address.replaceAll(':', '').toUpperCase(); - final edr = (device.edrAddr ?? '').replaceAll(':', '').toUpperCase(); - for (final d in User.instance.devices) { - final dm = d.devicemac.replaceAll(':', '').toUpperCase(); - if (dm.isEmpty) continue; - if (dm == addr || (edr.isNotEmpty && dm == edr)) return d; - } - return null; - } - - /// 点击"连接"或"绑定后自动连接"时调用。 - Future connect(JieliDevice device) async { - if (connectingAddress.value != null) { - Logger.i(_tag, '连接进行中,忽略重复点击: ${device.address}'); - return; - } - if (_bleManager.isConnected && - _bleManager.currentDeviceAddress == device.address) { - Logger.i(_tag, '设备已处于连接状态,直接进入首页: ${device.address}'); - Get.offAllNamed(Routes.home); - return; - } - - Logger.i(_tag, '连接设备: ${device.name} ${device.address}'); - connectingAddress.value = device.address; - await _bleManager.stopScan(); - - final completer = Completer(); - bool gotConnectionOk = false; - - final evtSub = Jielihome.instance.events.listen((evt) { - if (evt is ConnectionStateEvent) { - if (evt.address != device.address) return; - if (evt.state == ConnectionStateEvent.connectionOk) { - gotConnectionOk = true; - } else if (evt.state == ConnectionStateEvent.connectionDisconnect) { - if (!completer.isCompleted) completer.complete(false); - } - } else if (evt is RcspInitEvent) { - if (evt.address != device.address) return; - if (evt.success && gotConnectionOk) { - if (!completer.isCompleted) completer.complete(true); - } else if (!evt.success) { - if (!completer.isCompleted) completer.complete(false); - } - } - }); - - _showConnectingDialog(device); - - try { - await _bleManager.connectDevice(device); - final ok = await completer.future - .timeout(const Duration(seconds: 20), onTimeout: () => false); - - if (Get.isDialogOpen ?? false) Get.back(); - - if (ok) { - // 首次连上该品类设备 → AI 页铺开这个品类的默认功能编排 - AgentDefaults.onJieliConnected(_categoryId); - Get.offAllNamed(Routes.home); - } else { - Get.snackbar('error'.tr, 'connectionFailure'.tr); - } - } catch (e) { - Logger.e(_tag, '连接异常: $e'); - if (Get.isDialogOpen ?? false) Get.back(); - Get.snackbar('error'.tr, 'connectionFailure'.tr); - } finally { - await evtSub.cancel(); - connectingAddress.value = null; - } - } - - /// 点击"绑定"按钮:解析广播 → 调用 binddevice → 成功后自动 connect。 - Future bindAndConnect(JieliDevice device) async { - if (bindingAddress.value != null || connectingAddress.value != null) { - Logger.i(_tag, '已有绑定/连接进行中,忽略重复点击: ${device.address}'); - return; - } - if (!User.isLoggedIn()) { - Get.snackbar('tip'.tr, 'pleaseLoginFirst'.tr); - return; - } - - final info = JieliAdvParser.parse(device); - if (info == null || info.code.isEmpty) { - Logger.w(_tag, '无法从广播包解析绑定信息: ${device.address}'); - Get.snackbar('error'.tr, 'deviceUnauthorized'.tr); - return; - } - - bindingAddress.value = device.address; - try { - final response = await Api.binddevice({ - 'pid': info.pid, - 'code': info.code, - 'devicename': device.name, - 'devicemac': device.address, - }); - Logger.i(_tag, '绑定接口返回: $response'); - - if (response == null) { - Get.snackbar('error'.tr, 'deviceUnauthorized'.tr); - Get.offAllNamed(Routes.devices); - return; - } - - final deviceJson = response['device']; - if (deviceJson == null || deviceJson == '') { - Get.snackbar('error'.tr, 'deviceUnauthorized'.tr); - Get.offAllNamed(Routes.devices); - return; - } - - final newUserDevice = - UserDevice.fromJson(deviceJson as Map); - User.instance.addUserDevice(newUserDevice); - boundDevicesTick.value++; - - // 首次绑定奖励 - if (response['isgiveaway'] == true) { - _applyGiveaway(response); - } - - // 绑定成功后自动连接进入主界面 - await connect(device); - } catch (e) { - Logger.e(_tag, '绑定异常: $e'); - Get.snackbar('error'.tr, 'deviceUnauthorized'.tr); - } finally { - bindingAddress.value = null; - } - } - - void _applyGiveaway(Map response) { - User.instance.viplv = (response['viplv'] as num?)?.toInt() ?? User.instance.viplv; - User.instance.vipexptime = - (response['vipexptime'] as num?)?.toInt() ?? User.instance.vipexptime; - User.instance.aichatintegral = - (response['aichatintegral'] as num?)?.toInt() ?? User.instance.aichatintegral; - User.instance.aichattotalintegral = - (response['aichattotalintegral'] as num?)?.toInt() ?? - User.instance.aichattotalintegral; - User.instance.tradeintegral = - (response['tradeintegral'] as num?)?.toInt() ?? User.instance.tradeintegral; - User.instance.tradetotalintegral = - (response['tradetotalintegral'] as num?)?.toInt() ?? - User.instance.tradetotalintegral; - User.instance.meetintegral = - (response['meetintegral'] as num?)?.toInt() ?? User.instance.meetintegral; - User.instance.meettotalintegral = - (response['meettotalintegral'] as num?)?.toInt() ?? - User.instance.meettotalintegral; - Get.dialog( - AlertDialog( - title: Text('bindRewardTitle'.tr), - content: Text('bindRewardSimpleDesc'.tr), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: Text('ok'.tr), - ), - ], - ), - barrierDismissible: true, - ); - } - - void _showConnectingDialog(JieliDevice device) { - Get.dialog( - PopScope( - canPop: false, - child: Dialog( - backgroundColor: Colors.transparent, - elevation: 0, - insetPadding: EdgeInsets.symmetric(horizontal: 40.w), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 28.h), - decoration: BoxDecoration( - color: Get.isDarkMode - ? const Color(0xFF1C1C1E) - : Colors.white, - borderRadius: BorderRadius.circular(16.r), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 44.w, - height: 44.w, - child: const CircularProgressIndicator( - strokeWidth: 3, - valueColor: - AlwaysStoppedAnimation(Color(0xFF0066CC)), - ), - ), - SizedBox(height: 18.h), - Text( - 'connectingDevice'.tr, - style: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.w600, - color: Get.isDarkMode ? Colors.white : Colors.black87, - ), - ), - SizedBox(height: 6.h), - Text( - device.name.isEmpty ? device.address : device.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 12.sp, - color: Get.isDarkMode ? Colors.white54 : Colors.black54, - ), - ), - ], - ), - ), - ), - ), - barrierDismissible: false, - ); - } - - Future disconnect() async { - await _bleManager.disconnect(); - } - - bool isConnected(JieliDevice device) { - return _bleManager.isConnected && - _bleManager.currentDeviceAddress == device.address; - } - - /// 扫描设备需要的权限(Android) - static const List _scanPermissions = [ - Permission.bluetoothScan, - Permission.bluetoothConnect, - Permission.locationWhenInUse, - ]; - - /// 权限被拒且仍在 48h 冷却期 —— 视图据此把"扫描设备"入口置灰 - bool get isBluetoothPermissionCoolingDown { - if (Platform.isIOS) return false; - return _scanPermissions - .any((p) => PermissionUtil.instance.isDenialCoolingDown(p)); - } - - Future _ensureBluetoothPermission() async { - // iOS: CoreBluetooth 首次扫描时由系统自动弹出蓝牙权限对话框 - // (依赖 Info.plist 的 NSBluetoothAlwaysUsageDescription), - // permission_handler 的 bluetoothScan/bluetoothConnect 是 Android 专属, - // 在 iOS 上调用永远返回 denied,会误拦截扫描。 - if (Platform.isIOS) return true; - try { - // 合规(场景 1/2):走统一网关,用户拒绝一次后 48 小时内不再申请, - // 返回 false 时调用方置灰扫描入口,不得再弹窗或跳设置页。 - return await PermissionUtil.instance.requestPermissions( - permissions: _scanPermissions, - permissionName: 'bluetooth'.tr, - explanationText: 'bluetoothScanPermissionDesc'.tr, - ); - } catch (e) { - Logger.error('请求蓝牙权限失败: $e'); - return false; - } - } -} diff --git a/apps/client/lib/modules/devices/views/devices_view.dart b/apps/client/lib/modules/devices/views/devices_view.dart deleted file mode 100644 index ef45b4ea..00000000 --- a/apps/client/lib/modules/devices/views/devices_view.dart +++ /dev/null @@ -1,512 +0,0 @@ -import 'package:device_jieli/device_jieli.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -import '../controllers/devices_controller.dart'; -import '../../../core/theme/app_palette.dart'; - -class DevicesView extends StatefulWidget { - const DevicesView({Key? key}) : super(key: key); - - @override - State createState() => _DevicesViewState(); -} - -class _DevicesViewState extends State - with TickerProviderStateMixin { - late AnimationController _pulseController; - late AnimationController _rotationController; - late Animation _pulseAnimation; - late Animation _rotationAnimation; - - DevicesController get controller => Get.find(); - - @override - void initState() { - super.initState(); - _pulseController = AnimationController( - duration: const Duration(milliseconds: 1500), - vsync: this, - )..repeat(reverse: true); - _rotationController = AnimationController( - duration: const Duration(milliseconds: 2000), - vsync: this, - )..repeat(); - _pulseAnimation = - Tween(begin: 0.8, end: 1.2).animate(CurvedAnimation( - parent: _pulseController, - curve: Curves.easeInOut, - )); - _rotationAnimation = Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: _rotationController, curve: Curves.linear), - ); - } - - @override - void dispose() { - _pulseController.dispose(); - _rotationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return Scaffold( - backgroundColor: isDark ? Colors.black : Colors.white, - body: SafeArea( - child: Stack( - children: [ - Column( - children: [ - _buildTopSection(isDark), - Expanded(child: _buildDeviceListSection(isDark)), - _buildBottomActions(isDark), - ], - ), - // 返回箭头(左上角,悬浮不挤压居中标题) - Positioned( - left: 12.w, - top: 8.h, - child: GestureDetector( - onTap: () => Get.back(), - child: Container( - width: 40.w, - height: 40.w, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: isDark - ? Colors.white.withValues(alpha: 0.06) - : Colors.black.withValues(alpha: 0.04), - ), - child: Icon( - Icons.arrow_back_ios_new, - size: 18.sp, - color: isDark ? Colors.white : AppPalette.brand900, - ), - ), - ), - ), - ], - ), - ), - ); - } - - // ─── 顶部:标题 + 扫描动画 + 状态文字 ──────────────────── - Widget _buildTopSection(bool isDark) { - return Padding( - padding: EdgeInsets.only(top: 40.h, left: 24.w, right: 24.w), - child: Column( - children: [ - Text( - 'Eaimar', - style: TextStyle( - fontSize: 28.sp, - fontWeight: FontWeight.bold, - color: isDark ? Colors.white : Colors.black, - ), - ), - SizedBox(height: 6.h), - Text( - 'homeSlogan'.tr, - style: TextStyle( - fontSize: 14.sp, - color: isDark ? Colors.white60 : Colors.black54, - ), - ), - SizedBox(height: 24.h), - _buildScanAnimation(isDark), - SizedBox(height: 16.h), - Obx(() { - final scanning = controller.isScanning.value; - return Text( - scanning ? 'scanning'.tr : 'scanIdle'.tr, - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w500, - color: isDark ? Colors.white70 : Colors.black87, - ), - ); - }), - SizedBox(height: 6.h), - Text( - 'ensureHeadsetOnAndPairingMode'.tr, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12.sp, - color: isDark ? Colors.white38 : Colors.black45, - ), - ), - ], - ), - ); - } - - Widget _buildScanAnimation(bool isDark) { - const primaryColor = Color(0xFF0066CC); - const secondaryColor = Color(0xFF5E5CE6); - return SizedBox( - width: 180.w, - height: 180.w, - child: Obx(() { - final scanning = controller.isScanning.value; - return Stack( - alignment: Alignment.center, - children: [ - if (scanning) _buildPulse(primaryColor, secondaryColor), - if (scanning) _buildRotatingBorder(primaryColor), - Container( - width: 92.w, - height: 92.w, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [primaryColor, secondaryColor], - ), - shape: BoxShape.circle, - ), - child: Icon( - Icons.headphones, - color: Colors.white, - size: 44.w, - ), - ), - ], - ); - }), - ); - } - - Widget _buildPulse(Color primary, Color secondary) { - return AnimatedBuilder( - animation: _pulseAnimation, - builder: (_, __) { - final v = _pulseAnimation.value; - final opacity = 0.4 - (_pulseController.value * 0.3); - return Container( - width: 160.w * v, - height: 160.w * v, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - primary.withValues(alpha: opacity), - secondary.withValues(alpha: opacity), - ], - ), - shape: BoxShape.circle, - ), - ); - }, - ); - } - - Widget _buildRotatingBorder(Color primary) { - return AnimatedBuilder( - animation: _rotationAnimation, - builder: (_, __) { - return Transform.rotate( - angle: _rotationAnimation.value * 2 * 3.14159, - child: SizedBox( - width: 110.w, - height: 110.w, - child: CircularProgressIndicator( - strokeWidth: 3.w, - valueColor: AlwaysStoppedAnimation(primary), - backgroundColor: Colors.grey.withValues(alpha: 0.18), - value: 0.7, - ), - ), - ); - }, - ); - } - - // ─── 底部:扫描结果列表 ──────────────────────────────── - Widget _buildDeviceListSection(bool isDark) { - return Container( - margin: EdgeInsets.only(top: 20.h, left: 16.w, right: 16.w), - decoration: BoxDecoration( - color: isDark - ? Colors.white.withValues(alpha: 0.03) - : const Color(0xFFF5F6FA), - borderRadius: BorderRadius.circular(16.r), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: - EdgeInsets.only(left: 16.w, right: 16.w, top: 14.h, bottom: 8.h), - child: Row( - children: [ - Text( - 'discoveredDevices'.tr, - style: TextStyle( - fontSize: 13.sp, - fontWeight: FontWeight.w600, - color: isDark ? Colors.white70 : Colors.black87, - ), - ), - const Spacer(), - Obx(() => Text( - '${controller.scannedDevices.length}', - style: TextStyle( - fontSize: 12.sp, - color: isDark ? Colors.white54 : Colors.black54, - ), - )), - ], - ), - ), - Expanded( - child: Obx(() { - final devices = controller.scannedDevices; - if (devices.isEmpty) { - return Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 32.h), - child: Text( - 'noDevicesFound'.tr, - style: TextStyle( - fontSize: 13.sp, - color: isDark ? Colors.white38 : Colors.black45, - ), - ), - ), - ); - } - return ListView.separated( - padding: EdgeInsets.fromLTRB(8.w, 4.h, 8.w, 12.h), - itemCount: devices.length, - separatorBuilder: (_, __) => SizedBox(height: 6.h), - itemBuilder: (_, i) => _buildDeviceTile(devices[i], isDark), - ); - }), - ), - ], - ), - ); - } - - Widget _buildDeviceTile(JieliDevice device, bool isDark) { - return Obx(() { - // 订阅连接状态、当前连接地址、正在连接/绑定的目标 + 已绑定列表 tick,让 tile 实时刷新 - controller.connectionStateRx.value; - controller.boundDevicesTick.value; - final connected = controller.isConnected(device); - final connectingAddr = controller.connectingAddress.value; - final bindingAddr = controller.bindingAddress.value; - final connecting = connectingAddr == device.address; - final binding = bindingAddr == device.address; - final disabled = (connectingAddr != null && !connecting) || - (bindingAddr != null && !binding); - final bound = controller.isDeviceBound(device); - return _buildDeviceTileContent( - device, - isDark, - connected: connected, - connecting: connecting, - binding: binding, - disabled: disabled, - bound: bound, - ); - }); - } - - Widget _buildDeviceTileContent( - JieliDevice device, - bool isDark, { - required bool connected, - required bool connecting, - required bool binding, - required bool disabled, - required bool bound, - }) { - const connectedColor = Color(0xFF22C55E); - final canTap = !connecting && !binding && !disabled; - void onPrimaryTap() { - if (bound) { - controller.connect(device); - } else { - controller.bindAndConnect(device); - } - } - - return Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(12.r), - onTap: canTap ? onPrimaryTap : null, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.r), - color: isDark - ? Colors.white.withValues(alpha: 0.04) - : Colors.white, - border: Border.all( - color: connected - ? connectedColor.withValues(alpha: 0.3) - : (isDark - ? Colors.white.withValues(alpha: 0.05) - : Colors.black.withValues(alpha: 0.05)), - ), - ), - child: Row( - children: [ - Container( - width: 36.w, - height: 36.w, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: connected - ? connectedColor.withValues(alpha: 0.15) - : const Color(0xFF0066CC).withValues(alpha: 0.12), - ), - child: Icon( - Icons.headphones, - size: 18.sp, - color: connected - ? connectedColor - : const Color(0xFF0066CC), - ), - ), - SizedBox(width: 12.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - device.name.isEmpty - ? 'unnamedDevice'.tr - : device.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w600, - color: isDark ? Colors.white : Colors.black87, - ), - ), - ), - if (connected) - Container( - padding: EdgeInsets.symmetric( - horizontal: 6.w, vertical: 2.h), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.r), - color: connectedColor.withValues(alpha: 0.12), - ), - child: Text( - 'connected'.tr, - style: TextStyle( - fontSize: 10.sp, - fontWeight: FontWeight.w600, - color: connectedColor, - ), - ), - ), - ], - ), - SizedBox(height: 2.h), - Text( - '${device.address}${device.rssi != null ? ' RSSI ${device.rssi}' : ''}', - style: TextStyle( - fontSize: 10.sp, - color: isDark ? Colors.white38 : Colors.black45, - fontFamily: 'Menlo', - ), - ), - ], - ), - ), - if (connected) - TextButton( - onPressed: controller.disconnect, - style: TextButton.styleFrom( - minimumSize: Size(0, 28.h), - padding: EdgeInsets.symmetric(horizontal: 8.w), - ), - child: Text( - 'disconnect'.tr, - style: TextStyle(fontSize: 11.sp, color: Colors.red), - ), - ) - else if (connecting || binding) - SizedBox( - width: 18.w, - height: 18.w, - child: const CircularProgressIndicator( - strokeWidth: 2, - valueColor: - AlwaysStoppedAnimation(Color(0xFF0066CC)), - ), - ) - else - ElevatedButton( - onPressed: disabled ? null : onPrimaryTap, - style: ElevatedButton.styleFrom( - backgroundColor: bound - ? const Color(0xFF0066CC) - : const Color(0xFF8B5CF6), - foregroundColor: Colors.white, - minimumSize: Size(0, 30.h), - padding: EdgeInsets.symmetric(horizontal: 12.w), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - elevation: 0, - ), - child: Text( - bound ? 'connect'.tr : 'bind'.tr, - style: TextStyle( - fontSize: 11.sp, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), - ), - ); - } - - // ─── 底部:扫描操作按钮 ──────────────────────────────── - Widget _buildBottomActions(bool isDark) { - return Padding( - padding: EdgeInsets.fromLTRB(24.w, 12.h, 24.w, 20.h), - child: Obx(() { - final scanning = controller.isScanning.value; - return SizedBox( - width: double.infinity, - height: 48.h, - child: ElevatedButton.icon( - onPressed: scanning ? controller.stopScan : controller.startScan, - icon: Icon(scanning ? Icons.stop : Icons.refresh, size: 18.sp), - label: Text( - scanning ? 'stopScan'.tr : 'startScan'.tr, - style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w600), - ), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF0066CC), - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14.r), - ), - elevation: 0, - ), - ), - ); - }), - ); - } -} diff --git a/apps/client/lib/modules/eq_settings/bindings/eq_settings_binding.dart b/apps/client/lib/modules/eq_settings/bindings/eq_settings_binding.dart deleted file mode 100644 index 1f2884bd..00000000 --- a/apps/client/lib/modules/eq_settings/bindings/eq_settings_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/eq_settings_controller.dart'; - -class EqSettingsBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => EqSettingsController()); - } -} diff --git a/apps/client/lib/modules/eq_settings/controllers/eq_settings_controller.dart b/apps/client/lib/modules/eq_settings/controllers/eq_settings_controller.dart deleted file mode 100644 index 3b7e2541..00000000 --- a/apps/client/lib/modules/eq_settings/controllers/eq_settings_controller.dart +++ /dev/null @@ -1,217 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; - -import '../../../core/utils/logger.dart'; -import '../../../routes/app_routes.dart'; -import '../views/widgets/row_confirm_widget.dart'; -import '../views/widgets/row_equalizer_widget.dart'; -import '../views/widgets/row_grade_widget.dart'; -import '../views/widgets/row_input_widget.dart'; -import '../views/widgets/row_keypress_widget.dart'; -import '../views/widgets/row_radio_widget.dart'; -import '../views/widgets/row_switch_widget.dart'; -import '../views/widgets/row_timer_widget.dart'; - -class EqSettingsController extends GetxController { - final RxList uiList = [ - { - 'title': '音效', - 'templates': [ - { - 'type': 'equalizer', - 'name': '均衡器', - 'value': [], - 'options': [ - { - 'label': '默认', - 'value': [1, 2, 3, 4, 5, 4, 3, 2, 1, 0], - 'default': true, - }, - { - 'label': '流行', - 'value': [1, 0, 3, 4, 5, 4, 1, 2, 1, 0], - 'default': true, - }, - { - 'label': '摇滚', - 'value': [2, 3, 4, 5, 4, 3, 2, 1, 0, -1], - 'default': true, - }, - { - 'label': '爵士', - 'value': [3, 4, 0, 4, 3, 2, 1, 0, -1, -2], - 'default': true, - }, - { - 'label': '经典', - 'value': [3, 4, 2, 4, 3, 2, 1, 0, -1, -2], - 'default': true, - }, - { - 'label': '乡村', - 'value': [3, 4, 1, 4, 3, 2, 1, 0, -1, -2], - 'default': true, - }, - ], - 'xAxis': [ - '8', - '31', - '62', - '125', - '250', - '500', - '1K', - '2K', - '4K', - '16K' - ], - 'onCallback': (List value) { - Logger.d('equalizer', '均衡器:$value'); - }, - } - ], - }, - { - 'title': '按键功能', - 'templates': [ - { - 'type': 'keypress', - 'data': [ - {'key': 'key1', 'label': '单击', 'left': 1, 'right': 1}, - {'key': 'key2', 'label': '双击', 'left': 1, 'right': 1}, - {'key': 'key3', 'label': '三击', 'left': 1, 'right': 1}, - {'key': 'key4', 'label': '长按', 'left': 1, 'right': 1}, - ], - 'options': [ - {'label': '无作用', 'value': 0}, - {'label': '回拨电话', 'value': 1}, - {'label': '语音助手', 'value': 2}, - {'label': '上一曲', 'value': 3}, - {'label': '下一曲', 'value': 4}, - {'label': '音量加', 'value': 5}, - {'label': '音量减', 'value': 6}, - {'label': '播放暂停', 'value': 7}, - {'label': '游戏模式', 'value': 8}, - {'label': '降噪设置', 'value': 9}, - {'label': 'EQ切换', 'value': 10}, - ], - 'onCallback': (Map value) { - Logger.d('keypress', '按键功能:$value'); - }, - }, - ], - }, - { - 'title': '耳机功能', - 'templates': [ - { - 'type': 'input', - 'name': '修改名称', - 'value': 'eaimar', - 'onCallback': (String value) { - Logger.d('input', '修改名称:$value'); - }, - }, - { - 'type': 'radio', - 'name': '工作模式', - 'value': 1, - 'options': [ - {'label': '游戏模式', 'value': 1}, - {'label': '娱乐模式', 'value': 2}, - {'label': '睡眠模式', 'value': 3}, - ], - 'onCallback': (int value) { - Logger.d('radio', '工作模式:$value'); - }, - }, - { - 'type': 'switch', - 'name': '闪灯开关', - 'value': false, - 'onCallback': (bool value) { - Logger.d('switch', '闪灯开关:$value'); - }, - }, - { - 'type': 'switch', - 'name': '查找设备', - 'value': false, - 'description': '在设备上播放声音', - 'onCallback': (bool value) { - Logger.d('switch', '查找设备:$value'); - }, - }, - { - 'type': 'grade', - 'name': '降噪等级', - 'value': 0, - 'onCallback': (int value) { - Logger.d('grade', '降噪等级:$value'); - }, - }, - { - 'type': 'timer', - 'name': '定时关机', - 'value': 0, - 'onCallback': (int value) { - Logger.d('timer', '定时关机:$value'); - }, - }, - ], - }, - { - 'templates': [ - { - 'type': 'confirm', - 'name': '升级耳机固件', - 'onCallback': () => Get.toNamed(Routes.OTA_UPGRADE), - }, - { - 'type': 'confirm', - 'name': '恢复出厂设置', - 'tips': '确定要恢复出厂设置吗?', - 'onCallback': () { - Logger.d('confirm', '恢复出厂设置'); - }, - }, - { - 'type': 'confirm', - 'name': '清除配对记录', - 'tips': '确定要清除配对记录吗?', - 'onCallback': () { - Logger.d('confirm', '清除配对记录'); - }, - }, - ], - } - ].obs; - - final GetStorage _storage = GetStorage(); - - @override - void onInit() { - super.onInit(); - final List equalizerOptions = _storage.read('equalizer_options') ?? []; - if (equalizerOptions.isNotEmpty) { - List options = - equalizerOptions.map((e) => Map.from(e)).toList(); - uiList[0]['templates'][0]['options'].addAll(options); - } - } - - Widget getTemplate(Map data) { - return switch (data['type']) { - 'equalizer' => RowEqualizerWidget(data), - 'keypress' => RowKeypressWidget(data), - 'input' => RowInputWidget(data), - 'radio' => RowRadioWidget(data), - 'switch' => RowSwitchWidget(data), - 'grade' => RowGradeWidget(data), - 'timer' => RowTimerWidget(data), - 'confirm' => RowConfirmWidget(data), - _ => const SizedBox.shrink(), - }; - } -} diff --git a/apps/client/lib/modules/eq_settings/views/eq_settings_view.dart b/apps/client/lib/modules/eq_settings/views/eq_settings_view.dart deleted file mode 100644 index 87f3cddd..00000000 --- a/apps/client/lib/modules/eq_settings/views/eq_settings_view.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -import '../controllers/eq_settings_controller.dart'; - -class EqSettingsView extends GetView { - const EqSettingsView({super.key}); - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - appBar: _buildAppBar(isDarkMode), - body: SafeArea( - top: false, - child: _buildBody(isDarkMode), - ), - ); - } - - // 构建应用栏 - PreferredSizeWidget _buildAppBar(bool isDarkMode) { - return AppBar( - backgroundColor: isDarkMode ? Colors.black : Colors.white, - surfaceTintColor: Colors.transparent, - elevation: 0.5, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black, - size: 20.sp, - ), - onPressed: () => Get.back(), - ), - title: Text( - '设备设置', - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - centerTitle: true, - ); - } - - // 构建主体内容 - Widget _buildBody(bool isDarkMode) { - return Obx( - () => ListView.builder( - itemCount: controller.uiList.length, - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), - itemBuilder: (BuildContext context, int index) { - Map data = controller.uiList[index]; - return _item(isDarkMode, data); - }, - ), - ); - } - - Widget _item(bool isDarkMode, Map data) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), - margin: EdgeInsets.symmetric(vertical: 5.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withValues(alpha: 0.05) : Colors.white, - borderRadius: BorderRadius.circular(8.w), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withValues(alpha: 0.2) - : Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (data['title'] != null) - Text( - data['title'], - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ...data['templates'].map((e) => controller.getTemplate(e)).toList(), - ], - ), - ); - } -} diff --git a/apps/client/lib/modules/eq_settings/views/widgets/row_confirm_widget.dart b/apps/client/lib/modules/eq_settings/views/widgets/row_confirm_widget.dart deleted file mode 100644 index ce680b1d..00000000 --- a/apps/client/lib/modules/eq_settings/views/widgets/row_confirm_widget.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -class RowConfirmWidget extends StatefulWidget { - final Map templateData; - - const RowConfirmWidget(this.templateData, {super.key}); - - @override - State createState() => _RowConfirmWidgetState(); -} - -class _RowConfirmWidgetState extends State { - Map get _templateData => widget.templateData; - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return GestureDetector( - onTap: () { - if (_templateData['tips'] != null) { - _alertDialog(isDarkMode); - } else { - _templateData['onCallback'](); - } - }, - behavior: HitTestBehavior.translucent, - child: Padding( - padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), - child: Row( - children: [ - Expanded( - child: Text( - _templateData['name'], - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - Icon( - Icons.chevron_right, - size: 26.w, - color: Colors.blue, - ), - ], - ), - ), - ); - } - - void _alertDialog(bool isDarkMode) { - Get.dialog( - AlertDialog( - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, - title: Text( - _templateData['name'], - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - content: Text( - _templateData['tips'], - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: Text( - 'cancel'.tr, // 取消 - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ), - TextButton( - onPressed: () { - _templateData['onCallback'](); - Get.back(); - }, - child: Text( - 'confirm'.tr, // 确定 - style: TextStyle( - color: isDarkMode ? Colors.red[300] : Colors.red[600], - ), - ), - ), - ], - ), - ); - } -} diff --git a/apps/client/lib/modules/eq_settings/views/widgets/row_equalizer_widget.dart b/apps/client/lib/modules/eq_settings/views/widgets/row_equalizer_widget.dart deleted file mode 100644 index cccd2473..00000000 --- a/apps/client/lib/modules/eq_settings/views/widgets/row_equalizer_widget.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -import 'select_equalizer_widget.dart'; - -class RowEqualizerWidget extends StatefulWidget { - final Map templateData; - - const RowEqualizerWidget(this.templateData, {super.key}); - - @override - State createState() => _RowEqualizerWidgetState(); -} - -class _RowEqualizerWidgetState extends State { - Map get _templateData => widget.templateData; - - String _label = ''; - - @override - void initState() { - super.initState(); - _label = _templateData['options'].firstWhere( - (option) => listEquals(_templateData['value'], option['value']), - orElse: () => {'label': ''}, - )['label']; - } - - void _setLabel() { - setState(() { - _label = _templateData['options'].firstWhere( - (option) => listEquals(_templateData['value'], option['value']), - orElse: () => {'label': ''}, - )['label']; - }); - } - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return GestureDetector( - onTap: () async { - var result = await Get.to(() => SelectEqualizerWidget(_templateData)); - _setLabel(); - if (result != null) { - _templateData['onCallback'](_templateData['value']); - } - }, - behavior: HitTestBehavior.translucent, - child: Padding( - padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), - child: Row( - children: [ - Expanded( - child: Text( - _templateData['name'], - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - Text( - _label, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white : Colors.grey, - ), - ), - Icon( - Icons.chevron_right, - size: 26.w, - color: Colors.blue, - ), - ], - ), - ), - ); - } -} diff --git a/apps/client/lib/modules/eq_settings/views/widgets/row_grade_widget.dart b/apps/client/lib/modules/eq_settings/views/widgets/row_grade_widget.dart deleted file mode 100644 index 8abd2d32..00000000 --- a/apps/client/lib/modules/eq_settings/views/widgets/row_grade_widget.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class RowGradeWidget extends StatefulWidget { - final Map templateData; - - const RowGradeWidget(this.templateData, {super.key}); - - @override - State createState() => _RowGradeWidgetState(); -} - -class _RowGradeWidgetState extends State { - Map get _templateData => widget.templateData; - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Padding( - padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), - child: Row( - children: [ - Expanded( - child: Text( - _templateData['name'], - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - Slider( - value: _templateData['value'].toDouble(), - min: 0, - max: 10, - divisions: 10, - activeColor: Colors.blue, - onChanged: (value) { - setState(() { - _templateData['value'] = value.toInt(); - }); - }, - onChangeEnd: (value) { - _templateData['onCallback'](_templateData['value']); - }, - ), - SizedBox( - width: 15.w, - child: Text( - _templateData['value'].toString(), - textAlign: TextAlign.right, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white : Colors.grey, - ), - ), - ), - ], - ), - ); - } -} diff --git a/apps/client/lib/modules/eq_settings/views/widgets/row_input_widget.dart b/apps/client/lib/modules/eq_settings/views/widgets/row_input_widget.dart deleted file mode 100644 index 339b79ce..00000000 --- a/apps/client/lib/modules/eq_settings/views/widgets/row_input_widget.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -import 'settings_input_widget.dart'; - -class RowInputWidget extends StatefulWidget { - final Map templateData; - - const RowInputWidget(this.templateData, {super.key}); - - @override - State createState() => _RowInputWidgetState(); -} - -class _RowInputWidgetState extends State { - Map get _templateData => widget.templateData; - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return GestureDetector( - onTap: () => _inputBottomSheet(isDarkMode), - behavior: HitTestBehavior.translucent, - child: Padding( - padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), - child: Row( - children: [ - Expanded( - child: Text( - _templateData['name'], - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - Text( - _templateData['value'], - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white : Colors.grey, - ), - ), - Icon( - Icons.chevron_right, - size: 26.w, - color: Colors.blue, - ), - ], - ), - ), - ); - } - - void _inputBottomSheet(bool isDarkMode) { - Get.bottomSheet( - SettingsInputWidget(_templateData['name'], _templateData['value']), - isScrollControlled: true, - backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), - ), - ), - ).then((value) { - if (value != null) { - setState(() { - _templateData['value'] = value; - }); - _templateData['onCallback'](value); - } - }); - } -} diff --git a/apps/client/lib/modules/eq_settings/views/widgets/row_keypress_widget.dart b/apps/client/lib/modules/eq_settings/views/widgets/row_keypress_widget.dart deleted file mode 100644 index 2e772201..00000000 --- a/apps/client/lib/modules/eq_settings/views/widgets/row_keypress_widget.dart +++ /dev/null @@ -1,242 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -class RowKeypressWidget extends StatefulWidget { - final Map templateData; - - const RowKeypressWidget(this.templateData, {super.key}); - - @override - State createState() => _RowKeypressWidgetState(); -} - -class _RowKeypressWidgetState extends State { - Map get _templateData => widget.templateData; - - String _getLabel(int value) { - return _templateData['options'].firstWhere((item) => item['value'] == value, - orElse: () => {'label': ''})['label']; - } - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Column( - children: [ - Padding( - padding: EdgeInsets.symmetric(vertical: 10.w), - child: Row( - children: [ - const Spacer(), - Expanded( - child: Text( - '左', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - Expanded( - child: Text( - '右', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - ], - ), - ), - ..._templateData['data'] - .map((item) => _rowItem(isDarkMode, item)) - .toList(), - ], - ); - } - - Widget _rowItem(bool isDarkMode, Map item) { - return Row( - children: [ - Expanded( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 10.w), - child: Text( - item['label'], - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - ), - Expanded( - child: GestureDetector( - onTap: () => _radioDialog(isDarkMode, item, 'left'), - behavior: HitTestBehavior.translucent, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 10.w), - child: Text( - _getLabel(item['left']), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white : Colors.grey, - ), - ), - ), - ), - ), - Expanded( - child: GestureDetector( - onTap: () => _radioDialog(isDarkMode, item, 'right'), - behavior: HitTestBehavior.translucent, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 10.w), - child: Text( - _getLabel(item['right']), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white : Colors.grey, - ), - ), - ), - ), - ), - ], - ); - } - - void _radioDialog(bool isDarkMode, Map item, String key) { - Get.dialog( - Dialog( - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), - ), - child: Padding( - padding: EdgeInsets.all(12.w), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${item['label']}-${key == 'left' ? '左' : '右'}', - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - SizedBox(height: 16.h), - Container( - constraints: BoxConstraints(maxHeight: 0.5.sh), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: _templateData['options'] - .map((option) => - _radioItem(isDarkMode, option, item, key)) - .toList(), - ), - ), - ), - SizedBox(height: 16.h), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Get.back(), - child: Text( - 'cancel'.tr, // 取消 - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ), - ], - ), - ], - ), - ), - ), - ); - } - - Widget _radioItem(bool isDarkMode, Map option, Map item, String key) { - return GestureDetector( - onTap: () { - setState(() { - item[key] = option['value']; - }); - Map data = { - 'key': item['key'], - key: option['value'], - }; - _templateData['onCallback'](data); - Get.back(); - }, - child: Container( - margin: EdgeInsets.only(bottom: 10.h), - padding: EdgeInsets.all(10.w), - decoration: BoxDecoration( - color: item[key] == option['value'] - ? (isDarkMode - ? Colors.teal[900]!.withValues(alpha: 0.3) - : Colors.teal[50]!) - : (isDarkMode - ? Colors.white.withValues(alpha: 0.05) - : Colors.grey[100]!), - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: item[key] == option['value'] - ? (isDarkMode ? Colors.teal[300]! : Colors.teal[600]!) - : Colors.transparent, - width: 1, - ), - ), - child: Row( - children: [ - Expanded( - child: Text( - option['label'], - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.black87, - ), - overflow: TextOverflow.ellipsis, - ), - ), - Container( - width: 24.w, - height: 24.w, - decoration: BoxDecoration( - color: item[key] == option['value'] - ? (isDarkMode ? Colors.teal[300] : Colors.teal[600]) - : (isDarkMode - ? Colors.white.withValues(alpha: 0.1) - : Colors.grey[300]), - shape: BoxShape.circle, - ), - child: item[key] == option['value'] - ? Icon( - Icons.check, - size: 16.w, - color: Colors.white, - ) - : null, - ), - ], - ), - ), - ); - } -} diff --git a/apps/client/lib/modules/eq_settings/views/widgets/row_radio_widget.dart b/apps/client/lib/modules/eq_settings/views/widgets/row_radio_widget.dart deleted file mode 100644 index 6dbfd46f..00000000 --- a/apps/client/lib/modules/eq_settings/views/widgets/row_radio_widget.dart +++ /dev/null @@ -1,187 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -class RowRadioWidget extends StatefulWidget { - final Map templateData; - - const RowRadioWidget(this.templateData, {super.key}); - - @override - State createState() => _RowRadioWidgetState(); -} - -class _RowRadioWidgetState extends State { - Map get _templateData => widget.templateData; - - String _label = ''; - - @override - void initState() { - super.initState(); - _label = _templateData['options'].firstWhere( - (option) => option['value'] == _templateData['value'], - orElse: () => {'label': ''}, - )['label']; - } - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return GestureDetector( - onTap: () => _radioDialog(isDarkMode), - behavior: HitTestBehavior.translucent, - child: Padding( - padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), - child: Row( - children: [ - Expanded( - child: Text( - _templateData['name'], - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - Text( - _label, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white : Colors.grey, - ), - ), - Icon( - Icons.chevron_right, - size: 26.w, - color: Colors.blue, - ), - ], - ), - ), - ); - } - - void _radioDialog(bool isDarkMode) { - Get.dialog( - Dialog( - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), - ), - child: Padding( - padding: EdgeInsets.all(12.w), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _templateData['name'], - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - SizedBox(height: 16.h), - Container( - constraints: BoxConstraints(maxHeight: 0.5.sh), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: _templateData['options'] - .map((option) => _item(isDarkMode, option)) - .toList(), - ), - ), - ), - SizedBox(height: 16.h), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Get.back(), - child: Text( - 'cancel'.tr, // 取消 - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ), - ], - ), - ], - ), - ), - ), - ); - } - - Widget _item(bool isDarkMode, Map option) { - return GestureDetector( - onTap: () { - setState(() { - _label = option['label']; - }); - _templateData['value'] = option['value']; - _templateData['onCallback'](option['value']); - Get.back(); - }, - child: Container( - margin: EdgeInsets.only(bottom: 10.h), - padding: EdgeInsets.all(10.w), - decoration: BoxDecoration( - color: _templateData['value'] == option['value'] - ? (isDarkMode - ? Colors.teal[900]!.withValues(alpha: 0.3) - : Colors.teal[50]!) - : (isDarkMode - ? Colors.white.withValues(alpha: 0.05) - : Colors.grey[100]!), - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: _templateData['value'] == option['value'] - ? (isDarkMode ? Colors.teal[300]! : Colors.teal[600]!) - : Colors.transparent, - width: 1, - ), - ), - child: Row( - children: [ - Expanded( - child: Text( - option['label'], - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.black87, - ), - overflow: TextOverflow.ellipsis, - ), - ), - Container( - width: 24.w, - height: 24.w, - decoration: BoxDecoration( - color: _templateData['value'] == option['value'] - ? (isDarkMode ? Colors.teal[300] : Colors.teal[600]) - : (isDarkMode - ? Colors.white.withValues(alpha: 0.1) - : Colors.grey[300]), - shape: BoxShape.circle, - ), - child: _templateData['value'] == option['value'] - ? Icon( - Icons.check, - size: 16.w, - color: Colors.white, - ) - : null, - ), - ], - ), - ), - ); - } -} diff --git a/apps/client/lib/modules/eq_settings/views/widgets/row_switch_widget.dart b/apps/client/lib/modules/eq_settings/views/widgets/row_switch_widget.dart deleted file mode 100644 index e21223ba..00000000 --- a/apps/client/lib/modules/eq_settings/views/widgets/row_switch_widget.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class RowSwitchWidget extends StatefulWidget { - final Map templateData; - - const RowSwitchWidget(this.templateData, {super.key}); - - @override - State createState() => _RowSwitchWidgetState(); -} - -class _RowSwitchWidgetState extends State { - Map get _templateData => widget.templateData; - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Padding( - padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), - child: Row( - children: [ - Expanded( - child: Text( - _templateData['name'], - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - if (_templateData['description'] != null) - Text( - _templateData['description'], - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white : Colors.grey, - ), - ), - SizedBox( - height: 26.w, - child: Transform.scale( - scale: 0.8, - alignment: Alignment.centerRight, - child: Switch( - value: _templateData['value'], - onChanged: (bool value) { - setState(() { - _templateData['value'] = value; - }); - _templateData['onCallback'](value); - }, - ), - ), - ), - ], - ), - ); - } -} diff --git a/apps/client/lib/modules/eq_settings/views/widgets/row_timer_widget.dart b/apps/client/lib/modules/eq_settings/views/widgets/row_timer_widget.dart deleted file mode 100644 index 2a950f60..00000000 --- a/apps/client/lib/modules/eq_settings/views/widgets/row_timer_widget.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -import 'select_timer_widget.dart'; - -class RowTimerWidget extends StatefulWidget { - final Map templateData; - - const RowTimerWidget(this.templateData, {super.key}); - - @override - State createState() => _RowTimerWidgetState(); -} - -class _RowTimerWidgetState extends State { - Map get _templateData => widget.templateData; - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return GestureDetector( - onTap: () => _timerBottomSheet(isDarkMode), - behavior: HitTestBehavior.translucent, - child: Padding( - padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w), - child: Row( - children: [ - Expanded( - child: Text( - _templateData['name'], - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - if (_templateData['value'] > 0) - Text( - '${_templateData['value']}分钟', - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white : Colors.grey, - ), - ), - Icon( - Icons.chevron_right, - size: 26.w, - color: Colors.blue, - ), - ], - ), - ), - ); - } - - void _timerBottomSheet(bool isDarkMode) { - Get.bottomSheet( - SelectTimerWidget(_templateData['name'], _templateData['value']), - isScrollControlled: true, - backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), - ), - ), - ).then((value) { - if (value != null) { - setState(() { - _templateData['value'] = value; - }); - _templateData['onCallback'](value); - } - }); - } -} diff --git a/apps/client/lib/modules/eq_settings/views/widgets/select_equalizer_widget.dart b/apps/client/lib/modules/eq_settings/views/widgets/select_equalizer_widget.dart deleted file mode 100644 index 60178212..00000000 --- a/apps/client/lib/modules/eq_settings/views/widgets/select_equalizer_widget.dart +++ /dev/null @@ -1,308 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; - -import 'settings_input_widget.dart'; - -class SelectEqualizerWidget extends StatefulWidget { - final Map templateData; - - const SelectEqualizerWidget(this.templateData, {super.key}); - - @override - State createState() => _SelectEqualizerWidgetState(); -} - -class _SelectEqualizerWidgetState extends State { - Map get _templateData => widget.templateData; - - List _values = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - - bool isEdit = false; - - final GetStorage _storage = GetStorage(); - List _equalizerOptions = []; - - @override - void initState() { - super.initState(); - if (_templateData['value'].isNotEmpty) { - _values = List.from(_templateData['value']); - } - _equalizerOptions = _storage.read('equalizer_options') ?? []; - } - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - appBar: _buildAppBar(isDarkMode), - body: SafeArea( - top: false, - child: _buildBody(isDarkMode), - ), - ); - } - - // 构建应用栏 - PreferredSizeWidget _buildAppBar(bool isDarkMode) { - return AppBar( - backgroundColor: isDarkMode ? Colors.black : Colors.white, - surfaceTintColor: Colors.transparent, - elevation: 0.5, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black, - size: 20.sp, - ), - onPressed: () => Get.back(), - ), - title: Text( - _templateData['name'], - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - centerTitle: true, - actions: [ - GestureDetector( - onTap: () { - setState(() { - isEdit = !isEdit; - }); - }, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 12.w, - vertical: 6.w, - ), - child: Text( - isEdit ? 'cancel'.tr : 'edit'.tr, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 16.sp, - ), - ), - ), - ), - ], - ); - } - - // 构建主体内容 - Widget _buildBody(bool isDarkMode) { - bool isAdd = true; - - return ListView( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), - children: [ - _equalizer(isDarkMode), - Wrap( - spacing: 10.w, - runSpacing: 10.w, - children: [ - ..._templateData['options'].map((Map option) { - bool isSelected = listEquals(_values, option['value']); - if (isSelected) { - isAdd = false; - } - return _item(isDarkMode, option, isSelected); - }).toList(), - if (isAdd) - GestureDetector( - onTap: () => _inputBottomSheet(isDarkMode), - child: Container( - width: 110.w, - height: 110.w, - alignment: Alignment.center, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.05) - : Colors.white, - borderRadius: BorderRadius.circular(8.w), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withValues(alpha: 0.2) - : Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Icon( - Icons.add, - size: 40.sp, - color: Colors.green, - ), - ), - ), - ], - ), - ], - ); - } - - Widget _equalizer(bool isDarkMode) { - return Container( - height: 220.w, - margin: EdgeInsets.only(bottom: 5.w), - child: Row( - children: List.generate( - _templateData['xAxis'].length, - (index) => Expanded( - child: Stack( - children: [ - RotatedBox( - quarterTurns: 3, - child: StatefulBuilder( - builder: (BuildContext context, setStateful) { - return Slider( - value: _values[index].toDouble(), - min: -5, - max: 5, - divisions: 10, - activeColor: Colors.green, - onChanged: (value) { - setStateful(() { - _values[index] = value.toInt(); - }); - }, - onChangeEnd: (value) { - setState(() {}); - }, - ); - }, - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Text( - _templateData['xAxis'][index], - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.grey, - ), - ), - ), - ], - ), - ), - ), - ), - ); - } - - Widget _item(bool isDarkMode, Map option, bool isSelected) { - return GestureDetector( - onTap: () { - _templateData['value'] = option['value']; - Get.back(result: true); - }, - child: Stack( - children: [ - Container( - width: 110.w, - height: 110.w, - alignment: Alignment.center, - decoration: BoxDecoration( - color: isSelected - ? Colors.green - : isDarkMode - ? Colors.white.withValues(alpha: 0.05) - : Colors.white, - borderRadius: BorderRadius.circular(8.w), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withValues(alpha: 0.2) - : Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Text( - option['label'], - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isSelected - ? Colors.white - : isDarkMode - ? Colors.white - : Colors.black, - ), - ), - ), - if (isEdit && !option['default']) - Positioned( - top: 4.w, - right: 4.w, - child: GestureDetector( - onTap: () { - if (isSelected) { - _templateData['value'] = []; - _templateData['onCallback']([]); - } - _equalizerOptions.removeWhere( - (e) => listEquals(e['value'], option['value'])); - _storage.write('equalizer_options', _equalizerOptions); - setState(() { - _templateData['options'].remove(option); - }); - }, - child: Container( - width: 24.w, - height: 24.w, - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(12.w), - ), - child: Icon( - Icons.close, - size: 16.sp, - color: Colors.white, - ), - ), - ), - ), - ], - ), - ); - } - - void _inputBottomSheet(bool isDarkMode) { - Get.bottomSheet( - const SettingsInputWidget('添加均衡器', ''), - isScrollControlled: true, - backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), - ), - ), - ).then((value) { - if (value != null) { - Map option = { - 'label': value, - 'value': List.from(_values), - 'default': false, - }; - _equalizerOptions.add(option); - _storage.write('equalizer_options', _equalizerOptions); - setState(() { - _templateData['options'].add(option); - }); - } - }); - } -} diff --git a/apps/client/lib/modules/eq_settings/views/widgets/select_timer_widget.dart b/apps/client/lib/modules/eq_settings/views/widgets/select_timer_widget.dart deleted file mode 100644 index ec082d4e..00000000 --- a/apps/client/lib/modules/eq_settings/views/widgets/select_timer_widget.dart +++ /dev/null @@ -1,231 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -class SelectTimerWidget extends StatefulWidget { - final String title; - final int time; - - const SelectTimerWidget(this.title, this.time, {super.key}); - - @override - State createState() => _SelectTimerWidgetState(); -} - -class _SelectTimerWidgetState extends State { - late FixedExtentScrollController _hourScrollController; - late FixedExtentScrollController _minuteScrollController; - - int _hour = 0; - int _minute = 0; - - @override - void initState() { - super.initState(); - _hour = widget.time ~/ 60; - _minute = widget.time % 60; - _hourScrollController = FixedExtentScrollController(initialItem: _hour); - _minuteScrollController = FixedExtentScrollController(initialItem: _minute); - } - - @override - void dispose() { - super.dispose(); - _hourScrollController.dispose(); - _minuteScrollController.dispose(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: EdgeInsets.symmetric(vertical: 10.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.title, - style: TextStyle( - fontSize: 16.sp, - color: Colors.black87, - fontWeight: FontWeight.bold, - ), - ), - GestureDetector( - onTap: () => Get.back(), - child: Container( - width: 26.w, - height: 26.w, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(26.r), - ), - child: Icon( - Icons.close, - size: 18.w, - color: Colors.grey[400], - ), - ), - ) - ], - ), - ), - Divider( - height: 1, - color: Colors.grey[300], - ), - SizedBox( - height: 220.w, - child: Row( - children: [ - Expanded( - child: CupertinoPicker.builder( - itemExtent: 40.w, - diameterRatio: 1.1, - squeeze: 1.25, - childCount: 24, - selectionOverlay: _selectionOverlayWidget( - isRadiusLeft: true, - ), - scrollController: _hourScrollController, - itemBuilder: (BuildContext context, int index) { - return Container( - padding: EdgeInsets.only(right: 10.w), - alignment: Alignment.centerRight, - child: Text( - '$index时', - style: _hour == index - ? TextStyle( - color: const Color(0xFFFF5561), - fontSize: 15.sp, - fontWeight: FontWeight.w500, - ) - : TextStyle( - color: const Color(0xFF9D9D9D), - fontSize: 15.sp, - ), - ), - ); - }, - onSelectedItemChanged: (int index) { - setState(() { - _hour = index; - }); - }, - ), - ), - Expanded( - child: CupertinoPicker.builder( - itemExtent: 40.w, - diameterRatio: 1.1, - squeeze: 1.25, - childCount: 60, - selectionOverlay: _selectionOverlayWidget( - isRadiusRight: true, - ), - scrollController: _minuteScrollController, - itemBuilder: (BuildContext context, int index) { - return Container( - padding: EdgeInsets.only(left: 10.w), - alignment: Alignment.centerLeft, - child: Text( - '$index分', - style: _minute == index - ? TextStyle( - color: const Color(0xFFFF5561), - fontSize: 15.sp, - fontWeight: FontWeight.w500, - ) - : TextStyle( - color: const Color(0xFF9D9D9D), - fontSize: 15.sp, - ), - ), - ); - }, - onSelectedItemChanged: (int index) { - setState(() { - _minute = index; - }); - }, - ), - ), - ], - ), - ), - Padding( - padding: EdgeInsets.only(bottom: 20.w), - child: Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () => Get.back(), - child: Container( - height: 48.w, - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(12.r), - ), - child: Text( - 'cancel'.tr, // 取消 - style: TextStyle( - fontSize: 16.sp, - color: Colors.black, - ), - ), - ), - ), - ), - 10.horizontalSpace, - Expanded( - child: GestureDetector( - onTap: () { - int time = _hour * 60 + _minute; - Get.back(result: time); - }, - child: Container( - height: 48.w, - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(12.r), - ), - child: Text( - 'confirm'.tr, // 确认 - style: TextStyle( - fontSize: 16.sp, - color: Colors.white, - ), - ), - ), - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _selectionOverlayWidget({ - bool isRadiusLeft = false, - bool isRadiusRight = false, - }) { - return Container( - decoration: BoxDecoration( - color: const Color.fromARGB(45, 255, 85, 97), - borderRadius: BorderRadius.horizontal( - left: isRadiusLeft ? Radius.circular(8.w) : Radius.zero, - right: isRadiusRight ? Radius.circular(8.w) : Radius.zero, - ), - ), - ); - } -} diff --git a/apps/client/lib/modules/eq_settings/views/widgets/settings_input_widget.dart b/apps/client/lib/modules/eq_settings/views/widgets/settings_input_widget.dart deleted file mode 100644 index ea41dd49..00000000 --- a/apps/client/lib/modules/eq_settings/views/widgets/settings_input_widget.dart +++ /dev/null @@ -1,162 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; - -class SettingsInputWidget extends StatefulWidget { - final String title; - final String inputText; - - const SettingsInputWidget(this.title, this.inputText, {super.key}); - - @override - State createState() => _SettingsInputWidgetState(); -} - -class _SettingsInputWidgetState extends State { - final TextEditingController _inputController = TextEditingController(); - - @override - void initState() { - super.initState(); - _inputController.text = widget.inputText; - } - - @override - void dispose() { - _inputController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: EdgeInsets.symmetric(vertical: 10.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.title, - style: TextStyle( - fontSize: 16.sp, - color: Colors.black87, - fontWeight: FontWeight.bold, - ), - ), - GestureDetector( - onTap: () => Get.back(), - child: Container( - width: 26.w, - height: 26.w, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(26.r), - ), - child: Icon( - Icons.close, - size: 18.w, - color: Colors.grey[400], - ), - ), - ) - ], - ), - ), - Divider( - height: 1, - color: Colors.grey[300], - ), - Container( - margin: EdgeInsets.symmetric(vertical: 20.w), - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12.w), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: TextField( - controller: _inputController, - style: TextStyle( - color: Colors.black87, - fontSize: 16.sp, - ), - decoration: InputDecoration( - hintText: 'maxCharacters'.tr, - hintStyle: TextStyle( - color: Colors.black54, - fontSize: 16.sp, - ), - isDense: true, - fillColor: Colors.transparent, - border: InputBorder.none, - contentPadding: EdgeInsets.all(10.w), - ), - ), - ), - Padding( - padding: EdgeInsets.only(bottom: 20.w), - child: Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () => Get.back(), - child: Container( - height: 48.w, - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(12.r), - ), - child: Text( - 'cancel'.tr, // 取消 - style: TextStyle( - fontSize: 16.sp, - color: Colors.black, - ), - ), - ), - ), - ), - 10.horizontalSpace, - Expanded( - child: GestureDetector( - onTap: () { - if (_inputController.text.isNotEmpty) { - Get.back(result: _inputController.text); - } - }, - child: Container( - height: 48.w, - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(12.r), - ), - child: Text( - 'confirm'.tr, // 确认 - style: TextStyle( - fontSize: 16.sp, - color: Colors.white, - ), - ), - ), - ), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/apps/client/lib/modules/home/bindings/home_binding.dart b/apps/client/lib/modules/home/bindings/home_binding.dart deleted file mode 100644 index a9439e66..00000000 --- a/apps/client/lib/modules/home/bindings/home_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/home_controller.dart'; -import '../controllers/calendar_controller.dart'; - -class HomeBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => HomeController()); - // 首页日历面板控制器 - Get.lazyPut(() => CalendarController()); - } -} diff --git a/apps/client/lib/modules/home/views/home_view.dart b/apps/client/lib/modules/home/views/home_view.dart deleted file mode 100644 index 7b16b5d9..00000000 --- a/apps/client/lib/modules/home/views/home_view.dart +++ /dev/null @@ -1,520 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '../controllers/home_controller.dart'; -import '../../../routes/app_routes.dart'; -import 'widgets/calendar/calendar_card.dart'; -import 'widgets/calendar/calendar_task_list.dart'; -import '../controllers/calendar_controller.dart'; - -// 首页视图 -class HomeView extends GetView { - const HomeView({Key? key}) : super(key: key); - - // 功能卡片模块(语音记录/翻译/通话录音等)折叠态下的目标可视高度: - // 固定预算而非内容自然高度,避免卡片变多时撑爆首页固定布局; - // 展示不下的部分(如通话录音)在模块内部滚动查看。 - static const double _extrasTargetHeight = 420; - - @override - Widget build(BuildContext context) { - final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; - return Scaffold( - backgroundColor: Colors.transparent, - // 背景:顶部「左淡蓝·中浅蓝·右淡暖」横向晕染,向下渐隐到白(取自参考图真实色值) - body: Stack( - children: [ - // 1) 顶部横向色带(浅色模式)/ 深色竖直渐变(深色模式) - Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - gradient: isDarkMode - ? const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color(0xFF0A0F1A), Color(0xFF000000)], - ) - : const LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Color(0xFFD4EAFF), // 左:淡蓝 - Color(0xFFE6F3FE), // 中:浅蓝 - Color(0xFFFBF2F0), // 右:淡暖(浅奶粉) - ], - ), - ), - ), - ), - // 2) 竖直方向把顶部色带渐隐到白(仅浅色模式) - if (!isDarkMode) - const Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0x00FFFFFF), // 顶部透明:露出横向色带 - Color(0x00FFFFFF), - Color(0xFFFFFFFF), // 向下渐变为白 - ], - stops: [0.0, 0.15, 0.42], - ), - ), - ), - ), - // 内容(固定布局:日历展开时任务列表铺满并覆盖下方翻译卡片/AI) - SafeArea( - child: Padding( - // 横向不留白:任务列表需铺满到屏幕边(在屏幕边裁切,阴影完整); - // 其余元素各自加 16 横向内边距对齐 - padding: EdgeInsets.only(top: 16.w, bottom: 8.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 顶部标题栏(固定) - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w), - child: _buildAppBar(context, isDarkMode), - ), - SizedBox(height: 16.h), - // 日历 + 任务列表 + 翻译卡片/AI(随日历折叠/展开切换) - Expanded(child: _buildCalendarArea(isDarkMode)), - ], - ), - ), - ), - ], - ), - ); - } - - // 日历区域:日历卡片 + 任务列表(Expanded) + 翻译卡片/AI(随展开收起) - Widget _buildCalendarArea(bool isDarkMode) { - final cal = Get.find(); - return LayoutBuilder( - builder: (context, constraints) { - final double h = constraints.maxHeight; - // 极小高度(超大字体/超小屏)兜底:退回可滚动布局,避免溢出 - if (h < 360.h) { - return _buildCalendarScrollFallback(isDarkMode); - } - // 月历网格最大高度夹取:留出间距 + 最小任务列表 + 卡片其余高度,避免 Expanded 负高 - final double maxBody = h - 16.h - 96.h - 130.h; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w), - child: CalendarCard(maxBodyHeight: maxBody > 0 ? maxBody : null), - ), - SizedBox(height: 16.h), - // 任务列表:填满任务区(连续高度,便于折叠/展开两层按下拉距离交叉淡入淡出)。 - const Expanded(child: CalendarTaskList()), - // 翻译卡片 + AI:随日历展开(dragProgress→0)从底部收起、被覆盖。 - // 用固定目标高度(非内容自然高度)做插值:卡片模块内容一旦变多(如新增 - // "通话录音")就不会撑爆这个不滚动的整屏布局;模块内部改用可滚动容器 - // 兜底——展示不下的卡片(如通话录音)靠上下滑动查看,而不是让布局溢出。 - Obx(() => ClipRect( - child: SizedBox( - height: _extrasTargetHeight.h * cal.dragProgress.value, - child: IgnorePointer( - ignoring: cal.dragProgress.value < 1.0, - child: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 16.w), - physics: const BouncingScrollPhysics(), - child: _buildExtrasBlock(isDarkMode), - ), - ), - ), - )), - ], - ); - }, - ); - } - - // 翻译功能卡片(折叠态显示在任务列表下方;展开态被收起覆盖) - // AI 球已改为全局悬浮球(core/widgets/global_ai_orb.dart),不再嵌在首页里 - Widget _buildExtrasBlock(bool isDarkMode) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 16.h), - _buildFeatureGrid(isDarkMode), - SizedBox(height: 24.h), - ], - ); - } - - // 极小屏兜底:可滚动布局(任务列表给有限高度) - Widget _buildCalendarScrollFallback(bool isDarkMode) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w), - child: const CalendarCard(), - ), - SizedBox(height: 16.h), - SizedBox(height: 280.h, child: const CalendarTaskList()), - SizedBox(height: 16.h), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w), - child: _buildFeatureGrid(isDarkMode), - ), - SizedBox(height: 24.h), - ], - ), - ); - } - - // 顶部标题栏:左侧设备管理,右侧“语音打断”开关 + 设置入口 - Widget _buildAppBar(BuildContext context, bool isDarkMode) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // 左侧:设备管理入口 - _buildDeviceManager(isDarkMode), - // 右侧:标语 + 设置图标 - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'YM开启科技人生', - style: TextStyle( - fontSize: 13.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white70 : Colors.grey[700], - ), - ), - SizedBox(width: 8.w), - _buildSettingsEntry(isDarkMode), - ], - ), - ], - ); - } - - // 左侧设备管理:未连接显示“添加设备”,已连接显示电量 - Widget _buildDeviceManager(bool isDarkMode) { - final Color pillBg = isDarkMode ? const Color(0xFF1A1A1A) : Colors.white; - final Color textColor = isDarkMode ? Colors.white : const Color(0xFF222222); - return Obx(() { - final bool connected = controller.isFullyConnectedRx.value; - return GestureDetector( - behavior: HitTestBehavior.opaque, - // 未连接 → 扫描页;已连接 → 我的设备 - onTap: () => Get.toNamed(connected ? Routes.myDevices : Routes.devices), - child: Container( - padding: EdgeInsets.fromLTRB(6.w, 6.h, 14.w, 6.h), - decoration: BoxDecoration( - color: pillBg, - borderRadius: BorderRadius.circular(24.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(isDarkMode ? 0.25 : 0.08), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // 蓝色圆形图标 - Container( - width: 26.w, - height: 26.w, - decoration: const BoxDecoration( - color: Color(0xFF3B82F6), - shape: BoxShape.circle, - ), - child: Icon( - connected ? Icons.battery_full : Icons.add, - color: Colors.white, - size: connected ? 16.sp : 18.sp, - ), - ), - SizedBox(width: 8.w), - Text( - connected ? controller.getBatteryText() : 'addDevice'.tr, - style: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.bold, - color: textColor, - ), - ), - ], - ), - ), - ); - }); - } - - // 右侧“语音打断”开关(保留原有逻辑与样式) - // 设置入口:头像/人形图标,点击进入设置页 - Widget _buildSettingsEntry(bool isDarkMode) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => Get.toNamed(Routes.settings), - child: Container( - width: 36.w, - height: 36.w, - decoration: BoxDecoration( - color: isDarkMode - ? const Color(0xFF2A2A2A) - : Colors.white.withOpacity(0.7), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(isDarkMode ? 0.25 : 0.08), - blurRadius: 6, - offset: const Offset(0, 2), - ), - ], - ), - child: Icon( - Icons.person, - size: 22.sp, - color: isDarkMode ? Colors.white70 : Colors.grey[700], - ), - ), - ); - } - - // 功能卡片区:顶部(通话录音/现场录音,依赖设备连接) + 左1大(语音记录助手) + - // 右2(同声翻译/面对面翻译) + 下2(通话翻译/多媒体翻译) - Widget _buildFeatureGrid(bool isDarkMode) { - const double smallH = 84; // 小卡高度 - const double gap = 12; // 卡片间距 - return Column( - children: [ - // 顶区:通话录音 + 现场录音(依赖设备连接,未连接时置灰/透明) - Obx(() { - final bool connected = controller.isFullyConnectedRx.value; - return Row( - children: [ - Expanded( - child: _buildFeatureCard( - title: '通话录音', - icon: Icons.fiber_manual_record, - gradient: const [Color(0xFFEF5350), Color(0xFFE53935)], - height: smallH.h, - isDarkMode: isDarkMode, - enabled: connected, - onTap: controller.openCallRecording, - ), - ), - SizedBox(width: gap.w), - Expanded( - child: _buildFeatureCard( - title: '现场录音', - icon: Icons.mic, - gradient: const [Color(0xFF26C6DA), Color(0xFF00838F)], - height: smallH.h, - isDarkMode: isDarkMode, - onTap: controller.openFieldRecording, - ), - ), - ], - ); - }), - SizedBox(height: gap.h), - // 上区:左侧高卡 + 右侧两张卡 - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: _buildFeatureCard( - title: '语音记录助手', - subtitle: '实时记录各种场景语音', - icon: Icons.groups_2, - gradient: const [Color(0xFFAB47BC), Color(0xFFEC407A)], - height: (smallH * 2 + gap).h, - iconOnTop: true, - isDarkMode: isDarkMode, - onTap: controller.openMeet, // → 会议(Routes.meeting) - ), - ), - SizedBox(width: gap.w), - Expanded( - child: Column( - children: [ - _buildFeatureCard( - title: '同声翻译', - icon: Icons.record_voice_over, - gradient: const [Color(0xFF4CAF50), Color(0xFF009688)], - height: smallH.h, - isDarkMode: isDarkMode, - onTap: () => - controller.openTranslationFeature('simultaneous'), - ), - SizedBox(height: gap.h), - _buildFeatureCard( - title: '面对面翻译', - icon: Icons.forum, - gradient: const [Color(0xFF42A5F5), Color(0xFF1E88E5)], - height: smallH.h, - isDarkMode: isDarkMode, - onTap: () => - controller.openTranslationFeature('faceToFace'), - ), - ], - ), - ), - ], - ), - SizedBox(height: gap.h), - // 下区:通话翻译 + 多媒体翻译(依赖设备连接,未连接时置灰/透明) - Obx(() { - final bool connected = controller.isFullyConnectedRx.value; - return Row( - children: [ - Expanded( - child: _buildFeatureCard( - title: '通话翻译', - icon: Icons.phone_in_talk, - gradient: const [Color(0xFFFFA726), Color(0xFFFB8C00)], - height: smallH.h, - isDarkMode: isDarkMode, - enabled: connected, - onTap: () => controller.openTranslationFeature('call'), - ), - ), - SizedBox(width: gap.w), - Expanded( - child: _buildFeatureCard( - title: '多媒体翻译', - icon: Icons.photo_camera, - gradient: const [Color(0xFF5C6BC0), Color(0xFF3949AB)], - height: smallH.h, - isDarkMode: isDarkMode, - onTap: () => controller.openTranslationFeature('audioVideo'), - ), - ), - ], - ); - }), - ], - ); - } - - // 通用功能卡片 - Widget _buildFeatureCard({ - required String title, - String? subtitle, - required IconData icon, - required List gradient, - required double height, - required bool isDarkMode, - required VoidCallback onTap, - bool iconOnTop = false, - bool enabled = true, // false 时整卡变透明(未连接设备) - }) { - final Color cardBg = isDarkMode ? const Color(0xFF1A1A1A) : Colors.white; - final Color titleColor = - isDarkMode ? Colors.white : const Color(0xFF222222); - final Color subColor = - isDarkMode ? Colors.white60 : const Color(0xFF888888); - final Widget iconBox = Container( - width: 42.w, - height: 42.w, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: gradient, - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(12.r), - ), - child: Icon(icon, color: Colors.white, size: 22.sp), - ); - return Opacity( - opacity: enabled ? 1.0 : 0.4, // 未连接:整卡变透明(仍可点击 → 弹"请连接设备") - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: Container( - height: height, - padding: EdgeInsets.all(14.w), - decoration: BoxDecoration( - color: cardBg, - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(isDarkMode ? 0.06 : 0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: iconOnTop - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - iconBox, - const Spacer(), - Text( - title, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - color: titleColor, - ), - ), - if (subtitle != null) ...[ - SizedBox(height: 4.h), - Text( - subtitle, - style: TextStyle(fontSize: 12.sp, color: subColor), - ), - ], - ], - ) - : Row( - children: [ - iconBox, - SizedBox(width: 12.w), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, - style: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.bold, - color: titleColor, - ), - ), - if (subtitle != null) ...[ - SizedBox(height: 2.h), - Text( - subtitle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, - style: - TextStyle(fontSize: 11.sp, color: subColor), - ), - ], - ], - ), - ), - ], - ), - ), - ), - ); - } - -} diff --git a/apps/client/lib/modules/meeting/utils/jieli_call_recorder.dart b/apps/client/lib/modules/meeting/utils/jieli_call_recorder.dart deleted file mode 100644 index e611ef9d..00000000 --- a/apps/client/lib/modules/meeting/utils/jieli_call_recorder.dart +++ /dev/null @@ -1,242 +0,0 @@ -// 通话录音 Dart 端 PCM 写入器(设备录音通路 / stereo) -// -// 早先曾走过两版: -// 1) deviceRecordStart (MODE_CALL_TRANSLATION + ch=1, ALWAYS_RECORDING): -// 监听 uplink + downlink 两路 mono 配对写 stereo WAV。 -// 问题:那条路是为真实 SCO 电话通话准备的,没有通话期间设备不会推任何 -// PCM,本地测试无法采样。 -// 2) assistantStart (MODE_RECORD + ch=1, ALWAYS_RECORDING): -// 只能拿到本端单声道,对端音丢失,无法满足"会议记录"对双方音轨的需求。 -// -// 当前实现回到 deviceRecordStart,但 native 已经把 mode 切到 -// MODE_CALL_TRANSLATION_WITH_STEREO + ch=2 + STRATEGY_DEVICE_ALWAYS_RECORDING。 -// 耳机持续上推 SOURCE_E_SCO_MIX 双声道交织 OPUS(与真实通话状态解耦), -// 由 device_jieli 解码成 16k/16bit/stereo PCM(L=本端 R=对端)后,按 -// [DeviceRecordAudioEvent] 推到 Dart。本类直接写成标准 stereo WAV。 -// -// 注意:device_jieli 设备录音通路无原生 pause API,本类的暂停语义通过 -// 在 Dart 层丢弃接收到的 PCM 帧实现。 -import 'dart:async'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:device_jieli/device_jieli.dart'; - -import '/core/utils/logger.dart'; - -class JieliCallRecorder { - JieliCallRecorder({ - this.sampleRate = 16000, - this.bitsPerSample = 16, - this.onWavePcm, - }); - - /// 采样率(Hz)。device_jieli 设备录音通路固定 16000。 - final int sampleRate; - - /// 位深,固定 16bit。 - final int bitsPerSample; - - /// 写入文件后的 stereo PCM 通知(与 WAV 同步),用于驱动上层波形/振幅。 - /// payload 是 stereo 交织 PCM,调用方如需做 mono 振幅可自取一边或求平均。 - final void Function(Uint8List pcm)? onWavePcm; - - static const String _tag = 'JieliCallRecorder'; - static const int _channels = 2; // stereo: L=本端 R=对端 - - StreamSubscription? _eventSub; - RandomAccessFile? _raf; - String? _filePath; - int _dataBytes = 0; - bool _paused = false; - bool _running = false; - - bool get isRunning => _running; - String? get filePath => _filePath; - - /// 启动录音:写 WAV 头占位、订阅事件、调起设备录音通路。 - Future start(String filePath) async { - if (_running) { - Logger.w(_tag, '重复 start 调用,已忽略'); - return false; - } - try { - final file = File(filePath); - final dir = file.parent; - if (!await dir.exists()) { - await dir.create(recursive: true); - } - if (await file.exists()) { - await file.delete(); - } - _raf = await file.open(mode: FileMode.write); - await _raf!.writeFrom(_makeWavHeader(0)); - - _dataBytes = 0; - _filePath = filePath; - _paused = false; - - _eventSub = Jielihome.instance.events.listen(_onEvent); - - try { - // 防御性先 stop,避免外部已有录音通路在跑导致 start 行为不确定 - try { - await Jielihome.instance.deviceRecordStop(); - } catch (_) {} - await Jielihome.instance.deviceRecordStart(sampleRate: sampleRate); - } catch (e) { - Logger.error('$_tag deviceRecordStart 失败: $e'); - await _shutdown(saveFile: false); - return false; - } - - _running = true; - Logger.info( - '$_tag 启动通话录音 path=$filePath sr=$sampleRate ch=$_channels (设备录音通路 / stereo)'); - return true; - } catch (e) { - Logger.error('$_tag 启动失败: $e'); - await _shutdown(saveFile: false); - return false; - } - } - - /// 暂停:仅在 Dart 层丢帧;设备端继续推流。 - void pause() { - if (!_running) return; - _paused = true; - } - - /// 恢复接收设备端帧。 - void resume() { - if (!_running) return; - _paused = false; - } - - /// 停止录音;[save] 为 false 时回滚删除文件。 - Future stop({required bool save}) async { - if (!_running) { - // 即便外部异常重复 stop,也保证幂等 - await _shutdown(saveFile: save); - return true; - } - _running = false; - try { - try { - await Jielihome.instance.deviceRecordStop(); - } catch (e) { - Logger.w(_tag, 'deviceRecordStop 失败(忽略): $e'); - } - await _shutdown(saveFile: save); - return true; - } catch (e) { - Logger.error('$_tag 停止失败: $e'); - return false; - } - } - - Future _shutdown({required bool saveFile}) async { - await _eventSub?.cancel(); - _eventSub = null; - final raf = _raf; - final path = _filePath; - _raf = null; - _filePath = null; - _paused = false; - - if (raf != null) { - try { - if (saveFile) { - await raf.setPosition(0); - await raf.writeFrom(_makeWavHeader(_dataBytes)); - } - } catch (e) { - Logger.error('$_tag 写 WAV 头失败: $e'); - } finally { - try { - await raf.flush(); - } catch (_) {} - try { - await raf.close(); - } catch (_) {} - } - } - - if (!saveFile && path != null) { - try { - final f = File(path); - if (await f.exists()) { - await f.delete(); - } - } catch (e) { - Logger.w(_tag, '删除录音文件失败: $e'); - } - } - _dataBytes = 0; - } - - void _onEvent(JieliEvent event) { - if (event is DeviceRecordAudioEvent) { - if (_paused) return; - if (event.pcm.isEmpty) return; - _writePcm(event.pcm); - } else if (event is DeviceRecordErrorEvent) { - Logger.error( - '$_tag 设备录音通路错误: code=${event.code} msg=${event.message ?? ''}'); - } - } - - void _writePcm(Uint8List pcm) { - final raf = _raf; - if (raf == null) return; - try { - raf.writeFromSync(pcm); - _dataBytes += pcm.length; - } catch (e) { - Logger.error('$_tag 写 PCM 失败: $e'); - return; - } - onWavePcm?.call(pcm); - } - - /// 标准 PCM WAV 头(44 字节)。dataLength=0 用于占位,stop 时回填实际值。 - /// 头里的 channels=2 / blockAlign=4 反映 stereo 交织 PCM 格式。 - Uint8List _makeWavHeader(int dataLength) { - final byteRate = sampleRate * _channels * (bitsPerSample ~/ 8); - final blockAlign = _channels * (bitsPerSample ~/ 8); - final totalLength = 36 + dataLength; - final h = ByteData(44); - - // "RIFF" - h.setUint8(0, 0x52); - h.setUint8(1, 0x49); - h.setUint8(2, 0x46); - h.setUint8(3, 0x46); - h.setUint32(4, totalLength, Endian.little); - // "WAVE" - h.setUint8(8, 0x57); - h.setUint8(9, 0x41); - h.setUint8(10, 0x56); - h.setUint8(11, 0x45); - // "fmt " - h.setUint8(12, 0x66); - h.setUint8(13, 0x6d); - h.setUint8(14, 0x74); - h.setUint8(15, 0x20); - h.setUint32(16, 16, Endian.little); // fmt chunk size - h.setUint16(20, 1, Endian.little); // PCM - h.setUint16(22, _channels, Endian.little); - h.setUint32(24, sampleRate, Endian.little); - h.setUint32(28, byteRate, Endian.little); - h.setUint16(32, blockAlign, Endian.little); - h.setUint16(34, bitsPerSample, Endian.little); - // "data" - h.setUint8(36, 0x64); - h.setUint8(37, 0x61); - h.setUint8(38, 0x74); - h.setUint8(39, 0x61); - h.setUint32(40, dataLength, Endian.little); - - return h.buffer.asUint8List(); - } -} diff --git a/apps/client/lib/modules/music/bindings/music_playlist_binding.dart b/apps/client/lib/modules/music/bindings/music_playlist_binding.dart deleted file mode 100644 index 85f385d4..00000000 --- a/apps/client/lib/modules/music/bindings/music_playlist_binding.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/music_playlist_controller.dart'; - -class MusicPlaylistBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => MusicPlaylistController(), - ); - } -} diff --git a/apps/client/lib/modules/music/controllers/music_playlist_controller.dart b/apps/client/lib/modules/music/controllers/music_playlist_controller.dart deleted file mode 100644 index 5fe2c8d8..00000000 --- a/apps/client/lib/modules/music/controllers/music_playlist_controller.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:get/get.dart'; - -class MusicPlaylistController extends GetxController { - // 歌单数据 - final playlist = >[].obs; - - @override - void onInit() { - super.onInit(); - // 从路由参数获取歌单数据 - final List> playlistData = Get.arguments ?? []; - playlist.assignAll(playlistData); - } -} diff --git a/apps/client/lib/modules/music/views/music_playlist_view.dart b/apps/client/lib/modules/music/views/music_playlist_view.dart deleted file mode 100644 index 16a7dfaf..00000000 --- a/apps/client/lib/modules/music/views/music_playlist_view.dart +++ /dev/null @@ -1,724 +0,0 @@ -import 'package:get/get.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '../../../core/utils/logger.dart'; -import '../../agent/controllers/agent_controller.dart'; -import '../../agent/widgets/circular_progress_button.dart'; - -class MusicPlaylistView extends StatelessWidget { - const MusicPlaylistView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - ScreenUtil.init(context, designSize: const Size(375, 812)); // 添加设计尺寸参考 - final controller = Get.find(); // 获取控制器 - final List> playlist = Get.arguments ?? []; - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios, size: 20), // iOS风格箭头 - onPressed: () => Navigator.pop(context), - ), - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, - foregroundColor: isDarkMode ? Colors.white : Colors.black, - elevation: 0, - ), - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - body: playlist.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.music_off, - size: 64.w, - color: Colors.grey[400], - ), - SizedBox(height: 16.h), - Text( - 'noMusic'.tr, // 暂无音乐 - style: TextStyle( - fontSize: 16.sp, - color: Colors.grey[600], - ), - ), - ], - ), - ) - : Column( - children: [ - // 上部:当前播放音乐详情 - 响应式高度 - Container( - height: MediaQuery.of(context).size.height * 0.38, // 增大高度比例 - child: _buildCurrentPlayingSection( - controller, playlist, isDarkMode), - ), - // 中部:播放全部按钮 - _buildPlayAllSection(controller, playlist, isDarkMode), - // 下部:歌单列表 - 使用剩余空间 - Expanded( - child: - _buildPlaylistSection(controller, playlist, isDarkMode), - ), - ], - ), - ); - } - - // 构建当前播放音乐详情部分 - Widget _buildCurrentPlayingSection(AgentController controller, - List> playlist, bool isDarkMode) { - return SingleChildScrollView( - // 添加滚动支持 - child: Obx(() { - // 查找当前播放的歌曲 - Map? currentSong; - if (controller.currentId.value != 0) { - try { - currentSong = playlist.firstWhere( - (song) => song['id'] == controller.currentId.value, - ); - } catch (e) { - // 如果没找到,使用第一首歌曲 - - currentSong = playlist.isNotEmpty ? playlist.first : null; - } - } else { - currentSong = playlist.isNotEmpty ? playlist.first : null; - } - - if (currentSong == null) { - return Container( - padding: EdgeInsets.all(16.w), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.music_note, - size: 60.w, - color: Colors.grey[400], - ), - SizedBox(height: 12.h), - Text( - 'noPlayingMusic'.tr, // 暂无播放音乐 - style: TextStyle( - fontSize: 16.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - ), - ], - ), - ), - ); - } - - final title = currentSong['name'] ?? - currentSong['title'] ?? - 'unknownSong'.tr; // 未知歌曲 - final artist = currentSong['singer'] ?? - currentSong['sgener'] ?? - currentSong['artist'] ?? - 'unknownArtist'.tr; // 未知歌手 - final coverUrl = currentSong['image'] ?? currentSong['coverUrl'] ?? ''; - - return Container( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 封面图片 - 响应式尺寸 - _buildCoverImage(coverUrl, isDarkMode, size: 110.w), - SizedBox(height: 12.h), - // 歌曲信息 - _buildSongInfo(title, artist, isDarkMode), - SizedBox(height: 16.h), - // 进度条 - _buildProgressBar(controller, isDarkMode), - SizedBox(height: 15.h), - // 播放控制按钮 - _buildPlayControls(controller, playlist, isDarkMode), - ], - ), - ); - }), - ); - } - - // 构建播放全部按钮部分 - Widget _buildPlayAllSection(AgentController controller, - List> playlist, bool isDarkMode) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), - child: SizedBox( - width: double.infinity, - height: 48.h, - child: ElevatedButton.icon( - onPressed: () async { - if (playlist.isNotEmpty) { - await controller.playPlaylist(playlist); - } - }, - icon: Icon(Icons.play_arrow, color: Colors.white, size: 20.w), - label: Text( - 'playAll'.tr, // 播放全部 - style: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(Get.context!).primaryColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14.r), - ), - elevation: 3, - shadowColor: Theme.of(Get.context!).primaryColor.withAlpha(102), - ), - ), - ), - ); - } - - // 构建歌单列表部分 - Widget _buildPlaylistSection(AgentController controller, - List> playlist, bool isDarkMode) { - return ListView.builder( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h), - itemCount: playlist.length, - itemBuilder: (context, index) { - final item = playlist[index]; - final id = item["id"] ?? 0; - final name = item["name"] ?? 'unknownSong'.tr; // 未知歌曲 - final singer = - item["singer"] ?? item["sgener"] ?? 'unknownArtist'.tr; // 未知歌手 - final image = item["image"] ?? ""; - - return _buildMusicItem( - context, - controller, - id, - name, - singer, - image, - isDarkMode, - ); - }, - ); - } - - Widget _buildMusicItem( - BuildContext context, - AgentController controller, - int id, - String name, - String singer, - String image, - bool isDarkMode, - ) { - final song = { - 'id': id, - 'title': name, - 'artist': singer, - 'coverUrl': image, - }; - - return GestureDetector( - onTap: () async { - await _playSelectedSong(controller, song, Get.arguments ?? []); - }, - child: Container( - margin: EdgeInsets.only(bottom: 10.h), - padding: EdgeInsets.all(14.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.white, - borderRadius: BorderRadius.circular(14.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(isDarkMode ? 0.25 : 0.05), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - children: [ - // 封面图片 - Container( - width: 52.w, - height: 52.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.r), - color: isDarkMode ? Colors.grey[700] : Colors.grey[200], - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(isDarkMode ? 0.2 : 0.08), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Stack( - children: [ - image.isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(10.r), - child: Image.network( - image, - width: 52.w, - height: 52.w, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Icon( - Icons.music_note, - color: isDarkMode - ? Colors.grey[500] - : Colors.grey[600], - size: 24.w, - ); - }, - ), - ) - : Icon( - Icons.music_note, - color: - isDarkMode ? Colors.grey[500] : Colors.grey[600], - size: 24.w, - ), - // 右下角音乐图标 - if (image.isNotEmpty) - Positioned( - right: 2.w, - bottom: 2.w, - child: Container( - width: 14.w, - height: 14.w, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(2.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 1, - offset: const Offset(0, 0.5), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(2.r), - child: Image.asset( - 'assets/images/qqmusic.png', - fit: BoxFit.contain, - ), - ), - ), - ), - ], - ), - ), - SizedBox(width: 14.w), - // 歌曲信息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - name, - style: TextStyle( - fontSize: 15.5.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black87, - height: 1.2, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 3.h), - Text( - singer, - style: TextStyle( - fontSize: 13.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - height: 1.1, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - SizedBox(width: 14.w), - // 播放按钮 - Obx(() { - final currentIdInt = id; - final isThisPlaying = controller.isPlaying.value && - controller.currentId.value == currentIdInt; - final currentProgress = - isThisPlaying ? controller.progress.value : 0.0; - - return CircularProgressButton( - isPlaying: isThisPlaying, - progress: currentProgress, - primaryColor: Theme.of(context).primaryColor, - backgroundColor: Colors.grey, - size: 40.w, - strokeWidth: 2.5, - onPlayPause: () async { - try { - Logger.d('MusicPlaylistView', - '播放按钮被点击,当前播放状态: ${controller.isPlaying.value}'); - Logger.d('MusicPlaylistView', - '当前歌曲ID: ${controller.currentId.value}, 点击歌曲ID: $currentIdInt'); - if (controller.isPlaying.value) { - if (controller.currentId.value == currentIdInt) { - Logger.d('MusicPlaylistView', '暂停当前播放的歌曲'); - await controller.musicPause(); - } else { - Logger.d('MusicPlaylistView', '播放新歌曲'); - await controller.musicPlay(song); - } - } else { - Logger.d('MusicPlaylistView', '开始播放歌曲'); - await controller.musicPlay(song); - } - } catch (e) { - Logger.e('MusicPlaylistView', '播放音乐时发生错误: $e'); - } - }, - onSeek: isThisPlaying - ? (seekProgress) { - // 只有当前播放的歌曲才允许跳转 - final targetPosition = - (controller.duration.value * seekProgress).round(); - controller.seekToPosition(targetPosition); - } - : null, - ); - }), - ], - ), - ), - ); - } - - // 构建封面图片 - Widget _buildCoverImage(String coverUrl, bool isDarkMode, - {required double size}) { - return Container( - width: size, - height: size, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(isDarkMode ? 0.3 : 0.1), - blurRadius: 15, - spreadRadius: 2, - offset: const Offset(0, 6), - ), - ], - ), - child: Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(16.r), - child: coverUrl.isNotEmpty - ? Image.network( - coverUrl, - fit: BoxFit.cover, - width: size, - height: size, - errorBuilder: (context, error, stackTrace) { - return _buildDefaultCover(isDarkMode, size: size); - }, - ) - : _buildDefaultCover(isDarkMode, size: size), - ), - // 右下角音乐图标 - Positioned( - right: 4.w, - bottom: 4.w, - child: Container( - width: size * 0.25, - height: size * 0.25, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(4.r), - child: Image.asset( - 'assets/images/qqmusic.png', - fit: BoxFit.contain, - ), - ), - ), - ), - ], - ), - ); - } - - // 构建默认封面 - Widget _buildDefaultCover(bool isDarkMode, {required double size}) { - return Container( - color: isDarkMode ? Colors.grey[800] : Colors.grey[300], - child: Icon( - Icons.music_note, - size: size * 0.55, - color: isDarkMode ? Colors.grey[600] : Colors.grey[500], - ), - ); - } - - // 构建歌曲信息 - Widget _buildSongInfo(String title, String artist, bool isDarkMode) { - return Column( - children: [ - Text( - title, - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black87, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 6.h), - Text( - artist, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ); - } - - // 构建进度条 - Widget _buildProgressBar(AgentController controller, bool isDarkMode) { - return Obx(() { - return Row( - children: [ - SizedBox( - width: 40.w, - child: Text( - _formatDuration(controller.currentPosition.value), - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - fontFeatures: const [FontFeature.tabularFigures()], - ), - textAlign: TextAlign.left, - ), - ), - SizedBox(width: 8.w), - Expanded( - child: SliderTheme( - data: SliderThemeData( - activeTrackColor: Theme.of(Get.context!).primaryColor, - inactiveTrackColor: - isDarkMode ? Colors.grey[700] : Colors.grey[300], - thumbColor: Theme.of(Get.context!).primaryColor, - overlayColor: - Theme.of(Get.context!).primaryColor.withOpacity(0.2), - trackHeight: 4.h, - thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8), - overlayShape: const RoundSliderOverlayShape(overlayRadius: 14), - ), - child: Slider( - value: controller.progress.value.clamp(0.0, 1.0), - onChanged: (value) { - // 拖动中仅更新UI显示,不触发实际跳转 - controller.progress.value = value; - controller.currentPosition.value = - (controller.duration.value * value).round(); - }, - onChangeEnd: (value) { - // 松开时再执行跳转,确保拖动自然 - final targetPosition = - (controller.duration.value * value).round(); - controller.seekToPosition(targetPosition); - }, - ), - ), - ), - SizedBox(width: 8.w), - SizedBox( - width: 40.w, - child: Text( - _formatDuration(controller.duration.value), - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - fontFeatures: const [FontFeature.tabularFigures()], - ), - textAlign: TextAlign.right, - ), - ), - ], - ); - }); - } - - // 构建播放控制按钮 - Widget _buildPlayControls(AgentController controller, - List> playlist, bool isDarkMode) { - return Obx(() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - onPressed: () async => await _playPrevious(controller, playlist), - iconSize: 32.w, - padding: EdgeInsets.all(8.w), // 增大触摸区域 - icon: Icon( - Icons.skip_previous, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - SizedBox(width: 16.w), - Container( - width: 56.w, - height: 56.w, - decoration: BoxDecoration( - color: Theme.of(Get.context!).primaryColor, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Theme.of(Get.context!).primaryColor.withOpacity(0.3), - blurRadius: 10, - spreadRadius: 1, - offset: const Offset(0, 3), - ), - ], - ), - child: IconButton( - onPressed: () => _togglePlayPause(controller), - padding: EdgeInsets.zero, - icon: Icon( - controller.isPlaying.value ? Icons.pause : Icons.play_arrow, - size: 28.w, - color: Colors.white, - ), - ), - ), - SizedBox(width: 16.w), - IconButton( - onPressed: () async => await _playNext(controller, playlist), - iconSize: 32.w, - padding: EdgeInsets.all(8.w), // 增大触摸区域 - icon: Icon( - Icons.skip_next, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ], - ); - }); - } - - // ...(其他辅助方法保持不变,如_formatDuration, _togglePlayPause等) - // 格式化时间显示 - String _formatDuration(int milliseconds) { - final duration = Duration(milliseconds: milliseconds); - final minutes = duration.inMinutes; - final seconds = duration.inSeconds % 60; - return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; - } - - // 播放/暂停切换 - Future _togglePlayPause(AgentController controller) async { - if (controller.isPlaying.value) { - await controller.musicPause(); - } else { - await controller.musicResume(); - } - } - - // 播放上一曲 - Future _playPrevious( - AgentController controller, List> playlist) async { - // 防止重复点击导致的状态混乱 - if (controller.isLoadingMusic.value) { - return; - } - - try { - // 设置加载状态,防止重复操作 - controller.isLoadingMusic.value = true; - - // 更新播放列表上下文 - controller.updateCurrentPlaylistContext( - playlist, controller.currentId.value); - - // 执行上一曲操作 - await controller.playPrevious(); - } finally { - // 确保加载状态被重置 - controller.isLoadingMusic.value = false; - } - } - - // 播放下一曲 - Future _playNext( - AgentController controller, List> playlist) async { - // 防止重复点击导致的状态混乱 - if (controller.isLoadingMusic.value) { - return; - } - - try { - // 设置加载状态,防止重复操作 - controller.isLoadingMusic.value = true; - - // 更新播放列表上下文 - controller.updateCurrentPlaylistContext( - playlist, controller.currentId.value); - - // 执行下一曲操作 - await controller.playNext(); - } finally { - // 确保加载状态被重置 - controller.isLoadingMusic.value = false; - } - } - - // 播放选中的歌曲 - Future _playSelectedSong(AgentController controller, - Map song, List> playlist) async { - // 防止重复点击导致的状态混乱 - if (controller.isLoadingMusic.value) { - return; - } - - try { - // 设置加载状态,防止重复操作 - controller.isLoadingMusic.value = true; - - final songId = song['id'] ?? 0; - controller.updateCurrentPlaylistContext(playlist, songId); - await controller.musicPlay(song); - } finally { - // 确保加载状态被重置 - controller.isLoadingMusic.value = false; - } - } -} diff --git a/apps/client/lib/modules/opus_test/bindings/opus_test_binding.dart b/apps/client/lib/modules/opus_test/bindings/opus_test_binding.dart deleted file mode 100644 index c1e6a1aa..00000000 --- a/apps/client/lib/modules/opus_test/bindings/opus_test_binding.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/opus_test_controller.dart'; - -class OpusTestBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => OpusTestController()); - } -} \ No newline at end of file diff --git a/apps/client/lib/modules/opus_test/controllers/opus_test_controller.dart b/apps/client/lib/modules/opus_test/controllers/opus_test_controller.dart deleted file mode 100644 index 0cd479f8..00000000 --- a/apps/client/lib/modules/opus_test/controllers/opus_test_controller.dart +++ /dev/null @@ -1,676 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:typed_data'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:just_audio/just_audio.dart'; -import 'package:jl_opus/jl_opus.dart'; -import 'package:permission_handler/permission_handler.dart'; -import '../../../core/utils/permission_util.dart'; -import 'package:share_plus/share_plus.dart'; // 添加分享插件导入 - -class OpusTestController extends GetxController { - // 选中的文件 - final selectedFiles = [].obs; - - // 扫描到的外部存储文件 - final externalFiles = [].obs; - // 是否携带协议头 - final hasHeader = false.obs; - - // 数据包长度 - final packetLength = '80'.obs; - - // 采样率选项 - final sampleRateOptions = [8000, 16000, 24000, 32000, 44100, 48000]; - final selectedSampleRate = 16000.obs; - - // 通道数量 - final isMono = true.obs; - - // 播放状态 - final isPlaying = false.obs; - final isDecoding = false.obs; - final statusMessage = ''.obs; - - // 播放器 - final player = AudioPlayer(); - - // 临时文件路径 - String? tempPcmPath; - - // 杰理OPUS解码器 - JlOpus? jlOpus; - StreamSubscription? _eventSubscription; - static const MethodChannel _bleServiceChannel = - MethodChannel('com.yunqiinnovation.ble_service'); - - @override - void onInit() { - super.onInit(); - // 合规(场景 1/2):不在 onInit 里申请权限——进页面就弹框,且拒绝后每次 - // 重进/重启还会再弹。改为用户点击"扫描存储"时按需申请。 - _initOpusDecoder(); - } - - @override - void onClose() { - player.dispose(); - _eventSubscription?.cancel(); - jlOpus?.dispose(); - super.onClose(); - } - - // 初始化OPUS解码器 - Future _initOpusDecoder() async { - if (!Platform.isAndroid) { - return; - } - - jlOpus = JlOpus(); - - // 监听解码器事件 - _eventSubscription = jlOpus!.eventStream.listen((event) { - switch (event.event) { - case 'onStart': - statusMessage.value = '开始${event.type == "file" ? "文件" : "流"}解码...'; - break; - case 'onComplete': - if (event.type == 'file') { - statusMessage.value = '解码完成: ${event.filePath}'; - } else { - statusMessage.value = '流解码完成'; - } - break; - case 'onError': - statusMessage.value = - '解码错误: [${event.errorCode}] ${event.errorMessage}'; - isDecoding.value = false; - break; - } - }); - - // 初始化OPUS解码器 - final initialized = await jlOpus!.initOpusDecoder(); - if (!initialized) { - statusMessage.value = 'OPUS解码器初始化失败'; - } - } - - Future _decodeOpusFile( - String inPath, String outPath, OpusOption option) async { - if (Platform.isIOS) { - final result = await _bleServiceChannel.invokeMethod( - 'decodeOpusFile', - { - 'inPath': inPath, - 'outPath': outPath, - ...option.toMap(), - }, - ); - return result; - } - - return jlOpus?.decodeOpusFile(inPath, outPath, option); - } - - // 请求必要权限(仅在用户主动触发读取存储的操作时调用) - // - // 合规(场景 1/2):统一走 PermissionUtil,拒绝一次后 48 小时内不再申请, - // 返回 false 时直接放弃本次操作,不弹引导框、不跳设置页。 - Future requestPermissions() async { - if (!Platform.isAndroid) return true; - return PermissionUtil.instance.requestPermission( - permissionType: Permission.storage, - permissionName: 'storagePermission'.tr, - explanationText: 'storagePermissionExplanation'.tr, - permanentDenialText: 'storagePermissionExplanation'.tr, - ); - } - - // 加载外部存储文件 - Future loadExternalFiles() async { - // 用户主动触发,此时才申请存储权限;拿不到就直接退出该功能 - if (!await requestPermissions()) { - statusMessage.value = 'storagePermissionExplanation'.tr; - return; - } - try { - externalFiles.clear(); - statusMessage.value = '正在扫描存储...'; - print('正在扫描存储...'); - - List directories = []; - - if (Platform.isAndroid) { - // Android: 使用外部存储目录 - final androidDirs = await getExternalStorageDirectories(); - if (androidDirs != null && androidDirs.isNotEmpty) { - directories.addAll(androidDirs); - } - } else if (Platform.isIOS) { - // iOS: 使用Documents目录 - final iosDir = await getApplicationDocumentsDirectory(); - directories.add(iosDir); - } - - if (directories.isEmpty) { - statusMessage.value = '未找到存储目录'; - return; - } - - print('存储目录: $directories'); - - // 遍历所有存储目录 - for (final directory in directories) { - final dirPath = directory.path; - final dir = Directory(dirPath); - - // 检查目录是否存在 - if (await dir.exists()) { - // 递归列出所有文件 - final files = await dir.list(recursive: true).toList(); - - for (var entity in files) { - if (entity is File) { - final path = entity.path.toLowerCase(); - if (path.endsWith('.opus') || path.endsWith('.txt')) { - final fileSize = await entity.length(); - externalFiles.add(PlatformFile( - name: entity.path.split('/').last, - path: entity.path, - size: fileSize, - )); - } - } - } - } - } - - statusMessage.value = '扫描完成,找到 ${externalFiles.length} 个Opus文件'; - } catch (e) { - statusMessage.value = '扫描失败: $e'; - } - } - - // 选择文件 - Future pickFiles() async { - try { - final result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['opus'], - allowMultiple: false, - ); - - if (result != null && result.files.isNotEmpty) { - selectedFiles.clear(); - selectedFiles.add(result.files.first); - statusMessage.value = '已选择文件:${result.files.first.name}'; - } - } catch (e) { - statusMessage.value = '选择文件失败: $e'; - } - } - - // 从外部文件列表中选择文件 - void selectExternalFile(PlatformFile file) { - selectedFiles.clear(); - selectedFiles.add(file); - statusMessage.value = '已选择文件: ${file.name}'; - } - - // 切换通道模式 - void toggleChannel(bool isSingleChannel) { - isMono.value = isSingleChannel; - } - - // 设置采样率 - void setSampleRate(int rate) { - selectedSampleRate.value = rate; - } - - // 设置数据包长度 - void setPacketLength(String length) { - packetLength.value = length; - } - - // 切换是否携带协议头 - void toggleHeader(bool value) { - hasHeader.value = value; - } - - // 播放音频 - Future playAudio() async { - if (isPlaying.value) { - await player.stop(); - isPlaying.value = false; - statusMessage.value = '已停止播放'; - return; - } - - if (selectedFiles.isEmpty) { - statusMessage.value = '请先选择Opus文件'; - return; - } - - try { - isDecoding.value = true; - statusMessage.value = '正在解码...'; - - // 使用杰理OPUS库进行解码 - final inPath = selectedFiles.first.path!; - - // 创建WAV输出文件路径 - final tempDir = await getTemporaryDirectory(); - final wavFileName = - 'decoded_opus_${DateTime.now().millisecondsSinceEpoch}.wav'; - final outPath = '${tempDir.path}/$wavFileName'; - - // 创建解码参数 - final option = OpusOption( - hasHeader: hasHeader.value, - channel: isMono.value ? 1 : 2, - sampleRate: selectedSampleRate.value, - packetSize: int.tryParse(packetLength.value) ?? 40, - ); - - // 解码文件 - final pcmPath = await _decodeOpusFile(inPath, outPath, option); - if (pcmPath == null) { - statusMessage.value = '解码失败'; - isDecoding.value = false; - return; - } - print("pcmPath: $pcmPath"); - tempPcmPath = pcmPath; - - // 将PCM文件转换为WAV文件 - final wavFile = await _convertPcmToWav( - pcmPath, - '${tempDir.path}/$wavFileName', - selectedSampleRate.value, - isMono.value ? 1 : 2); - - statusMessage.value = '解码完成,准备播放'; - - // 播放WAV文件 - await player.setFilePath(wavFile.path); - await player.play(); - isPlaying.value = true; - statusMessage.value = '正在播放解码后的音频'; - - // 监听播放完成 - player.playerStateStream.listen((state) { - if (state.processingState == ProcessingState.completed) { - isPlaying.value = false; - statusMessage.value = '播放完成'; - } - }); - } catch (e) { - statusMessage.value = '解码或播放失败: $e'; - } finally { - isDecoding.value = false; - } - } - - // 将PCM转换为WAV格式 - Future _convertPcmToWav( - String pcmPath, String wavPath, int sampleRate, int channels) async { - final pcmFile = File(pcmPath); - final pcmData = await pcmFile.readAsBytes(); - - // 创建WAV文件 (44字节头部 + PCM数据) - final wavSize = 44 + pcmData.length; - final wavData = ByteData(wavSize); - - // 写入WAV头部 - _writeWavHeader(wavData, pcmData.length, sampleRate, channels); - - // 写入PCM数据 - for (int i = 0; i < pcmData.length; i++) { - wavData.setUint8(44 + i, pcmData[i]); - } - - // 保存为WAV文件 - final wavFile = File(wavPath); - await wavFile.writeAsBytes(wavData.buffer.asUint8List()); - - return wavFile; - } - - // 写入WAV头部到ByteData - void _writeWavHeader( - ByteData data, int pcmLength, int sampleRate, int channels) { - final bytesPerSample = 2; // 16位PCM - final byteRate = sampleRate * channels * bytesPerSample; - final blockAlign = channels * bytesPerSample; - - // "RIFF"标识 - data.setUint8(0, 0x52); // 'R' - data.setUint8(1, 0x49); // 'I' - data.setUint8(2, 0x46); // 'F' - data.setUint8(3, 0x46); // 'F' - - // 文件大小 (减去8个字节的RIFF头部和大小字段) - data.setUint32(4, 36 + pcmLength, Endian.little); - - // "WAVE"标识 - data.setUint8(8, 0x57); // 'W' - data.setUint8(9, 0x41); // 'A' - data.setUint8(10, 0x56); // 'V' - data.setUint8(11, 0x45); // 'E' - - // "fmt "子块 - data.setUint8(12, 0x66); // 'f' - data.setUint8(13, 0x6D); // 'm' - data.setUint8(14, 0x74); // 't' - data.setUint8(15, 0x20); // ' ' - - // 子块大小 (16 for PCM) - data.setUint32(16, 16, Endian.little); - - // 音频格式 (1 表示PCM) - data.setUint16(20, 1, Endian.little); - - // 通道数量 - data.setUint16(22, channels, Endian.little); - - // 采样率 - data.setUint32(24, sampleRate, Endian.little); - - // 字节率 = 采样率 * 通道数 * 每样本字节数 - data.setUint32(28, byteRate, Endian.little); - - // 块对齐 = 通道数 * 每样本字节数 - data.setUint16(32, blockAlign, Endian.little); - - // 每个样本位数 - data.setUint16(34, bytesPerSample * 8, Endian.little); - - // "data"子块 - data.setUint8(36, 0x64); // 'd' - data.setUint8(37, 0x61); // 'a' - data.setUint8(38, 0x74); // 't' - data.setUint8(39, 0x61); // 'a' - - // 数据大小 - data.setUint32(40, pcmLength, Endian.little); - } - - /// 删除外部文件 - Future deleteExternalFile(PlatformFile file) async { - try { - final fileToDelete = File(file.path!); - if (await fileToDelete.exists()) { - await fileToDelete.delete(); - externalFiles.removeWhere((f) => f.path == file.path); - - // 如果删除的是当前选中的文件,清空选择 - if (selectedFiles.isNotEmpty && selectedFiles.first.path == file.path) { - selectedFiles.clear(); - } - - statusMessage.value = '文件已删除: ${file.name}'; - } else { - statusMessage.value = '文件不存在: ${file.name}'; - } - } catch (e) { - statusMessage.value = '删除文件失败: $e'; - } - } - - /// 分享外部文件 - Future shareExternalFile(PlatformFile file) async { - try { - if (file.path != null) { - final fileToShare = File(file.path!); - if (await fileToShare.exists()) { - await Share.shareXFiles( - [XFile(file.path!)], - text: '分享Opus音频文件: ${file.name}', - ); - statusMessage.value = '正在分享文件: ${file.name}'; - } else { - statusMessage.value = '文件不存在: ${file.name}'; - } - } - } catch (e) { - statusMessage.value = '分享文件失败: $e'; - } - } - - /// 显示文件操作底部弹窗 - void showFileActionSheet(BuildContext context, PlatformFile file) { - showModalBottomSheet( - context: context, - backgroundColor: Get.isDarkMode ? Colors.grey[900] : Colors.white, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (context) => Container( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 文件信息 - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ - Icon( - Icons.audiotrack, - color: Get.isDarkMode ? Colors.white70 : Colors.black87, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - file.name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Get.isDarkMode ? Colors.white : Colors.black, - ), - overflow: TextOverflow.ellipsis, - ), - Text( - '${(file.size / 1024).toStringAsFixed(2)} KB', - style: TextStyle( - fontSize: 14, - color: Get.isDarkMode - ? Colors.white60 - : Colors.black54, - ), - ), - ], - ), - ), - ], - ), - ), - const Divider(), - // 分享选项 - ListTile( - leading: const Icon(Icons.share, color: Colors.blue), - title: const Text('分享文件'), - onTap: () { - Navigator.pop(context); - shareExternalFile(file); - }, - ), - // 删除选项 - ListTile( - leading: const Icon(Icons.delete, color: Colors.red), - title: const Text('删除文件', style: TextStyle(color: Colors.red)), - onTap: () { - Navigator.pop(context); - _showDeleteConfirmDialog(context, file); - }, - ), - // 取消选项 - ListTile( - leading: const Icon(Icons.cancel), - title: const Text('取消'), - onTap: () => Navigator.pop(context), - ), - ], - ), - ), - ); - } - - /// 显示删除确认对话框 - void _showDeleteConfirmDialog(BuildContext context, PlatformFile file) { - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: Get.isDarkMode ? Colors.grey[900] : Colors.white, - title: Text( - '确认删除', - style: TextStyle( - color: Get.isDarkMode ? Colors.white : Colors.black, - ), - ), - content: Text( - '确定要删除文件 "${file.name}" 吗?\n此操作不可撤销。', - style: TextStyle( - color: Get.isDarkMode ? Colors.white70 : Colors.black87, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('取消'), - ), - TextButton( - onPressed: () { - Navigator.pop(context); - deleteExternalFile(file); - }, - style: TextButton.styleFrom(foregroundColor: Colors.red), - child: const Text('删除'), - ), - ], - ), - ); - } -} - -// PCM音频源 -class PCMAudioSource extends StreamAudioSource { - final Uint8List pcmData; - final int sampleRate; - final int channels; - - PCMAudioSource({ - required this.pcmData, - this.sampleRate = 16000, - this.channels = 1, - }); - - @override - Future request([int? start, int? end]) async { - start = start ?? 0; - end = end ?? pcmData.length; - - // 转换PCM到WAV格式 - final wavHeader = _createWavHeader(pcmData.length, sampleRate, channels); - final wavData = Uint8List(wavHeader.length + pcmData.length); - wavData.setRange(0, wavHeader.length, wavHeader); - wavData.setRange( - wavHeader.length, wavHeader.length + pcmData.length, pcmData); - - // 调整请求范围来适应WAV头部 - int adjustedStart = start; - int adjustedEnd = end; - - if (start < wavHeader.length) { - adjustedStart = start; - } else { - adjustedStart = start + wavHeader.length; - } - - if (end <= wavHeader.length) { - adjustedEnd = end; - } else { - adjustedEnd = end + wavHeader.length; - adjustedEnd = adjustedEnd > wavData.length ? wavData.length : adjustedEnd; - } - - return StreamAudioResponse( - sourceLength: wavData.length, - contentLength: adjustedEnd - adjustedStart, - offset: adjustedStart, - stream: Stream.value(wavData.sublist(adjustedStart, adjustedEnd)), - contentType: 'audio/wav', - ); - } - - // 创建WAV头部 - Uint8List _createWavHeader(int dataLength, int sampleRate, int channels) { - final bytesPerSample = 2; // 16位PCM - final byteRate = sampleRate * channels * bytesPerSample; - final blockAlign = channels * bytesPerSample; - - final header = ByteData(44); // WAV头部固定44字节 - - // "RIFF"标识 - header.setUint8(0, 0x52); // 'R' - header.setUint8(1, 0x49); // 'I' - header.setUint8(2, 0x46); // 'F' - header.setUint8(3, 0x46); // 'F' - - // 文件大小 (减去8个字节的RIFF头部和大小字段) - header.setUint32(4, 36 + dataLength, Endian.little); - - // "WAVE"标识 - header.setUint8(8, 0x57); // 'W' - header.setUint8(9, 0x41); // 'A' - header.setUint8(10, 0x56); // 'V' - header.setUint8(11, 0x45); // 'E' - - // "fmt "子块 - header.setUint8(12, 0x66); // 'f' - header.setUint8(13, 0x6D); // 'm' - header.setUint8(14, 0x74); // 't' - header.setUint8(15, 0x20); // ' ' - - // 子块大小 (16 for PCM) - header.setUint32(16, 16, Endian.little); - - // 音频格式 (1 表示PCM) - header.setUint16(20, 1, Endian.little); - - // 通道数量 - header.setUint16(22, channels, Endian.little); - - // 采样率 - header.setUint32(24, sampleRate, Endian.little); - - // 字节率 = 采样率 * 通道数 * 每样本字节数 - header.setUint32(28, byteRate, Endian.little); - - // 块对齐 = 通道数 * 每样本字节数 - header.setUint16(32, blockAlign, Endian.little); - - // 每个样本位数 - header.setUint16(34, bytesPerSample * 8, Endian.little); - - // "data"子块 - header.setUint8(36, 0x64); // 'd' - header.setUint8(37, 0x61); // 'a' - header.setUint8(38, 0x74); // 't' - header.setUint8(39, 0x61); // 'a' - - // 数据大小 - header.setUint32(40, dataLength, Endian.little); - - return header.buffer.asUint8List(); - } -} diff --git a/apps/client/lib/modules/opus_test/views/opus_test_view.dart b/apps/client/lib/modules/opus_test/views/opus_test_view.dart deleted file mode 100644 index de02e513..00000000 --- a/apps/client/lib/modules/opus_test/views/opus_test_view.dart +++ /dev/null @@ -1,522 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import '../controllers/opus_test_controller.dart'; - -class OpusTestView extends GetView { - const OpusTestView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - // 获取主题模式 - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return DefaultTabController( - length: 2, - child: Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - appBar: AppBar( - backgroundColor: isDarkMode ? Colors.black : Colors.white, - elevation: 0.5, - title: Text( - 'OPUS解码', - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - centerTitle: true, - bottom: TabBar( - indicatorColor: Colors.blue, - indicatorWeight: 3, - labelColor: isDarkMode ? Colors.white : Colors.black, - unselectedLabelColor: isDarkMode ? Colors.grey : Colors.grey, - tabs: const [ - Tab(text: "解码"), - Tab(text: "输出"), - ], - ), - ), - body: TabBarView( - children: [ - _buildDecodeTab(isDarkMode), - _buildEncodeTab(isDarkMode), - ], - ), - ), - ); - } - - // 解码标签页 - Widget _buildDecodeTab(bool isDarkMode) { - return ListView( - padding: EdgeInsets.all(16.w), - children: [ - // 文件选择部分 - _buildSection( - title: '选择文件', - isDarkMode: isDarkMode, - child: Column( - children: [ - // 当前选中的文件 - Obx(() => controller.selectedFiles.isEmpty - ? const SizedBox.shrink() - : ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: controller.selectedFiles.length, - itemBuilder: (context, index) { - final file = controller.selectedFiles[index]; - return ListTile( - title: Text( - file.name, - style: TextStyle( - color: - isDarkMode ? Colors.white70 : Colors.black87, - fontSize: 14.sp, - ), - ), - subtitle: Text( - '${(file.size / 1024).toStringAsFixed(2)} Kb', - style: TextStyle( - color: - isDarkMode ? Colors.white38 : Colors.black54, - fontSize: 12.sp, - ), - ), - trailing: IconButton( - icon: Icon(Icons.clear, color: Colors.red), - onPressed: () { - controller.selectedFiles.removeAt(index); - }, - ), - ); - }, - )), - - // 外部文件列表标题和刷新按钮 - Padding( - padding: EdgeInsets.symmetric(vertical: 8.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '外部存储文件', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - IconButton( - icon: Icon( - Icons.refresh, - color: isDarkMode ? Colors.blue[300] : Colors.blue, - ), - onPressed: controller.loadExternalFiles, - ), - ], - ), - ), - - // 外部文件列表 - Obx(() => controller.externalFiles.isEmpty - ? Center( - child: Padding( - padding: EdgeInsets.all(16.h), - child: Text( - '未找到Opus文件', - style: TextStyle( - color: isDarkMode ? Colors.white54 : Colors.black54, - ), - ), - ), - ) - : Container( - height: 200.h, - decoration: BoxDecoration( - border: Border.all( - color: isDarkMode - ? Colors.grey[700]! - : Colors.grey[300]!, - ), - borderRadius: BorderRadius.circular(8.r), - ), - child: ListView.builder( - itemCount: controller.externalFiles.length, - itemBuilder: (context, index) { - final file = controller.externalFiles[index]; - return ListTile( - title: Text( - file.name, - style: TextStyle( - color: isDarkMode - ? Colors.white70 - : Colors.black87, - fontSize: 14.sp, - ), - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - '${(file.size / 1024).toStringAsFixed(2)} Kb', - style: TextStyle( - color: isDarkMode - ? Colors.white38 - : Colors.black54, - fontSize: 12.sp, - ), - ), - onTap: () { - controller.selectExternalFile(file); - }, - onLongPress: () { - // 长按显示操作菜单 - controller.showFileActionSheet(context, file); - }, - trailing: Icon( - Icons.arrow_forward, - color: - isDarkMode ? Colors.white54 : Colors.black54, - ), - ); - }, - ), - )), - ], - ), - ), - SizedBox(height: 16.h), - - // 解码参数部分 - _buildSection( - title: '解码参数', - isDarkMode: isDarkMode, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Obx(() => Checkbox( - value: controller.hasHeader.value, - onChanged: (value) => controller.toggleHeader(value!), - activeColor: Colors.blue, - )), - Text( - '是否携带协议头', - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black87, - fontSize: 14.sp, - ), - ), - ], - ), - // 数据包长度 - Row( - children: [ - Text( - '数据包长度', - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black87, - fontSize: 14.sp, - ), - ), - SizedBox(width: 16.w), - Expanded( - child: Obx(() => TextField( - controller: TextEditingController( - text: controller.packetLength.value), - onChanged: (value) => - controller.setPacketLength(value), - keyboardType: TextInputType.number, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - ), - decoration: InputDecoration( - isDense: true, - suffix: Text( - 'Byte', - style: TextStyle( - color: isDarkMode - ? Colors.white60 - : Colors.black54, - fontSize: 12.sp, - ), - ), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: isDarkMode - ? Colors.white30 - : Colors.black26, - ), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Colors.blue, - ), - ), - ), - )), - ), - ], - ), - SizedBox(height: 20.h), - - // 采样率 - Row( - children: [ - Text( - '采样率', - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black87, - fontSize: 14.sp, - ), - ), - SizedBox(width: 16.w), - Expanded( - child: Obx(() => DropdownButton( - value: controller.selectedSampleRate.value, - isExpanded: true, - dropdownColor: - isDarkMode ? Colors.grey[800] : Colors.white, - items: controller.sampleRateOptions.map((rate) { - return DropdownMenuItem( - value: rate, - child: Text( - rate.toString(), - style: TextStyle( - color: isDarkMode - ? Colors.white - : Colors.black87, - ), - ), - ); - }).toList(), - onChanged: (value) => - controller.setSampleRate(value!), - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - ), - )), - ), - ], - ), - SizedBox(height: 20.h), - - // 通道数量 - Text( - '通道数量', - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black87, - fontSize: 14.sp, - ), - ), - Row( - children: [ - Expanded( - child: Row( - children: [ - Obx(() => Radio( - value: true, - groupValue: controller.isMono.value, - onChanged: (value) => - controller.toggleChannel(value!), - activeColor: Colors.blue, - )), - Text( - '单通道', - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black87, - fontSize: 14.sp, - ), - ), - ], - ), - ), - Expanded( - child: Row( - children: [ - Obx(() => Radio( - value: false, - groupValue: controller.isMono.value, - onChanged: (value) => - controller.toggleChannel(value!), - activeColor: Colors.blue, - )), - Text( - '双通道', - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black87, - fontSize: 14.sp, - ), - ), - ], - ), - ), - ], - ), - ], - ), - ), - - SizedBox(height: 16.h), - - // 状态消息 - Obx(() => controller.statusMessage.value.isNotEmpty - ? Container( - padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w), - margin: EdgeInsets.only(bottom: 16.h), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.blue.withOpacity(0.2) - : Colors.blue.withOpacity(0.1), - borderRadius: BorderRadius.circular(4.r), - border: Border.all( - color: isDarkMode - ? Colors.blue.withOpacity(0.3) - : Colors.blue.withOpacity(0.2), - width: 1, - ), - ), - child: Text( - controller.statusMessage.value, - style: TextStyle( - color: isDarkMode ? Colors.blue[300] : Colors.blue[700], - fontSize: 12.sp, - ), - ), - ) - : const SizedBox.shrink()), - - // 播放按钮 - Container( - margin: EdgeInsets.symmetric(vertical: 8.h), - child: ElevatedButton.icon( - icon: Obx(() => Icon( - controller.isPlaying.value ? Icons.stop : Icons.play_arrow, - color: Colors.white, - )), - label: Obx(() => Text( - controller.isPlaying.value ? '停止播放' : '播放音频', - style: TextStyle( - color: Colors.white, - fontSize: 16.sp, - ), - )), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - padding: EdgeInsets.symmetric(vertical: 16.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - onPressed: controller.isDecoding.value - ? null - : () { - controller.playAudio(); - }, - ), - ), - - // 解码按钮 - Container( - margin: EdgeInsets.symmetric(vertical: 8.h), - child: Obx(() => ElevatedButton( - onPressed: controller.selectedFiles.isEmpty || - controller.isDecoding.value - ? null - : () { - controller.pickFiles(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - padding: EdgeInsets.symmetric(vertical: 16.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - child: Text( - '开始解码', - style: TextStyle( - color: Colors.white, - fontSize: 16.sp, - ), - ), - )), - ), - ], - ); - } - - // 编码标签页(暂未实现) - Widget _buildEncodeTab(bool isDarkMode) { - return Center( - child: Text( - '编码功能尚未实现', - style: TextStyle( - color: isDarkMode ? Colors.white54 : Colors.black54, - fontSize: 16.sp, - ), - ), - ); - } - - // 构建带标题的区段 - Widget _buildSection({ - required String title, - required Widget child, - required bool isDarkMode, - }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - title, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - SizedBox(width: 8.w), - Expanded( - child: Container( - height: 1, - color: isDarkMode ? Colors.white24 : Colors.black12, - ), - ), - if (title == '选择文件') - IconButton( - icon: Icon( - Icons.add_circle_outline, - color: isDarkMode ? Colors.blue[300] : Colors.blue, - ), - onPressed: () { - controller.pickFiles(); - }, - ), - ], - ), - Container( - padding: EdgeInsets.all(12.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, - borderRadius: BorderRadius.circular(8.r), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.03), - blurRadius: 4, - offset: Offset(0, 2), - ), - ], - ), - child: child, - ), - ], - ); - } -} diff --git a/apps/client/lib/modules/ota_upgrade/controllers/bes_ota_upgrade_controller.dart b/apps/client/lib/modules/ota_upgrade/controllers/bes_ota_upgrade_controller.dart deleted file mode 100644 index c66f50bd..00000000 --- a/apps/client/lib/modules/ota_upgrade/controllers/bes_ota_upgrade_controller.dart +++ /dev/null @@ -1,495 +0,0 @@ -import 'dart:async'; - -import 'package:bes_ota_manager/bes_ota_manager.dart'; -import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -import '../../../core/utils/logger.dart'; -import '../../../data/models/appconfig.dart'; -import '../../../data/models/appconfig_model.dart'; -import '../../../data/services/bes_bluetooth_service.dart'; -import 'bes_ota_download_service.dart'; - -/// OTA 升级阶段(驱动页面 UI)。 -class OtaPhase { - static const idle = 0; // 待升级 - static const downloading = 1; // 下载固件中 - static const upgrading = 2; // 蓝牙推送中 - static const success = 3; // 升级成功 - static const failed = 4; // 升级失败 -} - -/// 恒玄(BES)耳机固件 OTA 升级控制器。 -/// -/// 与杰理那条链路([OtaCoreService] / `ota_upgrade_controller.dart`)完全独立: -/// 不碰 BleManager / Jielihome,只依赖 [BesBluetoothService] 与 `bes_ota_manager` 插件。 -/// 本项目只有恒玄耳机,路由 `Routes.OTA_UPGRADE` 已指向这里。 -/// -/// 流程:读产品配置 → 断开 app 层 SPP → 下载固件 → 插件自管连接推送 → 设备重启。 -class BesOtaUpgradeController extends GetxController { - static const _tag = 'BesOtaUpgradeController'; - - /// 看门狗超时:升级阶段超过该时长无任何进度或状态推进,即判定卡死失败。 - /// 兜底原生侧「既不推进也不上报 onError」导致 UI 永久停在升级中的情况。 - static const Duration _watchdogTimeout = Duration(seconds: 90); - - /// 固件下载地址:来自后端 `user_getappconfig` 下发的产品配置,按设备名匹配。 - String _downloadUrl = ''; - - /// 页面已销毁标志:onClose 置位,用于让正在进行的下载/升级异步链提前退出, - /// 避免在已销毁的控制器上发起 startOta(僵尸会话)。 - bool _disposed = false; - - /// 当前下载的取消令牌:cancel()/onClose 时取消,立即中止下载、不再续跑。 - CancelToken? _downloadCancelToken; - - /// 升级看门狗定时器:每次收到进度/状态事件即重置;超时未重置则判失败。 - Timer? _watchdog; - - final BesBluetoothService _bes = BesBluetoothService.to; - final BesOtaManager _besOta = BesOtaManager(); - final BesOtaDownloadService _downloadService = BesOtaDownloadService(); - - // ---- 响应式状态 ---- - final RxString currentVersion = ''.obs; - final RxString latestVersion = ''.obs; - final RxString updateDescription = ''.obs; - final RxBool isUpdateAvailable = false.obs; - final RxBool isConnected = true.obs; - - /// 设备名快照:开始升级时会主动断开 app 层连接,[BesBluetoothService] 的字段被 - /// 清空,页面展示用控制器自己的快照,不跟着变空。 - final RxString deviceName = ''.obs; - - /// 设备地址快照,服务字段被清空后(如失败重试时)回退使用。 - String _address = ''; - - /// 是否为本次 OTA 主动断开过 app 层连接。主动断开不会自动回连, - /// 退出页面时据此决定是否恢复连接(没点过「开始升级」则无需处理)。 - bool _appConnectionReleased = false; - - final RxInt phase = OtaPhase.idle.obs; - final RxDouble downloadProgress = 0.0.obs; // 0~1 - final RxDouble otaProgress = 0.0.obs; // 0~1 - final RxString detailedStatus = ''.obs; - - StreamSubscription? _otaSub; - Worker? _versionWorker; - Worker? _statusWorker; - Worker? _deviceWorker; - Worker? _nameWorker; - - bool get isBusy => - phase.value == OtaPhase.downloading || phase.value == OtaPhase.upgrading; - - @override - void onInit() { - super.onInit(); - - isConnected.value = _bes.isConnected; - currentVersion.value = _bes.deviceVersion.value; - deviceName.value = _bes.connectedDeviceName.value; - _address = _bes.connectedDeviceId.value; - _loadProductConfig(); - - // 设备版本回包(`AA 06` → `BB 86`)后同步刷新;开始升级时主动断开会把 - // deviceVersion 清空,空值忽略,保留页面上已知的版本显示。 - _versionWorker = ever(_bes.deviceVersion, (String v) { - if (v.isEmpty) return; - currentVersion.value = v; - _refreshUpdateAvailable(); - }); - - // 跟踪 app 层连接状态(仅用于展示与按钮可用性)。 - // 注意:OTA 连接由插件自管、掉线会经 onError 上报;开始升级时 app 层连接 - // 是我们主动断开的,不能拿它的断开去中止 OTA。 - _statusWorker = ever(_bes.deviceStatus, (int s) { - isConnected.value = s == 2; - }); - - // (重)连接后重新捕获设备地址,失败重试时要用。 - _deviceWorker = ever(_bes.connectedDeviceId, (String id) { - if (id.isEmpty) return; - _address = id; - }); - - // 设备名就绪/变化时刷新快照并重载产品配置。注意:重连时服务里 id 先于 - // name 赋值且 worker 同步触发,挂在 id 上拿不到新名字;页面初始化恰逢 - // 重连中名字为空时,也靠这里在名字就绪后补加载,否则「最新版本」会一直为空。 - _nameWorker = ever(_bes.connectedDeviceName, (String n) { - if (n.isEmpty) return; - deviceName.value = n; - _loadProductConfig(); - }); - - _otaSub = _besOta.events.listen( - _handleOtaEvent, - // 事件通道本身异常(原生 sink 出错/通道断开)也要落到失败终态,避免无声卡死。 - onError: (Object err) { - if (phase.value == OtaPhase.success) return; - _onUpgradeError(null, err.toString()); - }, - ); - } - - @override - void onClose() { - _disposed = true; - _downloadCancelToken?.cancel('page closed'); // 中止可能仍在跑的下载 - _cancelWatchdog(); - _setKeepScreenOn(false); // 退出页面兜底关闭常亮 - _otaSub?.cancel(); - _versionWorker?.dispose(); - _statusWorker?.dispose(); - _deviceWorker?.dispose(); - _nameWorker?.dispose(); - - // 主动断开不会自动回连:若本次为 OTA 断开过 app 层连接,退出页面时恢复。 - // 升级成功时设备正在重启,延时几秒再连。 - final needReconnect = _appConnectionReleased && _address.isNotEmpty; - final addr = _address; - final name = deviceName.value; - // SDK 自己 OTA 成功后的内部重连等了 15s(MSG_OTA_OVER_RECONNECT),设备重启到 - // 蓝牙就绪需要数秒;即使这次连不上,耳机开机也会主动回连手机兜底。 - final delay = phase.value == OtaPhase.success - ? const Duration(seconds: 5) // 设备正在重启 - : const Duration(seconds: 2); // 等耳机拆完 OTA 链路再回连(与开始升级方向一致) - // 先停掉 OTA 会话释放插件占用的 SPP 连接,再回连 app 层,避免两条连接打架。 - _besOta.stopOta().whenComplete(() { - _besOta.dispose(); - if (!needReconnect) return; - Future.delayed(delay, () { - // 期间可能已(自动/手动)连上,避免重复发起。 - if (_bes.deviceStatus.value != 2) { - _bes.connectToDevice(addr, name); - } - }); - }); - super.onClose(); - } - - /// 从后端产品配置取最新固件版本与下载地址(按设备名匹配产品)。 - void _loadProductConfig() { - final product = _findProduct(deviceName.value); - if (product == null) { - Logger.w(_tag, '未找到设备对应的产品配置: ${deviceName.value}'); - latestVersion.value = ''; - updateDescription.value = ''; - _downloadUrl = ''; - } else { - latestVersion.value = product.version?.split('+').first.trim() ?? ''; - updateDescription.value = product.updatedescription ?? ''; - _downloadUrl = product.updatepackageaddress ?? ''; - // 「拉到的版本不对」这类问题以前只能靠猜:失败有日志、成功没有, - // 分不清是没匹配上产品、下发的值就是旧的、还是设备当前版本读错了。 - Logger.i( - _tag, - '产品配置: name=${product.devicename} pid=${product.id} ' - '服务端最新=${product.version} → 解析后=${latestVersion.value} ' - '设备当前=${currentVersion.value} ' - '产品表共${AppConfig.config.products.length}个'); - } - _refreshUpdateAvailable(); - } - - /// 后台是否允许「版本不一致即可升级」。取不到产品配置时按 false(严格)。 - bool get _allowDowngrade => - _findProduct(deviceName.value)?.updateonmismatch ?? false; - - /// 按设备名找产品。 - /// - /// ⚠️ 不要用 [AppConfig.getproduct]——它内部 `firstWhere` 没有 orElse, - /// 查不到直接抛 StateError,而调用点写的都是 `if (product == null)`, - /// 那个 null 判断永远等不到,异常会穿出去、在固件页表现成「加载失败」。 - /// - /// 顺序:设备名互相包含 → 唯一的经典蓝牙耳机产品 → null。 - DBProduct? _findProduct(String name) { - if (!AppConfig.isInitialized()) return null; - final products = AppConfig.config.products; - if (products.isEmpty) return null; - final n = name.trim().toUpperCase(); - if (n.isNotEmpty && n != 'UNKNOWNEQUIPMENT') { - for (final p in products) { - final pn = p.devicename.trim().toUpperCase(); - if (pn.isEmpty) continue; - if (pn.contains(n) || n.contains(pn)) return p; - } - } - // 本项目只有恒玄(经典蓝牙耳机)一种设备,名字对不上时退化到它是安全的 - final headsets = products.where((p) => p.devicetype == 1).toList(); - if (headsets.length == 1) return headsets.first; - return null; - } - - void _refreshUpdateAvailable() { - final cur = currentVersion.value.trim(); - final latest = latestVersion.value.trim(); - // 没有产品配置(无最新版本)不可升级 - if (latest.isEmpty) { - isUpdateAvailable.value = false; - return; - } - // 读不到当前版本时允许升级(未知 → 允许) - if (cur.isEmpty) { - isUpdateAvailable.value = true; - return; - } - // 判定口径由**后台产品配置**决定,不看包类型: - // updateonmismatch=true → 版本不一致就允许升(开发/测试,可刷回旧固件) - // updateonmismatch=false → 只有服务端版本更高才允许(正式,默认) - // ⚠️ 之前用 kDebugMode 分界是错的:测试同事拿到的往往是 release 包, - // 在开发环境照样点不动升级按钮。开关放后台才能按产品、按环境自由控制。 - // ⚠️ 字段可空(老服务端不下发),null 一律按 false 走严格判定—— - // 判宽的后果是线上用户被降级,判严只是开发时少一个便利。 - isUpdateAvailable.value = _allowDowngrade - ? latest != cur - : _isNewer(latest, cur); - } - - /// 入口:开始升级(下载 → 交接连接 → OTA 推送)。 - Future startUpdate() async { - if (isBusy) return; - if (!_bes.isConnected) { - _snack('otaError'.tr, 'otaNeedConnect'.tr); - return; - } - - // 关键:在断开现有连接前先捕获设备标识(disconnect 会清空); - // 服务字段已为空时(失败后重试)回退到控制器快照。 - final serviceId = _bes.connectedDeviceId.value; - final address = serviceId.isNotEmpty ? serviceId : _address; - final serviceName = _bes.connectedDeviceName.value; - final name = serviceName.isNotEmpty ? serviceName : deviceName.value; - if (address.isEmpty) { - _snack('otaError'.tr, 'otaNeedConnect'.tr); - return; - } - _address = address; - - // 产品配置缺失(AppConfig 晚于页面初始化等)时兜底重取一次。 - if (_downloadUrl.isEmpty) { - _loadProductConfig(); - if (_downloadUrl.isEmpty) { - _snack('otaError'.tr, 'otaVersionInfoFailed'.tr); - return; - } - } - - final cancelToken = CancelToken(); - _downloadCancelToken = cancelToken; - - try { - // 升级期间保持屏幕常亮(成功/失败/退出页面时关闭)。 - _setKeepScreenOn(true); - phase.value = OtaPhase.downloading; - downloadProgress.value = 0; - otaProgress.value = 0; - detailedStatus.value = 'otaPreparing'.tr; - - // 1) 释放 app 现有 SPP 连接,让插件自管 OTA 连接。 - await _bes.disconnectDevice(); - _appConnectionReleased = true; - // disconnect 返回只代表本端 socket 已关,耳机侧链路拆除还要在空中传播; - // 固件已缓存时下载不耗时、衔接近乎瞬时,立刻连 OTA 可能被拒 - //(多数固件同时只接受一条 SPP 链路),留 2s 缓冲等耳机拆完旧链路。 - // 曾出现过快速衔接导致 OTA 连接失败的案例,SDK 内部还有 3 次快速重连兜底。 - await Future.delayed(const Duration(seconds: 2)); - if (_disposed) return; // 期间已退出页面,不再继续 - // 2) 初始化原生 OTA 环境。 - await _besOta.init(); - - // 3) 下载固件。 - final filePath = await _downloadService.downloadFirmware( - downloadUrl: _downloadUrl, - cancelToken: cancelToken, - onProgress: (p) => downloadProgress.value = p, - onStatusUpdate: (s) => detailedStatus.value = s, - ); - - // 已退出/已取消则不再发起原生 OTA,避免在已销毁控制器上建立僵尸会话。 - if (_disposed || cancelToken.isCancelled) return; - - // 4) 启动 SPP OTA 2.0。 - phase.value = OtaPhase.upgrading; - detailedStatus.value = 'otaConnectingDevice'.tr; - // 看门狗从这里起覆盖「连接+握手+推送」全程:原生握手会持续发 onStatus、 - // 传输会持续发 onProgress,任一推进即重置;长时间静默(如原生卡住且不报错)兜底失败。 - // 下载阶段的卡死另由 Dio 超时与 CancelToken 处理,不纳入看门狗。 - _armWatchdog(); - await _besOta.startOta( - address: address, - name: name, - filePath: filePath, - protocol: 'SPP', - userFlag: 1, - upgradeType: 1, - user: 1, - ); - } catch (e, st) { - // 用户取消下载 / 页面已退出:静默收尾(控制器可能已销毁)。 - if (_disposed || (e is DioException && CancelToken.isCancel(e))) { - return; - } - Logger.e(_tag, '升级流程异常', e, st); - _onUpgradeError(null, e.toString()); // 统一走失败终态(含去重/看门狗关闭) - } - } - - /// 取消升级。 - void cancel() { - _downloadCancelToken?.cancel('user cancelled'); // 中止下载,阻断后续 startOta - _cancelWatchdog(); - _besOta.stopOta(); - _setKeepScreenOn(false); - phase.value = OtaPhase.idle; - detailedStatus.value = ''; - downloadProgress.value = 0; - otaProgress.value = 0; - } - - // ---- 看门狗:覆盖升级(连接/握手/推送)阶段,超时未推进则判失败 ---- - void _armWatchdog() { - if (_disposed) return; - _watchdog?.cancel(); - _watchdog = Timer(_watchdogTimeout, _onWatchdogTimeout); - } - - void _cancelWatchdog() { - _watchdog?.cancel(); - _watchdog = null; - } - - void _onWatchdogTimeout() { - if (_disposed) return; - if (phase.value == OtaPhase.upgrading) { - Logger.w(_tag, 'OTA 看门狗超时:${_watchdogTimeout.inSeconds}s 无进度推进'); - _onUpgradeError(null, 'otaTimeout'.tr); - } - } - - /// 屏幕常亮开关。失败不致命,吞掉异常。 - void _setKeepScreenOn(bool on) { - try { - on ? WakelockPlus.enable() : WakelockPlus.disable(); - } catch (e) { - Logger.w(_tag, '设置屏幕常亮失败: $e'); - } - } - - void _handleOtaEvent(BesOtaEvent e) { - // 进入终态(成功/失败)即锁定:stopOta 清理、设备重启断链、看门狗超时后迟到的 - // 进度/Canceled/Error 一律忽略,避免成功被覆盖成失败、或失败被迟到进度翻回升级中。 - // 新会话由 startUpdate 重置 phase。 - if (phase.value == OtaPhase.success || phase.value == OtaPhase.failed) { - return; - } - // 任何进度/状态推进都重置看门狗(成功/失败在各自处理里关闭看门狗)。 - if (e.event == 'onProgress' || e.event == 'onStatus') { - _armWatchdog(); - } - switch (e.event) { - case 'onProgress': - phase.value = OtaPhase.upgrading; - _updateProgress(e.progress ?? 0); - break; - case 'onStatus': - // SDK 的 OTA_SEND_DATA_PROGRESS 状态消息带原始浮点百分比(如 99.94765…), - // 当作进度更新并入单一写入口;其余状态文本只记日志不上屏—— - // 曾直接展示,但与进度文案高频交替会闪烁出看不清的内容。 - final numeric = double.tryParse(e.message ?? ''); - if (numeric != null && numeric > 0) { - phase.value = OtaPhase.upgrading; - _updateProgress((numeric / 100).clamp(0.0, 1.0)); - } - break; - case 'onSuccess': - _onUpgradeSuccess(); - break; - case 'onError': - _onUpgradeError(e.code, e.message); - break; - } - } - - /// 进度的单一写入口:onProgress(Android 为整数百分比)与数字型 onStatus - /// 消息(浮点百分比)都汇到这里,只在单调递增时刷新,避免两个来源 - /// 高频交替导致数字来回跳动。 - void _updateProgress(double p) { - if (p <= otaProgress.value) return; - otaProgress.value = p; - detailedStatus.value = 'otaUpgrading'.trParams({ - 'percent': (p * 100).toStringAsFixed(2), - }); - } - - void _onUpgradeSuccess() { - _cancelWatchdog(); - _setKeepScreenOn(false); - phase.value = OtaPhase.success; - otaProgress.value = 1.0; - detailedStatus.value = 'otaSuccessReboot'.tr; - currentVersion.value = latestVersion.value; - isUpdateAvailable.value = false; - _besOta.stopOta(); - } - - void _onUpgradeError(int? code, String? message) { - if (phase.value == OtaPhase.failed) return; // 已是失败终态,避免重复弹窗 - _cancelWatchdog(); - _setKeepScreenOn(false); - phase.value = OtaPhase.failed; - // 进入失败终态即释放原生 OTA 会话与 SPP 链路(与 _onUpgradeSuccess/cancel 对齐)。 - // 否则看门狗超时/连接失败后 native 会话仍在跑,占用唯一 SPP,且因终态已锁, - // 迟到的 onSuccess 会被吞——会卡死在「失败」直到用户退页面才由 onClose 清理。 - _besOta.stopOta(); - detailedStatus.value = 'otaFailed'.tr; - _snack( - 'otaFailed'.tr, - message?.isNotEmpty == true - ? message! - : 'otaFailedWithCode'.trParams({'code': '${code ?? -1}'}), - ); - } - - void _snack(String title, String msg) { - Get.snackbar( - title, - msg, - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.red[400], - colorText: Colors.white, - margin: const EdgeInsets.all(12), - duration: const Duration(seconds: 3), - ); - } - - /// [a] 是否比 [b] 新。 - /// - /// 逐段取前导数字比较,与服务端 `comm.CompareVersion` 同口径—— - /// `1.0.0-beta` / `1.2.3+4` 这类版本串不会抛异常(用 int.parse 会抛, - /// 被外层 catch 吞掉后表现为「明明有新版本却说已是最新」)。 - bool _isNewer(String a, String b) { - final pa = _splitVersion(a); - final pb = _splitVersion(b); - if (pa.isEmpty || pb.isEmpty) return a.trim() != b.trim(); - final len = pa.length > pb.length ? pa.length : pb.length; - for (var i = 0; i < len; i++) { - final x = i < pa.length ? pa[i] : 0; - final y = i < pb.length ? pb[i] : 0; - if (x != y) return x > y; - } - return false; - } - - /// 把版本串按 `.` 切开,每段只取前导数字(`3+4` → 3,`0-beta` → 0)。 - List _splitVersion(String v) { - final s = v.trim(); - if (s.isEmpty) return const []; - return s.split('.').map((seg) { - final m = RegExp(r'^\d+').firstMatch(seg.trim()); - return m == null ? 0 : int.parse(m.group(0)!); - }).toList(); - } -} diff --git a/apps/client/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart b/apps/client/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart deleted file mode 100644 index 8e202d32..00000000 --- a/apps/client/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart +++ /dev/null @@ -1,659 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:logger/web.dart'; -import '../../../data/services/ble_manager.dart'; -import '../../../data/services/bes_bluetooth_service.dart'; -import '../../../data/services/bluetooth_manager.dart'; // 添加蓝牙管理器导入 -import '../../../data/models/appconfig.dart'; -import '../../../data/models/appconfig_model.dart'; - -enum OtaCoreErrorType { - deviceInfoUnavailable, - productInfoUnavailable, - firmwareVersionFormatInvalid, -} - -class OtaCoreException implements Exception { - final OtaCoreErrorType type; - final Map params; - final String debugMessage; - - OtaCoreException({ - required this.type, - this.params = const {}, - this.debugMessage = '', - }); - - @override - String toString() => debugMessage; -} - -/// OTA核心服务 -/// 整合设备检查、版本管理和状态管理功能 -class OtaCoreService { - final BleManager _bleManager = Get.find(); - final BluetoothManager _bluetoothManager = - Get.find(); // 添加蓝牙管理器引用 - final int maxRetryAttempts = 3; - int currentRetryCount = 0; - - // ==================== 状态常量定义 ==================== - static const int NOT_STARTED = -1; // 未开始 - static const int VALIDATION_FILE = 0; // 验证文件 - static const int ONUPGRADE = 1; // 升级中 - static const int START_UPGRADING = 2; // 开始升级 - static const int UPGRADE_SUCCESS = 3; // 升级成功 - static const int UPGRADE_FAILED = 4; // 升级失败 - static const int CONNECTION_DISCONNECTION = 5; // 连接断开 - static const int CONNECTION_SUCCESS = 6; // 连接成功 - static const int DOWNLOADING_UP_FILES = 7; // 下载升级文件 - - /// 获取状态描述信息 - /// [type] 状态类型 - /// 返回对应的状态描述文本 - static String getStatusMessage(int type) { - switch (type) { - case NOT_STARTED: - return "未开始".tr; - case VALIDATION_FILE: - return "验证文件中".tr; - case ONUPGRADE: - return "升级中".tr; - case START_UPGRADING: - return "开始升级".tr; - case UPGRADE_SUCCESS: - return "升级成功".tr; - case UPGRADE_FAILED: - return "升级失败".tr; - case CONNECTION_DISCONNECTION: - return "连接断开".tr; - case CONNECTION_SUCCESS: - return "连接成功".tr; - case DOWNLOADING_UP_FILES: - return "下载升级文件".tr; - default: - return "未知状态".tr; - } - } - - // ==================== 设备检查功能 ==================== - - /// 检查BLE设备连接状态 - /// 返回true表示设备已连接,可以进行升级 - /// 返回false表示设备未连接,不能进行升级 - bool checkBleConnectionStatus() { - try { - // 检查BLE管理器的连接状态 - if (!_bleManager.isConnected) { - //_showBleDisconnectedDialog(); - return false; - } - - // 检查设备信息是否可用 - final deviceInfo = _bleManager.deviceInfo.value; - if (deviceInfo == null) { - _showDeviceInfoUnavailableDialog(); - return false; - } - - return true; - } catch (e) { - _showBleCheckFailedSnackbar('otaBleCheckErrorWithError'.trParams({'error': e.toString()})); - return false; - } - } - - /// 显示BLE设备未连接对话框 - void _showBleDisconnectedDialog() { - Get.dialog( - AlertDialog( - title: Text( - 'otaBleNotConnected'.tr, - style: TextStyle( - color: Colors.red[700], - fontWeight: FontWeight.w600, - ), - ), - content: Text( - 'otaBleNeedConnection'.tr, - style: TextStyle( - color: Colors.red[600], - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - style: TextButton.styleFrom( - foregroundColor: Colors.red[700], - ), - child: Text('okConfirm'.tr), - ), - ], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide( - color: Colors.red.withValues(alpha: 0.3), - width: 1, - ), - ), - backgroundColor: Colors.red[50], - ), - ); - } - - /// 显示设备信息不可用对话框 - void _showDeviceInfoUnavailableDialog() { - Get.dialog( - AlertDialog( - title: Text( - 'otaDeviceInfoUnavailableTitle'.tr, - style: TextStyle( - color: Colors.orange[700], - fontWeight: FontWeight.w600, - ), - ), - content: Text( - 'otaCannotGetDeviceInfo'.tr, - style: TextStyle( - color: Colors.orange[600], - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - style: TextButton.styleFrom( - foregroundColor: Colors.orange[700], - ), - child: Text('okConfirm'.tr), - ), - ], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide( - color: Colors.orange.withValues(alpha: 0.3), - width: 1, - ), - ), - backgroundColor: Colors.orange[50], - ), - ); - } - - /// 显示BLE连接检查失败提示 - /// [message] 错误信息 - void _showBleCheckFailedSnackbar(String message) { - Get.snackbar( - 'otaBleCheckFailed'.tr, - message, - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red[600], - colorText: Colors.white, - icon: const Icon( - Icons.bluetooth_disabled, - color: Colors.white, - ), - ); - } - - /// 检查设备电量是否满足升级条件 - /// 返回true表示电量充足,可以进行升级 - /// 返回false表示电量不足,不能进行升级 - bool checkBatteryLevel() { - try { - final deviceInfo = _bleManager.deviceInfo.value; - - if (deviceInfo == null) { - _showBatteryCheckFailedSnackbar('otaCannotGetDeviceInfoCheckConnection'.tr); - return false; - } - - // 获取设备类型以确定检查策略 - String deviceName = getDeviceName(); - final DBProduct? product = findProduct(deviceName); - if (product == null) { - _showBatteryCheckFailedSnackbar('otaCannotGetProductInfoWithName' - .trParams({'deviceName': deviceName})); - return false; - } - - final leftBattery = deviceInfo.leftEarBattery; - final rightBattery = deviceInfo.rightEarBattery; - - // 根据设备类型进行不同的电量检查策略 - if (product.devicetype == DeviceType.bleBluetoothSingleHeadset.value || - product.devicetype == DeviceType.smartEye.value || - product.devicetype == DeviceType.smartVoiceRecorder.value) { - // 单端耳机:只需要检查有效的那一端电量 - return _checkSingleHeadsetBattery(leftBattery, rightBattery); - } else if (product.devicetype == - DeviceType.bleBluetoothDoubleHeadset.value) { - // 双端耳机:需要检查左右两端电量 - return _checkDoubleHeadsetBattery(leftBattery, rightBattery); - } else { - // 其他设备类型,使用原有逻辑(双端检查) - return _checkDoubleHeadsetBattery(leftBattery, rightBattery); - } - } catch (e) { - _showBatteryCheckFailedSnackbar('otaBleCheckErrorWithError'.trParams({'error': e.toString()})); - return false; - } - } - - /// 检查单端耳机电量 - /// [leftBattery] 左耳电量 - /// [rightBattery] 右耳电量 - /// 返回true表示电量充足 - bool _checkSingleHeadsetBattery(int leftBattery, int rightBattery) { - // 单端耳机逻辑:优先检查左耳,如果左耳无效(<=0或127)则检查右耳 - int activeBattery; - String activeEar; - - if (leftBattery > 0 && leftBattery != 127) { - // 左耳有效,使用左耳电量 - activeBattery = leftBattery; - activeEar = 'leftEar'.tr; - } else if (rightBattery > 0 && rightBattery != 127) { - // 左耳无效,使用右耳电量 - activeBattery = rightBattery; - activeEar = 'rightEar'.tr; - } else { - // 两边都无效 - _showBatteryCheckFailedSnackbar('otaCannotGetBatteryInfo'.tr); - return false; - } - - // 检查有效端的电量是否大于20% - if (activeBattery <= 20) { - _showSingleHeadsetLowBatteryDialog(activeEar, activeBattery); - return false; - } - - return true; - } - - /// 检查双端耳机电量 - /// [leftBattery] 左耳电量 - /// [rightBattery] 右耳电量 - /// 返回true表示电量充足 - bool _checkDoubleHeadsetBattery(int leftBattery, int rightBattery) { - // 双端耳机逻辑:需要检查左右两端电量是否都大于20% - - // 检查电量数据有效性 - bool leftValid = leftBattery > 0 && leftBattery != 127; - bool rightValid = rightBattery > 0 && rightBattery != 127; - - if (!leftValid || !rightValid) { - _showBatteryCheckFailedSnackbar('otaCannotGetBatteryInfo'.tr); - return false; - } - - // 如果只有一端有效,按单端处理 - // if (!leftValid || !rightValid) { - // String activeEar = leftValid ? '左耳' : '右耳'; - // int activeBattery = leftValid ? leftBattery : rightBattery; - - // if (activeBattery <= 20) { - // _showSingleHeadsetLowBatteryDialog(activeEar, activeBattery); - // return false; - // } - // return true; - // } - - // 两端都有效,检查是否都大于20% - if (leftBattery <= 20 || rightBattery <= 20) { - _showLowBatteryDialog(leftBattery, rightBattery); - return false; - } - - return true; - } - - /// 显示单端耳机电量不足对话框 - /// [earSide] 耳机端(左耳/右耳) - /// [batteryLevel] 电量百分比 - void _showSingleHeadsetLowBatteryDialog(String earSide, int batteryLevel) { - String message = 'otaSingleBatteryLow'.trParams({ - 'ear': earSide, - 'battery': batteryLevel.toString(), - }); - - Get.dialog( - AlertDialog( - title: Text( - 'otaLowBattery'.tr, - style: TextStyle( - color: Colors.orange[700], - fontWeight: FontWeight.w600, - ), - ), - content: Text( - message, - style: TextStyle( - color: Colors.orange[600], - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - style: TextButton.styleFrom( - foregroundColor: Colors.orange[700], - ), - child: Text('okConfirm'.tr), - ), - ], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide( - color: Colors.orange.withValues(alpha: 0.3), - width: 1, - ), - ), - backgroundColor: Colors.orange[50], - ), - ); - } - - /// 显示电量不足对话框 - /// [leftBattery] 左耳电量 - /// [rightBattery] 右耳电量 - void _showLowBatteryDialog(int leftBattery, int rightBattery) { - String message = 'otaDoubleBatteryLow'.trParams({ - 'left': leftBattery.toString(), - 'right': rightBattery.toString(), - }); - - Get.dialog( - AlertDialog( - title: Text( - 'otaLowBattery'.tr, - style: TextStyle( - color: Colors.orange[700], - fontWeight: FontWeight.w600, - ), - ), - content: Text( - message, - style: TextStyle( - color: Colors.orange[600], - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - style: TextButton.styleFrom( - foregroundColor: Colors.orange[700], - ), - child: Text('okConfirm'.tr), - ), - ], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide( - color: Colors.orange.withValues(alpha: 0.3), - width: 1, - ), - ), - backgroundColor: Colors.orange[50], - ), - ); - } - - /// 显示电量检查失败提示 - /// [message] 错误信息 - void _showBatteryCheckFailedSnackbar(String message) { - Get.snackbar( - 'otaBatteryCheckFailed'.tr, - message, - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.orange[600], - colorText: Colors.white, - icon: const Icon( - Icons.warning_amber, - color: Colors.white, - ), - ); - } - - // ==================== 版本管理功能 ==================== - - - /// 按设备名安全地查产品配置。 - /// - /// 匹配规则比 `AppConfig.getproduct()` 宽:后者只做「产品名包含设备名」这一个 - /// 方向,恒玄耳机的广播名与产品表里的名字常常互为子串的另一头,单向匹配会漏。 - /// (getproduct 本身已经不会抛了,2026-09-03 起查不到返回 null。) - /// - /// 顺序:设备名互相包含 → 唯一的经典蓝牙耳机产品 → null。 - /// 老代码里的兜底名 "DeapSound" 是上一代品牌,早已不在产品表里,去掉了。 - DBProduct? findProduct(String deviceName) { - if (!AppConfig.isInitialized()) return null; - final products = AppConfig.config.products; - if (products.isEmpty) return null; - final n = deviceName.trim().toUpperCase(); - if (n.isNotEmpty && n != 'UNKNOWNEQUIPMENT') { - for (final p in products) { - final pn = p.devicename.trim().toUpperCase(); - if (pn.isEmpty) continue; - if (pn.contains(n) || n.contains(pn)) return p; - } - } - // 本项目只有恒玄(经典蓝牙耳机)一种设备,名字对不上时退化到它是安全的 - final headsets = products.where((p) => p.devicetype == 1).toList(); - if (headsets.length == 1) return headsets.first; - return null; - } - - /// 动态获取当前连接的设备名称 - /// 参考home_controller中的getDeviceName方法实现 - String getDeviceName() { - try { - // 优先从经典蓝牙获取设备名称 - final currentDevice = _bluetoothManager.currentDevice; - if (currentDevice?.name != null && currentDevice!.name.isNotEmpty) { - return currentDevice.name; - } - - // 其次从BLE设备信息获取 - final deviceInfo = _bleManager.deviceInfo.value; - if (deviceInfo?.name != null && deviceInfo!.name.isNotEmpty) { - return deviceInfo.name; - } - - // 恒玄耳机不在杰理 RCSP 链路上,上面两个来源都拿不到它。 - // 少了这一支,恒玄用户的设备名恒为 unknownEquipment, - // 后面按名字查产品必然落空 → 固件页取不到服务端版本。 - if (Get.isRegistered()) { - final besName = BesBluetoothService.to.connectedDeviceName.value; - if (besName.isNotEmpty) return besName; - } - - // 最后从BLE管理器的最后连接设备名称获取 - // if (_bleManager.currentDeviceName.isNotEmpty) { - // return _bleManager.currentDeviceName; - // } - - // 如果都获取不到,返回默认值 - return "unknownEquipment"; - } catch (e) { - Logger().e("获取设备名称失败: $e"); - return "unknownEquipment"; - } - } - - /// 获取当前设备版本信息 - /// 返回包含当前版本、产品ID和设备信息的Map - /// 抛出异常如果获取失败 - Future> getCurrentVersionInfo() async { - DeviceInfo? deviceInfo = _bleManager.deviceInfo.value; - - // 重试机制获取设备信息 - while (currentRetryCount < maxRetryAttempts) { - deviceInfo = _bleManager.deviceInfo.value; - - if (deviceInfo != null && - deviceInfo.firmwareVersion.isNotEmpty && - deviceInfo.firmwareProductId.isNotEmpty) { - break; - } - - await _bleManager.fetchDeviceInfo(); - await Future.delayed(const Duration(seconds: 5)); - currentRetryCount++; - } - - if (deviceInfo == null || - deviceInfo.firmwareVersion.isEmpty || - deviceInfo.firmwareProductId.isEmpty) { - throw OtaCoreException( - type: OtaCoreErrorType.deviceInfoUnavailable, - debugMessage: 'deviceInfoUnavailable', //获取设备信息失败,请检查连接 - ); - } - - String deviceName = getDeviceName(); - - // 获取产品信息以确定设备类型 - final DBProduct? product = findProduct(deviceName); - if (product == null) { - throw OtaCoreException( - type: OtaCoreErrorType.productInfoUnavailable, - params: {'deviceName': deviceName}, - debugMessage: 'productInfoUnavailable: $deviceName', //根据设备名称未找到产品信息 - ); - } - - // 根据设备类型进行不同的版本校验 - String currentVersion; - if (product.devicetype == DeviceType.bleBluetoothSingleHeadset.value) { - // 单端耳机:固件版本只有三位数(如:1.2.3) - final firmwareParts = deviceInfo.firmwareVersion.split('.'); - if (firmwareParts.length < 3) { - throw OtaCoreException( - type: OtaCoreErrorType.firmwareVersionFormatInvalid, - params: {'firmwareVersion': deviceInfo.firmwareVersion}, - debugMessage: - 'single firmwareVersion format invalid: ${deviceInfo.firmwareVersion}', //单端耳机固件版本格式无效 - ); - } - currentVersion = firmwareParts.sublist(0, 3).join('.'); - Logger().d("单端耳机版本: $currentVersion"); - } else if (product.devicetype == - DeviceType.bleBluetoothDoubleHeadset.value) { - // 双端耳机:固件版本有六位数(如:1.2.3.1.2.3) - final firmwareParts = deviceInfo.firmwareVersion.split('.'); - if (firmwareParts.length < 6) { - throw OtaCoreException( - type: OtaCoreErrorType.firmwareVersionFormatInvalid, - params: {'firmwareVersion': deviceInfo.firmwareVersion}, - debugMessage: - 'double firmwareVersion length invalid: ${deviceInfo.firmwareVersion}', //双端耳机固件版本格式无效 - ); - } - final firstPart = firmwareParts.sublist(0, 3).join('.'); - final secondPart = firmwareParts.sublist(3, 6).join('.'); - - Logger().d("双端耳机版本 - firstPart: $firstPart, secondPart: $secondPart"); - - if (firstPart != secondPart) { - throw OtaCoreException( - type: OtaCoreErrorType.firmwareVersionFormatInvalid, - params: {'firmwareVersion': deviceInfo.firmwareVersion}, - debugMessage: - 'double firmwareVersion mismatch: ${deviceInfo.firmwareVersion}', //双端耳机固件版本不一致,前三位和后三位不一样 - ); - } - currentVersion = firstPart; - } else { - // 其他设备类型,使用原有逻辑 - final firmwareParts = deviceInfo.firmwareVersion.split('.'); - if (firmwareParts.length >= 3) { - currentVersion = firmwareParts.sublist(0, 3).join('.'); - } else { - currentVersion = deviceInfo.firmwareVersion; - } - Logger().d("其他设备类型版本: $currentVersion"); - } - - return { - 'currentVersion': currentVersion, - 'productId': deviceInfo.firmwareProductId, - 'deviceInfo': deviceInfo, - }; - } - - /// 获取最新版本信息 - /// 返回包含最新版本、更新描述和更新包地址的Map - /// 抛出异常如果获取失败 - Future> getLatestVersionInfo() async { - // 动态获取当前连接的设备名称 - String deviceName = getDeviceName(); - - final DBProduct? product = findProduct(deviceName); - if (product == null) { - throw "无法获取产品信息,设备名称: $deviceName"; - } - - // ⚠️ 强更以**后台 product.isforceupdate 字段**为准。 - // 原来写的是 `product.version?.contains("+")`——拿版本号里有没有 "+" - // 当强更开关,跟后台配置完全脱节(后台勾了拦不住、版本号带 + 的全被强更)。 - // 这和 App 版本更新那边踩过的土规则是同一个毛病,别改回去。 - return { - 'latestVersion': product.version?.split('+').first ?? "", - 'updateDescription': product.updatedescription ?? "", - 'updatePackageAddress': product.updatepackageaddress ?? "", - 'isForceUpdate': product.isforceupdate ?? false, - }; - } - - /// 把版本串按 `.` 切开,每段只取前导数字(`3+4` → 3,`0-beta` → 0)。 - /// 与服务端 comm.CompareVersion 的口径一致。 - static List _splitVersion(String v) { - final s = v.trim(); - if (s.isEmpty) return const []; - return s.split('.').map((seg) { - final m = RegExp(r'^\d+').firstMatch(seg.trim()); - return m == null ? 0 : int.parse(m.group(0)!); - }).toList(); - } - - /// 比较版本号,判断是否有新版本可用 - /// [currentVersion] 当前版本号 - /// [latestVersion] 最新版本号 - /// 返回true表示有新版本可用(只支持升级) - bool isNewVersionAvailable(String currentVersion, String latestVersion) { - try { - // ⚠️ 逐段取**前导数字**,不能用 int.parse:固件版本出现过 - // `1.0.0-beta` / `1.2.3+4` 这类后缀,int.parse 直接抛, - // 异常被下面的 catch 吞掉后返回 false —— 表现是「明明有新版本却说已是最新」。 - List currentParts = _splitVersion(currentVersion); - List latestParts = _splitVersion(latestVersion); - if (currentParts.isEmpty || latestParts.isEmpty) return false; - - // 确保两个版本号长度一致,短的用0补齐 - int maxLength = currentParts.length > latestParts.length - ? currentParts.length - : latestParts.length; - - while (currentParts.length < maxLength) { - currentParts.add(0); - } - while (latestParts.length < maxLength) { - latestParts.add(0); - } - - // 从高位到低位比较版本号 - for (int i = 0; i < maxLength; i++) { - if (latestParts[i] > currentParts[i]) { - return true; - } else if (latestParts[i] < currentParts[i]) { - return false; - } - } - - return false; - } catch (e) { - Logger().d('版本比较错误: $e'); - return false; - } - } -} diff --git a/apps/client/lib/modules/ota_upgrade/controllers/ota_download_contropller.dart b/apps/client/lib/modules/ota_upgrade/controllers/ota_download_contropller.dart deleted file mode 100644 index a3f97485..00000000 --- a/apps/client/lib/modules/ota_upgrade/controllers/ota_download_contropller.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'dart:io'; -import 'package:dio/dio.dart'; -import 'package:dio/io.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:path/path.dart'; -import 'package:logger/web.dart'; - -/// OTA下载服务 -/// 负责处理固件包的下载、文件管理等功能 -class OtaDownloadService { - late final Dio _dio; - - /// 构造函数 - /// 初始化Dio实例并配置SSL证书处理 - OtaDownloadService() { - _dio = Dio(BaseOptions( - connectTimeout: const Duration(seconds: 30), // 增加连接超时时间 - receiveTimeout: Duration.zero, - sendTimeout: const Duration(seconds: 30), // 增加发送超时时间 - headers: {'Accept-Encoding': 'gzip'}, - )); - - // 配置SSL证书验证 - (_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { - final client = HttpClient(); - - // 处理SSL证书验证 - client.badCertificateCallback = - (X509Certificate cert, String host, int port) { - // 在生产环境中,应该进行适当的证书验证 - // 这里为了解决证书过期问题,暂时跳过验证 - // 建议:在生产环境中实现更安全的证书验证逻辑 - Logger().w('SSL证书验证跳过: $host:$port'); - return true; - }; - - return client; - }; - } - - /// 下载固件包 - /// [downloadUrl] 下载地址 - /// [onProgress] 下载进度回调 - /// [onStatusUpdate] 状态更新回调 - /// 返回下载文件的本地路径 - /// 抛出异常如果下载失败 - Future downloadFirmware({ - required String downloadUrl, - required Function(double) onProgress, - required Function(String) onStatusUpdate, - }) async { - onStatusUpdate('准备下载固件包...'); - - final fileName = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1); - const directoryName = "ota_upgrades"; - - // 获取下载目录 - onStatusUpdate('正在获取下载目录...'); - final Directory? downloadsDir = await getApplicationSupportDirectory(); - if (downloadsDir == null) { - throw FileSystemException('无法访问下载目录'); - } - - final targetDir = Directory('${downloadsDir.path}/Download/$directoryName'); - if (!await targetDir.exists()) { - onStatusUpdate('创建下载目录...'); - await targetDir.create(recursive: true); - } - - final savePath = '${targetDir.path}/$fileName'; - - // 检查是否已存在同名文件 - onStatusUpdate('检查本地文件...'); - if (await _checkExistingFile(targetDir, fileName)) { - onStatusUpdate('文件已存在,准备升级...'); - return savePath; - } - - // 清理旧文件 - onStatusUpdate('清理旧文件...'); - await _cleanOldFiles(targetDir); - - // 开始下载 - onStatusUpdate('开始下载固件包...'); - Logger().d('文件保存路径: $savePath'); - - final options = Options( - receiveTimeout: Duration.zero, - followRedirects: true, - maxRedirects: 5, - ); - - await _dio.download( - downloadUrl, - savePath, - options: options, - onReceiveProgress: (received, total) { - if (total != -1) { - final progress = received / total; - onProgress(progress); - onStatusUpdate('正在下载固件包... ${(progress * 100).toStringAsFixed(1)}%'); - } - }, - ); - - // 验证文件是否下载成功 - final file = File(savePath); - if (!await file.exists()) { - throw FileSystemException('文件下载失败,请检查写入权限'); - } - - onStatusUpdate('下载完成,准备升级...'); - return savePath; - } - - /// 检查是否存在同名文件 - /// [targetDir] 目标目录 - /// [fileName] 文件名 - /// 返回true表示文件已存在 - Future _checkExistingFile(Directory targetDir, String fileName) async { - try { - final files = await targetDir.list().toList(); - for (var entity in files) { - if (await FileSystemEntity.isFile(entity.path)) { - final name = basename(entity.path); - if (name == fileName) { - return true; - } - } - } - return false; - } catch (e) { - Logger().d('检查本地文件失败: $e'); - return false; - } - } - - /// 清理旧文件 - /// [targetDir] 目标目录 - /// 抛出异常如果清理失败 - Future _cleanOldFiles(Directory targetDir) async { - try { - final files = await targetDir.list().toList(); - for (var entity in files) { - if (await FileSystemEntity.isFile(entity.path)) { - await entity.delete(); - } - } - } catch (e) { - throw Exception('删除旧文件失败: $e'); - } - } -} \ No newline at end of file diff --git a/apps/client/lib/modules/ota_upgrade/controllers/bes_ota_download_service.dart b/apps/client/lib/modules/ota_upgrade/controllers/ota_download_service.dart similarity index 100% rename from apps/client/lib/modules/ota_upgrade/controllers/bes_ota_download_service.dart rename to apps/client/lib/modules/ota_upgrade/controllers/ota_download_service.dart diff --git a/apps/client/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/apps/client/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart deleted file mode 100644 index c17c940c..00000000 --- a/apps/client/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ /dev/null @@ -1,424 +0,0 @@ -import 'dart:async'; -import '/data/services/bes_bluetooth_service.dart'; - -import 'package:classic_bluetooth/classic_bluetooth.dart'; -import 'package:device_jieli/device_jieli.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -// import 'package:logger/web.dart'; -import 'package:get_storage/get_storage.dart'; -import '../../../data/services/ble_manager.dart'; -import 'ota_core_contropller.dart'; -import 'ota_download_contropller.dart'; -import '../views/ota_upgrade_popup_view.dart'; -import '../../../core/utils/logger.dart'; - -/// OTA升级主控制器 - 重构后的简化版本 -/// 负责协调各个服务,管理升级流程的整体状态 -class OtaUpgradeController extends GetxController { - // 服务依赖 - 使用新的合并后的服务 - final _coreService = OtaCoreService(); - final _downloadService = OtaDownloadService(); - final _uiService = OtaUpgradePopUp(); - final _bleManager = Get.find(); - final _jlota = Jielihome.instance; - StreamSubscription? _otaEventSub; - - // 状态管理 - final RxString errorMessage = ''.obs; - final RxString latestVersion = ''.obs; - final RxString currentVersion = ''.obs; - final RxString detailedStatus = ''.obs; - final RxString updatedescription = ''.obs; - final RxDouble downloadProgress = 0.0.obs; - final RxDouble progress = 0.0.obs; - final RxInt type = OtaCoreService.NOT_STARTED.obs; - final RxBool isUpdateAvailable = false.obs; - final RxBool isDownloading = false.obs; - final RxBool isUpgrade = false.obs; - final RxBool isFetchingDeviceInfo = false.obs; - final RxBool showDetailedStatus = false.obs; - final RxBool isBleConnected = true.obs; - final GetStorage _storage = GetStorage(); - - String _address = ''; - String _updatePackageAddress = ''; - String? _lastConnectedDeviceImage; // 新增:存储上一次连接设备的图片链接 - - get lastConnectedDeviceImage => _lastConnectedDeviceImage; - - @override - void onInit() { - super.onInit(); - _loadLastConnectedDeviceImage(); // 加载持久化的设备图片 - _bindBesVersion(); - _initializeController(); - } - - @override - void onClose() { - _jlota.otaCancel(); - _otaEventSub?.cancel(); - _besVersionWorker?.dispose(); - _otaEventSub = null; - _bleManager.setEnterOta(false); - isUpgrade.value = false; - super.onClose(); - } - - /// 恒玄耳机的固件版本是连接后异步查回来的(`AA 06` → `0x86` 解析), - /// 一次性读取会赶不上:页面先开、版本后到就一直空着。 - /// 这里改成订阅,版本一到就刷新界面。 - Worker? _besVersionWorker; - - void _bindBesVersion() { - if (!Get.isRegistered()) return; - final bes = BesBluetoothService.to; - void apply(String v) { - if (v.isEmpty || !bes.isConnected) return; - currentVersion.value = v; - isBleConnected.value = true; - // 有了当前版本才能和服务端最新版本比对 - isUpdateAvailable.value = - _coreService.isNewVersionAvailable(v, latestVersion.value); - } - - apply(bes.deviceVersion.value); - _besVersionWorker = ever(bes.deviceVersion, apply); - // 连上但版本还没回来时,主动补查一次 - if (bes.isConnected && bes.deviceVersion.value.isEmpty) { - bes.sendData([0xAA, 0x06]); - } - } - - /// 从本地存储加载上一次连接设备的图片链接 - void _loadLastConnectedDeviceImage() { - _lastConnectedDeviceImage = _storage.read('last_connected_device_image'); - Logger.d( - 'OtaUpgradeController', '加载上一次连接设备的图片: $_lastConnectedDeviceImage'); - } - - /// 初始化控制器 - /// 设置事件监听并加载版本信息 - /// 恒玄耳机是否连着。恒玄不在杰理 RCSP 链路上, - /// [OtaCoreService.checkBleConnectionStatus] 查的是 BleManager(杰理), - /// 对恒玄恒为 false。 - bool get _besConnected => - Get.isRegistered() && - BesBluetoothService.to.isConnected; - - Future _initializeController() async { - // ⚠️ 这里原来是「杰理没连上就直接 return」,而本项目用的是恒玄耳机—— - // 于是 _loadVersionInfo() 永远不会被调到,固件页从头到尾**没有任何一次 - // 服务端拉取**,看起来就是「接口没通」。实际上服务端版本是随 - // user_getappconfig 的 products 下发的(product.version / - // updatepackageaddress / updatedescription / isforceupdate), - // 一直都在,只是没人去读。 - final bool jieliReady = _coreService.checkBleConnectionStatus(); - if (!jieliReady && !_besConnected) { - isBleConnected.value = false; - return; - } - isBleConnected.value = true; - // 杰理的 OTA 事件流只有杰理链路才有意义 - if (jieliReady) _listenToOtaEvents(); - await _loadVersionInfo(); - } - - /// 加载版本信息 - /// 获取当前设备版本和最新版本信息,检查是否有更新 - Future _loadVersionInfo() async { - try { - isFetchingDeviceInfo.value = true; - errorMessage.value = ''; - - // 获取当前版本信息。 - // 恒玄耳机不在杰理 RCSP 链路上,_coreService 读不到它的版本, - // 但连接时已经用 `AA 06` 查过并解析好了,这里直接取。 - if (Get.isRegistered() && - BesBluetoothService.to.isConnected && - BesBluetoothService.to.deviceVersion.value.isNotEmpty) { - currentVersion.value = BesBluetoothService.to.deviceVersion.value; - isBleConnected.value = true; - } else { - final currentInfo = await _coreService.getCurrentVersionInfo(); - currentVersion.value = currentInfo['currentVersion']; - } - - // 获取蓝牙地址 - final connectedDevices = await ClassicBluetooth.getConnectedDevices(); - if (connectedDevices.isNotEmpty) { - _address = connectedDevices.first.address; - } - - // 获取最新版本信息 - final latestInfo = await _coreService.getLatestVersionInfo(); - latestVersion.value = latestInfo['latestVersion']; - _updatePackageAddress = latestInfo['updatePackageAddress']; - - // 设置更新描述信息 - updatedescription.value = - latestInfo['updateDescription'] ?? 'otaDefaultUpdateDesc'.tr; - - // 检查是否有更新 - isUpdateAvailable.value = _coreService.isNewVersionAvailable( - currentVersion.value, - latestVersion.value, - ); - } catch (e) { - _handleError('error'.tr, _getUserFriendlyErrorMessage(e)); - } finally { - isFetchingDeviceInfo.value = false; - } - } - - /// 检查更新 - /// 显示更新确认对话框或错误提示 - Future checkUpdate() async { - try { - if (latestVersion.value.isNotEmpty) { - _uiService.showUpdateDialog(latestVersion.value, _startUpdateProcess); - } else { - _uiService.showErrorSnackbar('error'.tr, 'otaGetLatestVersionFailed'.tr, - backgroundColor: Colors.orange[800]); - } - } catch (e) { - _uiService.showErrorSnackbar('error'.tr, 'otaGetVersionInfoFailed'.tr, - backgroundColor: Colors.orange[800]); - } - } - - /// 开始更新流程的公共方法 - /// 供视图调用的更新入口点 - Future startUpdate() async { - // 恒玄耳机目前**没有固件推送通道**:bluetooth_manager 插件里没有任何 - // OTA 实现,下面这条流程整条是杰理 RCSP 的(下载 → RCSP 分包 → 重启配对)。 - // 让它跑下去只会在中途失败,用户看到的是一串莫名其妙的错误码。 - // 所以这里明确拦一下:版本对比与更新提示照常给,升级动作说清楚做不了。 - if (_besConnected && !_coreService.checkBleConnectionStatus()) { - _uiService.showErrorSnackbar( - 'tip'.tr, - 'otaBesNotSupported'.tr, - backgroundColor: Colors.orange[800], - ); - return; - } - //立即给用户状态反馈 - showDetailedStatus.value = true; - detailedStatus.value = 'otaCheckingDeviceStatus'.tr; - - await _startUpdateProcess(); - } - - /// 检查设备状态后开始下载和升级 - Future _startUpdateProcess() async { - // 检查设备电量 - if (!_coreService.checkBatteryLevel()) { - return; - } - - await _startDownload(); - } - - /// 开始下载固件 - /// 初始化OTA环境并下载固件包 - Future _startDownload() async { - try { - await _bleManager.setEnterOta(true); - await Future.delayed(const Duration(seconds: 2)); - // device_jieli 已经在全局 InitialBinding 里 initialize 过; - // 这里无需重复 init —— 直接订阅 OTA 事件流即可。 - - isDownloading.value = true; - showDetailedStatus.value = true; - type.value = OtaCoreService.DOWNLOADING_UP_FILES; - - final filePath = await _downloadService.downloadFirmware( - downloadUrl: _updatePackageAddress, - onProgress: (progress) { - downloadProgress.value = progress; - }, - onStatusUpdate: (status) { - detailedStatus.value = status; - }, - ); - - isDownloading.value = false; - await _startUpgrading(filePath); - } catch (e) { - isDownloading.value = false; - detailedStatus.value = 'otaDownloadFailedCheckNetwork'.tr; - showDetailedStatus.value = false; - _handleError('otaDownloadFailed'.tr, e.toString()); - Logger.d('OtaUpgradeController', '下载失败: $e'); - } - } - - /// 开始升级 - /// [filePath] 固件文件路径 - /// 启动OTA升级过程 - Future _startUpgrading(String filePath) async { - detailedStatus.value = 'otaPreparingConnection'.tr; - await Future.delayed(const Duration(milliseconds: 500)); - detailedStatus.value = 'otaConnectingDevice'.tr; - _jlota.otaStart(address: _address, firmwareFilePath: filePath); - } - - /// 停止升级 - /// 关闭OTA连接 - void stopUpgrading() { - _jlota.otaCancel(); - } - - /// 重试连接 - /// 清除错误信息并重新加载版本信息 - void retryConnection() { - errorMessage.value = ''; - _loadVersionInfo(); - } - - /// 获取状态描述 - /// [type] 状态类型 - /// 返回对应的状态描述文本 - String getStatusMessage(int type) { - return OtaCoreService.getStatusMessage(type); - } - - /// 监听OTA事件 - /// 订阅 device_jieli 的统一事件流,过滤 OTA 相关事件并转发到分发器。 - void _listenToOtaEvents() { - _otaEventSub?.cancel(); - _otaEventSub = _jlota.events.listen(_handleOtaEvent); - } - - /// 处理 device_jieli 抛出的 OTA / 连接事件。 - /// - [OtaStateEvent]: 进度 / 状态机推进 - /// - [OtaErrorEvent]: 升级失败 - /// - [ConnectionStateEvent]: 设备连接 / 断开(OTA 期间用于"中断"指示) - void _handleOtaEvent(JieliEvent event) { - if (event is OtaStateEvent) { - switch (event.state) { - case OtaState.entering: - case OtaState.inquiring: - case OtaState.notifyingSize: - type.value = OtaCoreService.START_UPGRADING; - isUpgrade.value = true; - detailedStatus.value = 'otaUpgradeStarted'.tr; - break; - case OtaState.transferring: - if (event.percent >= 0) { - // device_jieli 发的是 0..100 整数;UI 这边按 0..1 浮点用。 - progress.value = event.percent / 100.0; - type.value = OtaCoreService.ONUPGRADE; - isUpgrade.value = true; - detailedStatus.value = 'otaUpgradeProgressWithPercent' - .trParams({'percent': event.percent.toStringAsFixed(1)}); - } - break; - case OtaState.verifying: - detailedStatus.value = 'otaUpgradeStarted'.tr; - break; - case OtaState.rebooting: - // 设备重启等待,仍在升级中 - isUpgrade.value = true; - break; - case OtaState.done: - _handleUpgradeSuccess(); - break; - case OtaState.failed: - _handleUpgradeError(-1); - break; - case OtaState.cancelled: - isUpgrade.value = false; - showDetailedStatus.value = false; - break; - case OtaState.idle: - break; - } - return; - } - if (event is OtaErrorEvent) { - _handleUpgradeError(event.code); - return; - } - if (event is ConnectionStateEvent) { - if (event.state == ConnectionStateEvent.connectionDisconnect && - isUpgrade.value) { - _handleConnectionLost(); - } else if (event.state == ConnectionStateEvent.connectionOk && - isUpgrade.value) { - _handleConnectionSuccess(); - } - } - } - - /// 处理升级成功 - /// 更新状态并显示成功对话框 - void _handleUpgradeSuccess() { - type.value = OtaCoreService.UPGRADE_SUCCESS; - isUpgrade.value = false; - detailedStatus.value = 'otaUpgradeSuccessComplete'.tr; - showDetailedStatus.value = false; - - // 更新版本信息 - currentVersion.value = latestVersion.value; - isUpdateAvailable.value = false; - - // 显示成功对话框 - _uiService.showUpgradeSuccessDialog(); - } - - /// 处理升级错误 - /// [errorCode] 错误代码 - /// 更新状态并显示错误提示 - void _handleUpgradeError(dynamic errorCode) { - type.value = OtaCoreService.UPGRADE_FAILED; - isUpgrade.value = false; - detailedStatus.value = 'otaUpgradeFailedWithCode'.trParams({'code': errorCode.toString()}); - showDetailedStatus.value = false; - _uiService.showErrorSnackbar( - 'otaUpgradeFailedTitle'.tr, - 'otaUpgradeFailedDesc'.trParams({'code': errorCode.toString()}), - ); - } - - /// 处理连接断开 - /// 更新连接状态 - void _handleConnectionLost() { - type.value = OtaCoreService.CONNECTION_DISCONNECTION; - isUpgrade.value = false; - detailedStatus.value = 'otaDeviceDisconnected'.tr; - showDetailedStatus.value = false; - } - - /// 处理连接成功 - /// 更新连接状态 - void _handleConnectionSuccess() { - type.value = OtaCoreService.CONNECTION_SUCCESS; - isUpgrade.value = true; - detailedStatus.value = 'otaDeviceConnected'.tr; - } - - /// 统一错误处理 - /// [title] 错误标题 - /// [message] 错误信息 - /// 设置错误状态并显示错误提示 - void _handleError(String title, String message) { - errorMessage.value = message; - _uiService.showErrorSnackbar(title, message); - } - - String _getUserFriendlyErrorMessage(Object error) { - if (error is OtaCoreException) { - switch (error.type) { - case OtaCoreErrorType.deviceInfoUnavailable: - return 'otaDeviceInfoUnavailable'.tr; - case OtaCoreErrorType.productInfoUnavailable: - return 'otaProductInfoUnavailable'.tr; - case OtaCoreErrorType.firmwareVersionFormatInvalid: - return 'otaFirmwareVersionInvalid'.tr; - } - } - return 'otaVersionInfoFailed'.tr; - } -} diff --git a/apps/client/lib/modules/ota_upgrade/views/bes_ota_upgrade_view.dart b/apps/client/lib/modules/ota_upgrade/views/device_ota_view.dart similarity index 100% rename from apps/client/lib/modules/ota_upgrade/views/bes_ota_upgrade_view.dart rename to apps/client/lib/modules/ota_upgrade/views/device_ota_view.dart diff --git a/apps/client/lib/modules/ota_upgrade/views/ota_upgrade_view.dart b/apps/client/lib/modules/ota_upgrade/views/ota_upgrade_view.dart deleted file mode 100644 index 572dcc74..00000000 --- a/apps/client/lib/modules/ota_upgrade/views/ota_upgrade_view.dart +++ /dev/null @@ -1,1132 +0,0 @@ -import 'package:flutter/material.dart'; -import '/data/services/bes_bluetooth_service.dart'; -import '/core/utils/device_connection.dart'; -import 'package:get/get.dart'; -import '../controllers/ota_upgrade_controller.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '../../../data/services/ble_manager.dart'; - -class OtaUpgradeView extends GetView { - const OtaUpgradeView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return WillPopScope( - onWillPop: () async { - if (controller.isUpgrade.value && controller.showDetailedStatus.value) { - // 显示确认弹窗 - await Get.dialog( - AlertDialog( - title: Text('tip'.tr), // 提示 - content: - Text('upgradeExitConfirm'.tr), // 当前正在升级,退出将中断升级进程,确定要退出吗? - actions: [ - TextButton( - onPressed: () => Get.back(), // 关闭弹窗 - child: Text('cancel'.tr), // 取消 - ), - TextButton( - onPressed: () { - controller.stopUpgrading(); - Get.back(); // 关闭弹窗 - Get.back(); // 返回上一页 - }, - child: Text('confirm'.tr), // 确定 - ), - ], - ), - ); - return false; // 阻止默认返回 - } - return true; // 允许返回 - }, - child: Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[100], - appBar: AppBar( - automaticallyImplyLeading: false, - centerTitle: true, - elevation: 0, - backgroundColor: isDarkMode ? Colors.black : Colors.white, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - size: 20.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - onPressed: () { - if (controller.isUpgrade.value && - controller.showDetailedStatus.value) { - // 显示确认弹窗 - Get.dialog( - AlertDialog( - title: Text('tip'.tr), // 提示 - content: Text( - 'upgradeExitConfirm'.tr), // 当前正在升级,退出将中断升级进程,确定要退出吗? - actions: [ - TextButton( - onPressed: () => Get.back(), // 关闭弹窗 - child: Text('cancel'.tr), // 取消 - ), - TextButton( - onPressed: () { - controller.stopUpgrading(); - Get.back(); // 关闭弹窗 - Get.back(); // 返回上一页 - }, - child: Text('confirm'.tr), // 确定 - ), - ], - ), - ); - } else { - Get.back(); // 正常返回 - } - }, - ), - title: Text( - 'firmwareUpgrade'.tr, //固件升级 - style: TextStyle( - fontSize: 20.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - body: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 设备信息卡片 - 优化后的版本 - _buildDeviceCard(isDarkMode), - SizedBox(height: 16.h), - - // 更新说明 - 当有可用更新时显示 - //_buildUpdateNotes(isDarkMode), - - // 状态显示区域 - Obx(() { - if (controller.isFetchingDeviceInfo.value) { - return _buildLoadingIndicator(isDarkMode); - } - if (controller.errorMessage.value.isNotEmpty) { - return _buildErrorInfo(isDarkMode); - } - return _buildUpdateButton(isDarkMode); - }), - ], - ), - ), - ), - ), - ); - } - - // 设备信息卡片 - Widget _buildDeviceCard(bool isDarkMode) { - return Container( - width: double.infinity, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [ - const Color(0xFF1A1A1A).withValues(alpha: 0.9), - const Color(0xFF2A2A2A).withValues(alpha: 0.9), - ] - : [ - const Color(0xFFF8FAFF), - const Color(0xFFE3F2FD), - ], - ), - borderRadius: BorderRadius.circular(20.r), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withValues(alpha: 0.3) - : Colors.black.withValues(alpha: 0.08), - blurRadius: 20, - offset: const Offset(0, 8), - spreadRadius: 0, - ), - ], - ), - child: Padding( - padding: EdgeInsets.all(20.w), - child: Column( - children: [ - // 设备图标区域 - _buildDeviceIcon(isDarkMode), - SizedBox(height: 16.h), - // 设备详细信息区域 - _buildDeviceDetailInfo(isDarkMode), - SizedBox(height: 16.h), - // 版本信息区域 - _buildVersionInfo(isDarkMode), - ], - ), - ), - ); - } - - // 设备图标 - Widget _buildDeviceIcon(bool isDarkMode) { - return Container( - padding: EdgeInsets.all(6.w), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.05) - : Colors.white.withValues(alpha: 0.7), - borderRadius: BorderRadius.circular(16.r), - border: Border.all( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.1) - : Colors.blue.withValues(alpha: 0.1), - width: 1, - ), - ), - child: GetBuilder( - builder: (controller) { - // 一律按设备名读本地产品小图(<设备名>_icon.png,规则见 DeviceImages)。 - // - // ⚠️ 这里原来会先试服务端下发的 productimage 网络图,已去掉: - // 产品图统一走本地资源(2026-09-08 定的),保留网络图会出现 - // 「同一台设备在设备页显示本地图、在这里显示另一张网络图」。 - // 顺带也省掉了加载中转圈和破图兜底那一堆分支。 - return ClipRRect( - borderRadius: BorderRadius.circular(12.r), - child: SizedBox( - width: 120.h, - height: 120.h, - child: Image.asset( - DeviceConnection.currentIconImage(), - // 产品图有留白,用 contain 等比缩放,cover 会把机身裁掉 - fit: BoxFit.contain, - errorBuilder: (_, __, ___) => Icon( - Icons.headphones, - size: 48.h, - color: isDarkMode ? Colors.white54 : Colors.blue, - ), - ), - ), - ); - }, - ), - ); - } - - // 设备详细信息显示 - Widget _buildDeviceDetailInfo(bool isDarkMode) { - return Obx(() { - final deviceInfo = Get.find().deviceInfo.value; - // 恒玄耳机走的是另一条链路,杰理的 deviceInfo 里没有它 - final besName = Get.isRegistered() && - BesBluetoothService.to.isConnected - ? BesBluetoothService.to.connectedDeviceName.value - : ''; - - return Container( - padding: EdgeInsets.all(14.w), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.03) - : Colors.white.withValues(alpha: 0.8), - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.08) - : Colors.grey.withValues(alpha: 0.2), - width: 1, - ), - ), - child: Column( - children: [ - // 设备型号信息 - _buildDeviceInfoRow( - label: 'deviceModel'.tr, // 设备型号 - value: besName.isNotEmpty - ? besName - : (deviceInfo?.name ?? 'unknownDevice'.tr), - isDarkMode: isDarkMode, - ), - // SizedBox(height: 10.h), - // // 电量信息区域 - // Row( - // children: [ - // // 左耳电量 - // Expanded( - // child: _buildBatteryInfo( - // label: '左耳电量', - // batteryLevel: deviceInfo?.leftEarBattery ?? 0, - // isCharging: deviceInfo?.leftEarCharging ?? false, - // isDarkMode: isDarkMode, - // icon: Icons.headset, - // ), - // ), - // SizedBox(width: 10.w), - // // 右耳电量 - // Expanded( - // child: _buildBatteryInfo( - // label: '右耳电量', - // batteryLevel: deviceInfo?.rightEarBattery ?? 0, - // isCharging: deviceInfo?.rightEarCharging ?? false, - // isDarkMode: isDarkMode, - // icon: Icons.headset, - // ), - // ), - // ], - // ), - ], - ), - ); - }); - } - - // 设备信息行组件 - // - // 行首图标用 App 自己的图标(原来是蓝色 Icons.headset glyph 套一层浅蓝底)。 - // app_icon.png 自带深色背景,再套浅蓝底会脏,所以底色一并去掉, - // 直接按原来的整体尺寸(6.w 内边距 + 16.sp 字形 ≈ 28.w)出图。 - Widget _buildDeviceInfoRow({ - required String label, - required String value, - required bool isDarkMode, - }) { - return Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(6.r), - child: Image.asset( - 'assets/images/app_icon.png', - width: 28.w, - height: 28.w, - fit: BoxFit.cover, - ), - ), - SizedBox(width: 12.w), - Text( - label, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - fontWeight: FontWeight.w500, - ), - ), - const Spacer(), - Text( - value, - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - ], - ); - } - - // 电池信息组件 - Widget _buildBatteryInfo({ - required String label, - required int batteryLevel, - required bool isCharging, - required bool isDarkMode, - required IconData icon, - }) { - Color batteryColor; - if (batteryLevel > 60) { - batteryColor = Colors.green; - } else if (batteryLevel > 30) { - batteryColor = Colors.orange; - } else { - batteryColor = Colors.red; - } - - return Container( - padding: EdgeInsets.all(10.w), - decoration: BoxDecoration( - color: isDarkMode - ? batteryColor.withValues(alpha: 0.1) - : batteryColor.withValues(alpha: 0.05), - borderRadius: BorderRadius.circular(8.r), - border: Border.all( - color: batteryColor.withValues(alpha: 0.3), - width: 1, - ), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - isCharging ? Icons.battery_charging_full : Icons.battery_std, - size: 16.sp, - color: batteryColor, - ), - SizedBox(width: 4.w), - Text( - label, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - fontWeight: FontWeight.w500, - ), - ), - ], - ), - SizedBox(height: 4.h), - Text( - '${batteryLevel}%', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - color: batteryColor, - ), - ), - if (isCharging) ...[ - SizedBox(height: 1.h), - Text( - 'charging'.tr, // 充电中 - style: TextStyle( - fontSize: 10.sp, - color: batteryColor, - fontWeight: FontWeight.w500, - ), - ), - ], - ], - ), - ); - } - - // 版本信息显示 - Widget _buildVersionInfo(bool isDarkMode) { - return Column( - children: [ - // 当前版本信息 - _buildVersionInfoRow( - label: 'currentFirmwareVersion'.tr, //当前固件版本 - version: controller.currentVersion, - isDarkMode: isDarkMode, - isLatest: false, - ), - SizedBox(height: 8.h), - // 最新版本信息 - _buildVersionInfoRow( - label: 'latestFirmwareVersion'.tr, //最新固件版本 - version: controller.latestVersion, - isDarkMode: isDarkMode, - isLatest: true, - ), - // 版本状态指示器 - SizedBox(height: 14.h), - if (controller.isBleConnected.value) - _buildVersionStatusIndicator(isDarkMode), - ], - ); - } - - // 版本信息行组件 - Widget _buildVersionInfoRow({ - required String label, - required RxString version, - required bool isDarkMode, - required bool isLatest, - }) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.h), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.03) - : Colors.white.withValues(alpha: 0.8), - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.08) - : Colors.grey.withValues(alpha: 0.2), - width: 1, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - fontWeight: FontWeight.w500, - ), - ), - Obx(() => Row( - children: [ - if (isLatest && controller.isUpdateAvailable.value) ...[ - Container( - padding: - EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h), - decoration: BoxDecoration( - color: Colors.orange.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(8.r), - ), - child: Text( - 'NEW', - style: TextStyle( - fontSize: 10.sp, - fontWeight: FontWeight.bold, - color: Colors.orange, - ), - ), - ), - SizedBox(width: 8.w), - ], - Text( - 'V ${version.value}', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode - ? (isLatest ? Colors.green[300] : Colors.blue[300]) - : (isLatest ? Colors.green[600] : Colors.blue[600]), - ), - ), - ], - )), - ], - ), - ); - } - - // 版本状态指示器 - Widget _buildVersionStatusIndicator(bool isDarkMode) { - return Obx(() { - if (controller.isUpdateAvailable.value) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.orange.withValues(alpha: 0.1), - Colors.red.withValues(alpha: 0.1), - ], - ), - borderRadius: BorderRadius.circular(20.r), - border: Border.all( - color: Colors.orange.withValues(alpha: 0.3), - width: 1, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.update, - size: 16.sp, - color: Colors.orange, - ), - SizedBox(width: 6.w), - Text( - 'ota_newVersionFound'.tr, // 发现新版本 - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w500, - color: Colors.orange, - ), - ), - ], - ), - ); - } else { - return Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), - decoration: BoxDecoration( - color: Colors.green.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(20.r), - border: Border.all( - color: Colors.green.withValues(alpha: 0.3), - width: 1, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.check_circle, - size: 16.sp, - color: Colors.green, - ), - SizedBox(width: 6.w), - Text( - 'latestVersion'.tr, // 已是最新版本 - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w500, - color: Colors.green, - ), - ), - ], - ), - ); - } - }); - } - - // 固件更新说明 - Widget _buildUpdateNotes(bool isDarkMode) { - return Obx(() => controller.isUpdateAvailable.value && - controller.updatedescription.value.isNotEmpty - ? Container( - width: double.infinity, - margin: EdgeInsets.only(bottom: 8.h), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [ - Colors.amber[900]!.withValues(alpha: 0.1), - Colors.orange[900]!.withValues(alpha: 0.05), - ] - : [ - Colors.amber[50]!, - Colors.orange[100]!.withValues(alpha: 0.3), - ], - ), - borderRadius: BorderRadius.circular(16.r), - border: Border.all( - color: isDarkMode - ? Colors.amber[700]!.withValues(alpha: 0.3) - : Colors.amber[200]!.withValues(alpha: 0.5), - width: 1, - ), - ), - child: Padding( - padding: EdgeInsets.all(20.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: EdgeInsets.all(8.w), - decoration: BoxDecoration( - color: Colors.amber.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(8.r), - ), - child: Icon( - Icons.new_releases, - color: isDarkMode - ? Colors.amber[300] - : Colors.amber[700], - size: 18.sp, - ), - ), - SizedBox(width: 12.w), - Text( - 'updateContent'.tr, //更新内容 - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode - ? Colors.amber[200] - : Colors.amber[800], - ), - ), - ], - ), - SizedBox(height: 16.h), - Container( - width: double.infinity, - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.03) - : Colors.white.withValues(alpha: 0.7), - borderRadius: BorderRadius.circular(12.r), - ), - child: Text( - controller.updatedescription.value, - style: TextStyle( - fontSize: 14.sp, - height: 1.5, - color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - ), - ), - ), - ], - ), - ), - ) - : const SizedBox.shrink()); - } - - // 加载指示器 - Widget _buildLoadingIndicator(bool isDarkMode) { - return Container( - padding: EdgeInsets.all(32.w), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.03) - : Colors.white.withValues(alpha: 0.8), - borderRadius: BorderRadius.circular(20.r), - border: Border.all( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.08) - : Colors.grey.withValues(alpha: 0.2), - width: 1, - ), - ), - child: Column( - children: [ - // 自定义加载动画 - SizedBox( - width: 50.w, - height: 50.w, - child: CircularProgressIndicator( - strokeWidth: 3, - valueColor: AlwaysStoppedAnimation( - isDarkMode ? const Color(0xFF0066CC) : Colors.blue, - ), - backgroundColor: isDarkMode - ? Colors.white.withValues(alpha: 0.1) - : Colors.grey.withValues(alpha: 0.2), - ), - ), - SizedBox(height: 24.h), - Text( - 'gettingDeviceInfo'.tr, // 正在获取设备信息... - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - ), - ), - SizedBox(height: 8.h), - Text( - 'keepDeviceConnected'.tr, // 请保持设备连接 - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[500] : Colors.grey[500], - ), - ), - ], - ), - ); - } - - // 错误信息显示 - Widget _buildErrorInfo(bool isDarkMode) { - return Container( - padding: EdgeInsets.all(24.w), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [ - Colors.red[900]!.withValues(alpha: 0.15), - Colors.red[800]!.withValues(alpha: 0.1), - ] - : [ - Colors.red[50]!, - Colors.red[100]!.withValues(alpha: 0.3), - ], - ), - borderRadius: BorderRadius.circular(20.r), - border: Border.all( - color: isDarkMode - ? Colors.red[700]!.withValues(alpha: 0.3) - : Colors.red[200]!.withValues(alpha: 0.5), - width: 1, - ), - ), - child: Column( - children: [ - // 错误图标 - Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: Colors.red.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(50.r), - ), - child: Icon( - Icons.error_outline, - color: Colors.red, - size: 32.sp, - ), - ), - SizedBox(height: 16.h), - // 错误标题 - Text( - 'connectionFailed'.tr, // 连接失败 - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.red[300] : Colors.red[700], - ), - ), - SizedBox(height: 8.h), - // 错误信息 - Text( - controller.errorMessage.value, - textAlign: TextAlign.center, - style: TextStyle( - color: isDarkMode ? Colors.red[200] : Colors.red[600], - fontSize: 14.sp, - height: 1.4, - ), - ), - SizedBox(height: 24.h), - // 重试按钮 - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - icon: Icon(Icons.refresh, size: 18.sp), - label: Text( - 'retryConnection'.tr, //重试连接 - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: - isDarkMode ? const Color(0xFF0066CC) : Colors.blue, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(vertical: 14.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), - ), - elevation: 0, - ), - onPressed: controller.retryConnection, - ), - ), - ], - ), - ); - } - - // 更新按钮 - Widget _buildUpdateButton(bool isDarkMode) { - return Obx(() { - if (controller.isUpgrade.value && controller.showDetailedStatus.value) { - return _buildProgressIndicator(isDarkMode); - } - - // 显示详细状态提示 - if (controller.showDetailedStatus.value) { - return _buildDetailedStatusIndicator(isDarkMode); - } - - return SizedBox( - width: double.infinity, - child: Column( - children: [ - // 主要更新按钮 - Container( - width: double.infinity, - height: 56.h, - decoration: BoxDecoration( - gradient: controller.isUpdateAvailable.value - ? LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: isDarkMode - ? [const Color(0xFF0066CC), const Color(0xFF0052A3)] - : [Colors.blue, Colors.blue[700]!], - ) - : null, - color: !controller.isUpdateAvailable.value - ? (isDarkMode ? Colors.grey[800] : Colors.grey[300]) - : null, - borderRadius: BorderRadius.circular(16.r), - boxShadow: controller.isUpdateAvailable.value - ? [ - BoxShadow( - color: Colors.blue.withValues(alpha: 0.3), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ] - : null, - ), - child: ElevatedButton.icon( - icon: Icon( - controller.isUpdateAvailable.value - ? Icons.system_update - : Icons.check_circle, - size: 20.sp, - ), - label: Text( - controller.isUpdateAvailable.value - ? 'ota_updateNow'.tr - : (controller.isBleConnected.value || - DeviceConnection.besConnected) - ? 'latestVersion'.tr // 立即更新 : 已是最新版本 - : 'unconnected'.tr, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - foregroundColor: Colors.white, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), - ), - ), - onPressed: controller.isUpdateAvailable.value - ? controller.startUpdate - : null, - ), - ), - - // 更新提示信息 - if (controller.isUpdateAvailable.value) ...[ - SizedBox(height: 16.h), - Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.orange.withValues(alpha: 0.1) - : Colors.orange[50], - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: Colors.orange.withValues(alpha: 0.3), - width: 1, - ), - ), - child: Row( - children: [ - Icon( - Icons.info_outline, - color: Colors.orange, - size: 20.sp, - ), - SizedBox(width: 12.w), - Expanded( - child: Text( - 'removeEarbudsEnsureBattery'.tr, //取出双耳机,并保证电量均为20%以上的状态 - style: TextStyle( - fontSize: 13.sp, - color: isDarkMode - ? Colors.orange[200] - : Colors.orange[800], - height: 1.3, - ), - ), - ), - ], - ), - ), - ], - ], - ), - ); - }); - } - - // 详细状态指示器 - Widget _buildDetailedStatusIndicator(bool isDarkMode) { - return Obx(() { - return Container( - width: double.infinity, - padding: EdgeInsets.all(24.w), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [ - const Color(0xFF1A1A1A).withValues(alpha: 0.9), - const Color(0xFF2A2A2A).withValues(alpha: 0.9), - ] - : [ - const Color(0xFFF8FAFF), - const Color(0xFFE3F2FD), - ], - ), - borderRadius: BorderRadius.circular(20.r), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withValues(alpha: 0.3) - : Colors.black.withValues(alpha: 0.08), - blurRadius: 20, - offset: const Offset(0, 8), - ), - ], - ), - child: Column( - children: [ - // 状态图标和文字 - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: EdgeInsets.all(8.w), - decoration: BoxDecoration( - color: Colors.blue.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8.r), - ), - child: SizedBox( - width: 20.sp, - height: 20.sp, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - isDarkMode ? const Color(0xFF0066CC) : Colors.blue, - ), - ), - ), - ), - SizedBox(width: 12.w), - Expanded( - child: Text( - controller.detailedStatus.value, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.blue[300] : Colors.blue[700], - ), - ), - ), - ], - ), - SizedBox(height: 16.h), - - // 提示信息 - Text( - 'keepConnectedDontClose'.tr, // 请保持设备连接,不要关闭应用 - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - ), - ], - ), - ); - }); - } - - // 进度指示器 - Widget _buildProgressIndicator(bool isDarkMode) { - return Obx(() { - final progress = controller.progress.value; - final type = controller.type.value; - final statusMessage = controller.getStatusMessage(type); - - return Container( - width: double.infinity, - padding: EdgeInsets.all(24.w), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: isDarkMode - ? [ - const Color(0xFF1A1A1A).withValues(alpha: 0.9), - const Color(0xFF2A2A2A).withValues(alpha: 0.9), - ] - : [ - const Color(0xFFF8FAFF), - const Color(0xFFE3F2FD), - ], - ), - borderRadius: BorderRadius.circular(20.r), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withValues(alpha: 0.3) - : Colors.black.withValues(alpha: 0.08), - blurRadius: 20, - offset: const Offset(0, 8), - ), - ], - ), - child: Column( - children: [ - // 状态图标和文字 - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: EdgeInsets.all(8.w), - decoration: BoxDecoration( - color: Colors.blue.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8.r), - ), - child: Icon( - Icons.system_update, - color: isDarkMode ? const Color(0xFF0066CC) : Colors.blue, - size: 20.sp, - ), - ), - SizedBox(width: 12.w), - Text( - statusMessage, - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.blue[300] : Colors.blue[700], - ), - ), - ], - ), - SizedBox(height: 24.h), - - // 进度条容器 - Container( - height: 8.h, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withValues(alpha: 0.1) - : Colors.grey[200], - borderRadius: BorderRadius.circular(4.r), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(4.r), - child: LinearProgressIndicator( - value: progress / 100.0, - backgroundColor: Colors.transparent, - valueColor: AlwaysStoppedAnimation( - isDarkMode ? const Color(0xFF0066CC) : Colors.blue, - ), - ), - ), - ), - SizedBox(height: 16.h), - - // 进度百分比和详细信息 - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${progress.toStringAsFixed(1)}%', - style: TextStyle( - fontSize: 24.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.blue[300] : Colors.blue[700], - ), - ), - Text( - 'doNotDisconnect'.tr, // 请勿断开连接 - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - ), - ], - ), - ], - ), - ); - }); - } -} diff --git a/apps/client/lib/modules/qqmusic/bindings/qqmusic_playlist_binding.dart b/apps/client/lib/modules/qqmusic/bindings/qqmusic_playlist_binding.dart deleted file mode 100644 index 04c53d81..00000000 --- a/apps/client/lib/modules/qqmusic/bindings/qqmusic_playlist_binding.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/qqmusic_playlist_controller.dart'; - -class QQMusicPlaylistBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => QQMusicPlaylistController()); - } -} diff --git a/apps/client/lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart b/apps/client/lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart deleted file mode 100644 index 7db886a1..00000000 --- a/apps/client/lib/modules/qqmusic/controllers/qqmusic_playlist_controller.dart +++ /dev/null @@ -1,237 +0,0 @@ -import 'package:flutter/scheduler.dart'; -import 'package:flutter/widgets.dart'; -import 'package:get/get.dart'; -import '../../../data/models/song_model.dart'; -import '../../../data/services/qq_music.dart'; -import '../../../core/utils/logger.dart'; - -class QQMusicPlaylistController extends GetxController { - final QqMusicService _qqMusicService = QqMusicService.to; - - // 登录相关 - final isLoggedIn = false.obs; - final userInfo = Rxn>(); - - // 界面维护的播放列表状态 - RxList playlist = [].obs; - RxInt currentIndex = 0.obs; - - // 界面显示的当前歌曲(可能与实际播放的不同) - Rxn displayCurrentSong = Rxn(); - - // 界面播放状态(当前播放歌曲不在列表中时为未播放状态) - RxBool displayIsPlaying = false.obs; - - // 从全局服务获取实际播放状态(仅用于同步) - RxBool get actualIsPlaying => _qqMusicService.isPlaying; - RxInt get currentPosition => _qqMusicService.currentPosition; - RxInt get totalDuration => _qqMusicService.totalDuration; - Rxn get actualCurrentSong => _qqMusicService.currentSong; - - @override - void onInit() { - super.onInit(); - _initializePlaylist(); - print('🎵 [QQMusicPlaylistController] 控制器初始化完成'); - } - - /// 初始化播放列表 - void _initializePlaylist() { - // 从路由参数获取播放列表 - final arguments = Get.arguments; - if (arguments != null && arguments['playlist'] != null) { - final List songList = arguments['playlist']; - // 更新播放列表 - playlist.assignAll(songList); - // 检查当前播放歌曲是否在列表中 - _syncDisplayStateWithActualPlayback(); - } else { - Logger.w('QQMusicPlaylist', '未接收到播放列表参数'); - _setDefaultDisplayState(); - } - // 界面初始化时同步播放状态和进度 - _qqMusicService.syncCurrentPlayInfo(); - } - - /// 同步显示状态与实际播放状态 - void _syncDisplayStateWithActualPlayback() { - if (playlist.isEmpty) { - _setDefaultDisplayState(); - return; - } - - final actualSong = actualCurrentSong.value; - - if (actualSong != null) { - // 查找当前播放歌曲在列表中的位置 - final index = playlist.indexWhere((song) => song.mid == actualSong.mid); - - if (index != -1) { - // 当前播放歌曲在列表中 - currentIndex.value = index; - displayCurrentSong.value = playlist[index]; - displayIsPlaying.value = actualIsPlaying.value; - Logger.d('QQMusicPlaylist', '当前播放歌曲在列表中,索引: $index'); - } else { - // 当前播放歌曲不在列表中,显示列表第一个,状态为未播放 - _setDefaultDisplayState(); - Logger.d('QQMusicPlaylist', '当前播放歌曲不在列表中,显示默认状态'); - } - } else { - // 没有当前播放歌曲,显示列表第一个,状态为未播放 - _setDefaultDisplayState(); - Logger.d('QQMusicPlaylist', '无当前播放歌曲,显示默认状态'); - } - } - - /// 设置默认显示状态 - void _setDefaultDisplayState() { - if (playlist.isNotEmpty) { - currentIndex.value = 0; - displayCurrentSong.value = playlist.first; - displayIsPlaying.value = false; // 默认为未播放状态 - Logger.d('QQMusicPlaylist', '设置默认显示状态: ${playlist.first.name}'); - } else { - currentIndex.value = 0; - displayCurrentSong.value = null; - displayIsPlaying.value = false; - Logger.w('QQMusicPlaylist', '播放列表为空,无法设置默认状态'); - } - } - - /// 音乐状态变化回调 - void _onMusicStateChanged() { - SchedulerBinding.instance.addPostFrameCallback((_) { - if (!isClosed) { - // 重新同步显示状态 - _syncDisplayStateWithActualPlayback(); - update(); - } - }); - } - - @override - void onClose() { - // 移除状态监听器 - super.onClose(); - } - - /// 播放歌曲 - Future playSong(SongModel song, {List? songList}) async { - try { - displayIsPlaying.value = true; - // 使用传入的歌曲列表或当前播放列表 - final List finalSongList = songList ?? playlist.toList(); - final songMids = finalSongList.map((s) => s.mid).toList(); - final playIndex = finalSongList.indexOf(song); - - // 歌曲在当前列表中,更新界面状态 - currentIndex.value = playIndex; - displayCurrentSong.value = song; - final result = await _qqMusicService.playSongs( - songMids: songMids, - index: playIndex, - songList: finalSongList, - currentSongModel: song, - ); - - if (result['success'] == true) { - displayIsPlaying.value = true; - // _qqMusicService.syncCurrentPlayInfo(); - // Get.snackbar('开始播放', song.name); - } else { - displayIsPlaying.value = false; - // Get.snackbar('播放失败', result['error'] ?? '未知错误'); - } - } catch (e) { - displayIsPlaying.value = false; - // Get.snackbar('播放失败', e.toString()); - } - } - - /// 播放列表中的指定歌曲 - Future playPlaylistSong(int index) async { - if (index >= 0 && index < playlist.length) { - await playSong(playlist[index]); - } - } - - /// 暂停/继续播放 - Future togglePlayPause() async { - // 检查当前显示的歌曲是否正在播放 - final displaySong = displayCurrentSong.value; - final actualSong = actualCurrentSong.value; - - if (displaySong != null && - actualSong != null && - displaySong.mid == actualSong.mid) { - // 显示的歌曲正在播放,执行暂停/继续 - if (actualIsPlaying.value) { - await _qqMusicService.pauseMusic(); - displayIsPlaying.value = false; - } else { - await _qqMusicService.resumeMusic(); - displayIsPlaying.value = true; - } - } else { - // 显示的歌曲不是当前播放的,播放显示的歌曲 - if (displaySong != null) { - await playSong(displaySong); - } - } - } - - /// 下一首(列表内) - Future nextSong() async { - if (playlist.isNotEmpty) { - final nextIndex = (currentIndex.value + 1) % playlist.length; - await playPlaylistSong(nextIndex); - } - } - - /// 上一首(列表内) - Future previousSong() async { - if (playlist.isNotEmpty) { - final prevIndex = - currentIndex.value > 0 ? currentIndex.value - 1 : playlist.length - 1; - await playPlaylistSong(prevIndex); - } - } - - /// 停止播放 - Future stopMusic() async { - await _qqMusicService.stopMusic(); - displayIsPlaying.value = false; - } - - /// 格式化时间 - String formatDuration(int milliseconds) { - return _qqMusicService.formatDuration(milliseconds); - } - - /// 获取当前显示的播放状态(用于UI显示) - bool get isCurrentSongPlaying { - final displaySong = displayCurrentSong.value; - final actualSong = actualCurrentSong.value; - - // 只有当显示的歌曲与实际播放的歌曲相同时,才显示播放状态 - if (displaySong != null && - actualSong != null && - displaySong.mid == actualSong.mid) { - return actualIsPlaying.value; - } - return false; - } - - /// 检查歌曲是否在当前播放列表中 - bool isSongInPlaylist(SongModel song) { - return playlist.any((s) => s.mid == song.mid); - } - - /// 更新播放列表(支持多个播放列表场景) - void updatePlaylist(List newPlaylist) { - playlist.assignAll(newPlaylist); - _syncDisplayStateWithActualPlayback(); - Logger.d('QQMusicPlaylist', '播放列表已更新,歌曲数量: ${newPlaylist.length}'); - } -} diff --git a/apps/client/lib/modules/qqmusic/views/qqmusic_playlist_view.dart b/apps/client/lib/modules/qqmusic/views/qqmusic_playlist_view.dart deleted file mode 100644 index 799dac92..00000000 --- a/apps/client/lib/modules/qqmusic/views/qqmusic_playlist_view.dart +++ /dev/null @@ -1,651 +0,0 @@ -import 'package:get/get.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '/modules/agent/widgets/circular_progress_button.dart'; -import '/modules/qqmusic/controllers/qqmusic_playlist_controller.dart'; -import '/data/models/song_model.dart'; // Use the data model instead -import '../../../core/utils/logger.dart'; - -class QqmusicPlaylistView extends GetView { - const QqmusicPlaylistView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - ScreenUtil.init(context, designSize: const Size(375, 812)); - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios, size: 20), - onPressed: () => Navigator.pop(context), - ), - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, - foregroundColor: isDarkMode ? Colors.white : Colors.black, - elevation: 0, - ), - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - body: controller.playlist.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.music_off, - size: 64.w, - color: Colors.grey[400], - ), - SizedBox(height: 16.h), - Text( - 'noMusic'.tr, - style: TextStyle( - fontSize: 16.sp, - color: Colors.grey[600], - ), - ), - ], - ), - ) - : Column( - children: [ - // 上部:当前播放音乐详情 - Container( - height: MediaQuery.of(context).size.height * 0.38, - child: _buildCurrentPlayingSection(isDarkMode), - ), - // 中部:播放全部按钮 - _buildPlayAllSection(isDarkMode), - // 下部:歌单列表 - Expanded( - child: _buildPlaylistSection(isDarkMode), - ), - ], - ), - // 添加迷你播放器 - // bottomNavigationBar: _buildMiniPlayer(isDarkMode), - ); - } - - // 构建当前播放音乐详情部分 - Widget _buildCurrentPlayingSection(bool isDarkMode) { - return SingleChildScrollView( - child: Obx(() { - // 使用 displayCurrentSong 而不是 actualCurrentSong - final displayCurrentSong = controller.displayCurrentSong.value; - if (displayCurrentSong == null) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.music_note, - size: 110.w, - color: Colors.grey[400], - ), - SizedBox(height: 12.h), - Text( - 'noMusicPlaying'.tr, - style: TextStyle( - fontSize: 16.sp, - color: Colors.grey[600], - ), - ), - ], - ), - ); - } - - return Container( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 封面图片 - 使用 displayCurrentSong - _buildCoverImage(displayCurrentSong.albumPic ?? '', isDarkMode, - size: 110.w), - SizedBox(height: 12.h), - // 歌曲信息 - 使用 displayCurrentSong - _buildSongInfo(displayCurrentSong.name, displayCurrentSong.artist, - isDarkMode), - SizedBox(height: 16.h), - // 进度条 - _buildProgressBar(isDarkMode), - SizedBox(height: 15.h), - // 播放控制按钮 - _buildPlayControls(isDarkMode), - ], - ), - ); - }), - ); - } - - // 构建播放全部按钮部分 - Widget _buildPlayAllSection(bool isDarkMode) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.h), - child: SizedBox( - width: double.infinity, - height: 48.h, - child: ElevatedButton.icon( - onPressed: () async { - if (controller.playlist.isNotEmpty) {} - }, - icon: Icon(Icons.play_arrow, color: Colors.white, size: 20.w), - label: Text( - 'playAll'.tr, // 播放全部 - style: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(Get.context!).primaryColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14.r), - ), - elevation: 3, - shadowColor: Theme.of(Get.context!).primaryColor.withAlpha(102), - ), - ), - ), - ); - } - - // 构建歌单列表部分 - Widget _buildPlaylistSection(bool isDarkMode) { - return GetBuilder( - builder: (controller) { - return ListView.builder( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h), - itemCount: controller.playlist.length, - itemBuilder: (context, index) { - final song = controller.playlist[index]; - return _buildMusicItem( - context, controller.playlist, song, index, isDarkMode); - }, - ); - }, - ); - } - - Widget _buildMusicItem( - BuildContext context, - List songs, - SongModel song, - int index, - bool isDarkMode, - ) { - return GestureDetector( - onTap: () async { - await controller.playSong(song, songList: songs); - }, - child: Container( - margin: EdgeInsets.only(bottom: 10.h), - padding: EdgeInsets.all(14.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.white, - borderRadius: BorderRadius.circular(14.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(isDarkMode ? 0.25 : 0.05), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - children: [ - // 封面图片 - Container( - width: 52.w, - height: 52.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.r), - color: isDarkMode ? Colors.grey[700] : Colors.grey[200], - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(isDarkMode ? 0.2 : 0.08), - blurRadius: 2, - offset: const Offset(0, 1), - ), - ], - ), - child: Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(10.r), - child: Image.network( - song.albumPic ?? '', - width: 52.w, - height: 52.w, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Icon( - Icons.music_note, - color: isDarkMode - ? Colors.grey[500] - : Colors.grey[600], - size: 24.w, - ); - }, - ), - ), - // 右下角音乐图标 - Positioned( - right: 0.w, - bottom: 0.w, - child: Container( - width: 14.w, - height: 14.w, - child: Image.asset( - 'assets/images/qqmusic.png', - fit: BoxFit.contain, - ), - ), - ), - ], - )), - SizedBox(width: 14.w), - // 歌曲信息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - song.name, - style: TextStyle( - fontSize: 15.5.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black87, - height: 1.2, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 3.h), - Text( - song.artist, - style: TextStyle( - fontSize: 13.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - height: 1.1, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - SizedBox(width: 14.w), - // 播放按钮 - Obx(() { - // 使用 actualCurrentSong 的 mid 来判断是否是当前播放的歌曲 - final isThactualIsPlaying = controller.actualIsPlaying.value && - controller.actualCurrentSong.value?.mid == song.mid; - - final currentProgress = isThactualIsPlaying - ? controller.totalDuration.value > 0 - ? controller.currentPosition.value / - controller.totalDuration.value - : 0.0 - : 0.0; - return CircularProgressButton( - isPlaying: isThactualIsPlaying, - progress: currentProgress, - primaryColor: Theme.of(context).primaryColor, - backgroundColor: Colors.grey, - size: 40.w, - strokeWidth: 2.5, - onPlayPause: () async { - try { - Logger.d('MusicPlaylistView', - '播放按钮被点击,当前播放状态: ${controller.actualIsPlaying.value}'); - Logger.d('MusicPlaylistView', - '当前歌曲ID: ${controller.actualCurrentSong.value?.mid}, 点击歌曲ID: ${song.mid}'); - if (controller.actualIsPlaying.value) { - if (controller.actualCurrentSong.value?.mid == song.mid) { - Logger.d('MusicPlaylistView', '暂停当前播放的歌曲'); - await controller.togglePlayPause(); - } else { - Logger.d('MusicPlaylistView', '播放新歌曲'); - await controller.playSong(song, songList: songs); - } - } else { - Logger.d('MusicPlaylistView', '开始播放歌曲'); - await controller.playSong(song, songList: songs); - } - } catch (e) { - Logger.e('MusicPlaylistView', '播放音乐时发生错误: $e'); - } - }, - onSeek: isThactualIsPlaying ? (seekProgress) {} : null, - ); - }), - ], - ), - ), - ); - } - - // 构建封面图片 - Widget _buildCoverImage(String coverUrl, bool isDarkMode, - {required double size}) { - return Container( - width: size, - height: size, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(isDarkMode ? 0.3 : 0.1), - blurRadius: 15, - spreadRadius: 2, - offset: const Offset(0, 6), - ), - ], - ), - child: Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(16.r), - child: coverUrl.isNotEmpty - ? Image.network( - coverUrl, - fit: BoxFit.cover, - width: size, - height: size, - errorBuilder: (context, error, stackTrace) { - return _buildDefaultCover(isDarkMode, size: size); - }, - ) - : _buildDefaultCover(isDarkMode, size: size), - ), - // 右下角音乐图标 - Positioned( - right: 0.w, - bottom: 0.w, - child: Container( - width: size * 0.3, - height: size * 0.3, - child: ClipRRect( - borderRadius: BorderRadius.circular(4.r), - child: Image.asset( - 'assets/images/qqmusic.png', - fit: BoxFit.contain, - ), - ), - ), - ), - ], - ), - ); - } - - // 构建默认封面 - Widget _buildDefaultCover(bool isDarkMode, {required double size}) { - return Container( - color: isDarkMode ? Colors.grey[800] : Colors.grey[300], - child: Icon( - Icons.music_note, - size: size * 0.55, - color: isDarkMode ? Colors.grey[600] : Colors.grey[500], - ), - ); - } - - // 构建歌曲信息 - Widget _buildSongInfo(String title, String artist, bool isDarkMode) { - return Column( - children: [ - Text( - title, - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black87, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 6.h), - Text( - artist, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ); - } - - // 构建进度条 - 参考qqmusic_test的简洁设计 - Widget _buildProgressBar(bool isDarkMode) { - return Obx(() { - // 计算进度值,确保在0.0-1.0范围内 - double progressValue = 0.0; - if (controller.totalDuration.value > 0) { - progressValue = - (controller.currentPosition.value / controller.totalDuration.value) - .clamp(0.0, 1.0); - } - - return Column( - children: [ - // 时间显示 - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - _formatDuration(controller.currentPosition.value), - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - ), - Text( - _formatDuration(controller.totalDuration.value), - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - ), - ], - ), - SizedBox(height: 8.h), - // 进度条 - 使用LinearProgressIndicator,更简洁 - LinearProgressIndicator( - value: progressValue, - backgroundColor: isDarkMode ? Colors.grey[700] : Colors.grey[300], - valueColor: AlwaysStoppedAnimation( - Theme.of(Get.context!).primaryColor), - minHeight: 4.h, - ), - ], - ); - }); - } - - // 构建迷你播放器 - 参考qqmusic_test的设计 - Widget _buildMiniPlayer(bool isDarkMode) { - return Obx(() { - if (controller.actualCurrentSong.value == null) { - return const SizedBox.shrink(); - } - - final song = controller.actualCurrentSong.value!; - return Container( - height: 80.h, - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, -2), - ), - ], - ), - child: Column( - children: [ - // 进度条 - LinearProgressIndicator( - value: controller.totalDuration.value > 0 - ? (controller.currentPosition.value / - controller.totalDuration.value) - .clamp(0.0, 1.0) - : 0, - backgroundColor: Colors.grey[300], - valueColor: AlwaysStoppedAnimation( - Theme.of(Get.context!).primaryColor), - ), - Expanded( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w), - child: Row( - children: [ - // 歌曲信息 - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - song.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - Text( - song.artist, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: isDarkMode - ? Colors.grey[400] - : Colors.grey[600], - fontSize: 12.sp, - ), - ), - ], - ), - ), - // 控制按钮 - Row( - children: [ - IconButton( - icon: const Icon(Icons.skip_previous), - onPressed: controller.previousSong, - color: isDarkMode ? Colors.white : Colors.black, - ), - IconButton( - icon: Icon( - controller.actualIsPlaying.value - ? Icons.pause - : Icons.play_arrow, - ), - onPressed: controller.togglePlayPause, - color: Theme.of(Get.context!).primaryColor, - ), - IconButton( - icon: const Icon(Icons.skip_next), - onPressed: controller.nextSong, - color: isDarkMode ? Colors.white : Colors.black, - ), - ], - ), - ], - ), - ), - ), - ], - ), - ); - }); - } - - // 构建播放控制按钮 - Widget _buildPlayControls(bool isDarkMode) { - return Obx(() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - onPressed: () async { - await controller.previousSong(); - }, - iconSize: 32.w, - padding: EdgeInsets.all(8.w), // 增大触摸区域 - icon: Icon( - Icons.skip_previous, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - SizedBox(width: 16.w), - Container( - width: 56.w, - height: 56.w, - decoration: BoxDecoration( - color: Theme.of(Get.context!).primaryColor, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Theme.of(Get.context!).primaryColor.withOpacity(0.3), - blurRadius: 10, - spreadRadius: 1, - offset: const Offset(0, 3), - ), - ], - ), - child: IconButton( - onPressed: () async { - await controller.togglePlayPause(); - }, - padding: EdgeInsets.zero, - icon: Icon( - controller.actualIsPlaying.value - ? Icons.pause - : Icons.play_arrow, - size: 28.w, - color: Colors.white, - ), - ), - ), - SizedBox(width: 16.w), - IconButton( - onPressed: () async { - await controller.nextSong(); - }, - iconSize: 32.w, - padding: EdgeInsets.all(8.w), // 增大触摸区域 - icon: Icon( - Icons.skip_next, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ], - ); - }); - } - - // ...(其他辅助方法保持不变,如_formatDuration, _togglePlayPause等) - // 格式化时间显示 - String _formatDuration(int milliseconds) { - final duration = Duration(milliseconds: milliseconds); - final minutes = duration.inMinutes; - final seconds = duration.inSeconds % 60; - return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; - } -} diff --git a/apps/client/lib/modules/realtime/README.md b/apps/client/lib/modules/realtime/README.md deleted file mode 100644 index c41004a5..00000000 --- a/apps/client/lib/modules/realtime/README.md +++ /dev/null @@ -1,160 +0,0 @@ -# Realtime 实时语音聊天模块 - -## ⚠️ 当前状态 - -**此模块为功能预览版本,提供完整的实时语音对话功能。** - -通过原生插件实现语音识别、AI对话和语音合成的完整语音交互体验。 - -## 概述 - -Realtime模块实现了类似ChatGPT app的实时语音对话功能,当前版本提供: - -- ✅ **完整语音交互** - 实时语音识别、AI对话和语音合成 -- ✅ **状态管理** - 聆听、思考、回答状态的可视化 -- ✅ **动画效果** - 基于Shader的流动云效果,支持音频振幅驱动 -- ✅ **字幕显示** - 可选择开启/关闭的字幕模式 -- ✅ **智能UI** - 根据连接状态自动调整按钮可用性 - -## 界面设计 - -### 布局结构 -- **顶部状态区域** - 显示当前状态和字幕模式切换按钮 -- **中间可视化区域** - 圆形Shader动画,根据音频振幅呈现呼吸效果 -- **底部控制区域** - 主控制按钮和状态指示器 - -### 状态指示 -- 🔵 **聆听状态** - 蓝色动画,接收用户语音输入 -- 🟠 **思考状态** - 橙色动画,AI处理中 -- 🟢 **回答状态** - 绿色动画,AI语音输出 -- ⚪ **空闲状态** - 灰色静态,等待交互 - -### 字幕模式 -- **默认关闭** - 初始状态下不显示任何字幕内容 -- **手动切换** - 通过右上角按钮手动开启/关闭字幕 -- **完整隐藏** - 关闭时字幕区域完全不显示,不占用界面空间 -- **智能布局** - 开启时圆形动画自动上移,字幕区域占据下半屏 - -### 交互控制 -- **录音按钮** - 仅在成功连接AI服务器且开始录音后可用 -- **连接状态** - 未连接时录音按钮显示不可用状态 -- **状态反馈** - 按钮颜色和可点击性根据连接状态动态调整 - -## 使用方法 - -### 路由导航 - -```dart -// 跳转到实时语音对话页面 -Get.toNamed(Routes.realtime); -``` - -### 交互流程 - -1. **自动连接** - 页面加载后自动尝试连接AI服务器 -2. **等待连接** - 录音按钮在连接成功前保持不可用状态 -3. **开始录音** - 连接成功后自动开始录音,录音按钮变为可用 -4. **语音对话** - 用户说话 → AI处理 → 语音回复 -5. **字幕控制** - 可随时通过右上角按钮切换字幕显示 - -## 技术架构 - -### 控制器 (RealtimeController) -- **连接管理**: WebSocket连接到AI服务器 -- **状态同步**: `isConnected`, `isListening`, `isSpeaking` -- **字幕控制**: `isSubtitleMode` 默认为false,用户手动控制 -- **消息管理**: 智能去重,避免流式响应重复显示 -- **音频处理**: 原生层计算RMS值,Flutter层接收并控制动画 - -### 视图组件 -- **RealtimeView** - 主视图容器,响应式布局 -- **ShaderMicFlow** - 基于GLSL Shader的流动云动画组件 -- **AutoScrollList** - 自动滚动的字幕列表 - -### 原生集成 -- **RealtimeService** - 原生音频处理和WebSocket通信 -- **音频格式** - 16kHz, 1声道, 16位PCM -- **实时传输** - 低延迟音频流处理 -- **RMS计算** - 原生层实时计算音频振幅 - -## 默认行为 - -### 字幕模式 -- **初始状态**: 关闭 (`isSubtitleMode.value = false`) -- **显示逻辑**: 仅在用户手动开启时显示字幕内容 -- **空间占用**: 关闭时字幕区域高度为0,不影响其他UI元素 -- **切换效果**: 平滑动画过渡,圆形动画位置和大小同步调整 - -### 按钮状态 -- **录音按钮**: 默认不可用,连接成功且开始录音后才可点击 -- **状态指示**: 通过颜色和图标变化反映当前可用性 -- **交互反馈**: 不可用时点击无效果,避免误操作 - -## 开发计划 - -### 已完成功能 ✅ -- [x] 完整语音交互流程 -- [x] 实时WebSocket通信 -- [x] Shader流动云动画 -- [x] RMS音频振幅计算 -- [x] 智能UI状态管理 -- [x] 字幕模式控制 -- [x] 消息去重优化 - -### 计划增强 ⏳ -- [ ] 多语言语音识别 -- [ ] 语音中断检测 -- [ ] 音频质量自适应 -- [ ] 离线降级处理 -- [ ] 语音情感识别 - -## 注意事项 - -1. **网络依赖** - 需要连接到AI服务器 (ws://192.168.1.11:8000/ws) -2. **麦克风权限** - 需要获取麦克风使用权限 -3. **音频会话** - 自动管理音频会话,支持蓝牙设备 -4. **资源管理** - 页面关闭时自动清理音频资源和网络连接 - -## 自定义配置 - -### 修改服务器地址 - -```dart -// 在RealtimeController中修改连接URL -await _realtimeService.initialize( - serverUrl: 'ws://your-server:port/ws', - // ... 其他参数 -); -``` - -### 调整UI布局 - -```dart -// 修改字幕模式下圆形动画位置 -alignment: controller.isSubtitleMode.value - ? const Alignment(0, -0.9) // 调整Y轴位置 - : const Alignment(0, -0.25), -``` - -### 音频参数调优 - -```dart -// 在初始化时调整音频参数 -sampleRate: 16000, // 采样率 -channels: 1, // 声道数 -bitsPerSample: 16, // 位深度 -``` - -## 技术栈 - -- **Flutter SDK**: >= 3.0.0 -- **GetX**: >= 4.6.5 (状态管理) -- **Flutter Shaders**: >= 0.0.6 (Shader动画) -- **原生插件**: Realtime服务 (音频处理) -- **WebSocket**: 实时通信协议 - -## 版本信息 - -- **当前版本**: v1.0.0 (Production) -- **最后更新**: 2024年 -- **状态**: 完整功能实现 \ No newline at end of file diff --git a/apps/client/lib/modules/realtime/bindings/realtime_binding.dart b/apps/client/lib/modules/realtime/bindings/realtime_binding.dart deleted file mode 100644 index a9b6b65b..00000000 --- a/apps/client/lib/modules/realtime/bindings/realtime_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/realtime_controller.dart'; - -class RealtimeBinding extends Bindings { - @override - void dependencies() { - // 注册实时语音聊天控制器 - Get.lazyPut(() => RealtimeController()); - } -} \ No newline at end of file diff --git a/apps/client/lib/modules/realtime/controllers/realtime_controller.dart b/apps/client/lib/modules/realtime/controllers/realtime_controller.dart deleted file mode 100644 index a68b7989..00000000 --- a/apps/client/lib/modules/realtime/controllers/realtime_controller.dart +++ /dev/null @@ -1,406 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import 'package:realtime/realtime.dart'; - -import '../../../core/utils/logger.dart'; - -class RealtimeController extends GetxController with GetTickerProviderStateMixin { - static const int _sampleRate = 16000; - - // Realtime服务实例 - final RealtimeService _realtimeService = RealtimeService(); - - // 音频振幅值(RMS) - final RxDouble audioAmplitude = 0.0.obs; - - // 连接状态 - final RxBool isConnected = false.obs; - final RxBool isConnecting = false.obs; - - // 语音状态 - final RxBool isListening = false.obs; - final RxBool isSpeaking = false.obs; - - // UI状态 - final RxString currentStatus = ''.obs; // 移除默认文字 - final RxString userSpeechText = ''.obs; - final RxBool isUserSpeechFinal = true.obs; - final RxList aiResponseMessages = [].obs; - final RxBool isSubtitleMode = true.obs; - - // 字幕显示用的最新消息 - final RxString latestUserMessage = ''.obs; - final RxString latestAiMessage = ''.obs; - - // 实时字幕更新用的临时消息 - final RxString partialUserMessage = ''.obs; - final RxString streamingAiMessage = ''.obs; - - // 状态监听 - StreamSubscription? _eventSubscription; - - - @override - void onInit() { - super.onInit(); - _setupEventListener(); - - // 自动开始连接 - WidgetsBinding.instance.addPostFrameCallback((_) { - _autoConnect(); - }); - } - - @override - void onClose() { - _cleanup(); - super.onClose(); - } - - /// 设置事件监听器 - void _setupEventListener() { - _eventSubscription = _realtimeService.eventStream.listen( - (event) { - _handleRealtimeEvent(event); - }, - onError: (error) { - Logger.error('Realtime事件流错误: $error'); - }, - ); - } - - /// 处理Realtime事件 - void _handleRealtimeEvent(RealtimeEvent event) { - switch (event.type) { - case 'connectionStatusChanged': - _handleConnectionStatusChanged(event.data); - break; - case 'voiceStatusChanged': - _handleVoiceStatusChanged(event.data); - break; - case 'textReceived': - _handleTextReceived(event.data); - break; - case 'audioRms': - _handleAudioRms(event.data); - break; - case 'error': - _handleError(event.data); - break; - } - } - - /// 自动连接 - Future _autoConnect() async { - currentStatus.value = ''; // 不显示任何文字 - await connect(); - } - - /// 处理连接状态变化 - void _handleConnectionStatusChanged(String status) { - switch (status) { - case 'connected': - isConnected.value = true; - isConnecting.value = false; - currentStatus.value = ''; // 不显示任何文字 - // 连接成功后自动开始录音 - Timer(const Duration(milliseconds: 500), () { - startListening(); - }); - break; - case 'connecting': - isConnecting.value = true; - currentStatus.value = ''; // 不显示任何文字 - break; - case 'disconnected': - isConnected.value = false; - isConnecting.value = false; - currentStatus.value = ''; // 不显示任何文字 - _resetState(); - break; - case 'error': - isConnected.value = false; - isConnecting.value = false; - currentStatus.value = '连接失败,请检查网络'; // 保留错误提示 - _resetState(); - Logger.error('Vocode服务器连接错误'); - break; - } - } - - /// 处理语音状态变化 - void _handleVoiceStatusChanged(String status) { - switch (status) { - case 'recording': - isListening.value = true; - isSpeaking.value = false; - currentStatus.value = ''; // 不显示任何文字 - break; - case 'processing': - isListening.value = false; - currentStatus.value = ''; // 不显示任何文字 - break; - case 'playing': - isListening.value = false; - isSpeaking.value = true; - currentStatus.value = ''; // 不显示任何文字 - break; - case 'idle': - isListening.value = false; - isSpeaking.value = false; - currentStatus.value = ''; // 不显示任何文字 - // 空闲时音频振幅归零 - audioAmplitude.value = 0.0; - break; - } - } - - /// 处理收到的文本消息 - 标准Vocode WebSocket协议 - void _handleTextReceived(dynamic payload) { - if (payload is String) { - try { - // 解析JSON消息 - final Map json = jsonDecode(payload); - final String messageType = json['type'] ?? ''; - - switch (messageType) { - case 'websocket_transcript': - // 处理统一的文本消息格式 - final String text = json['text'] ?? ''; - final String sender = json['sender'] ?? ''; - final bool isFinal = json['is_final'] ?? false; - // final dynamic timestamp = json['timestamp']; // 保留供未来使用 - - if (sender.toLowerCase() == 'human') { - // 用户语音识别消息 - if (isFinal) { - // ASR最终结果 - if (text.isNotEmpty) { - latestUserMessage.value = text; - userSpeechText.value = text; - isUserSpeechFinal.value = true; - } - // 清空部分消息 - partialUserMessage.value = ''; - } else { - // ASR实时识别 - if (text.isNotEmpty) { - partialUserMessage.value = text; - isUserSpeechFinal.value = false; - } - } - } else if (sender.toLowerCase() == 'bot') { - // AI回复消息 - if (isFinal) { - // LLM回复完成标识 (text为空) - if (streamingAiMessage.value.isNotEmpty) { - // 将流式消息确认为最终消息 - latestAiMessage.value = streamingAiMessage.value; - if (aiResponseMessages.isNotEmpty) { - aiResponseMessages[aiResponseMessages.length - 1] = streamingAiMessage.value; - } else { - aiResponseMessages.add(streamingAiMessage.value); - } - streamingAiMessage.value = ''; - } - } else { - // LLM流式回复 - if (text.isNotEmpty) { - streamingAiMessage.value += text; - } - } - } - break; - - case 'websocket_ready': - // 连接就绪 - break; - - case 'websocket_stop': - // 会话结束 - break; - - case 'websocket_start': - // 会话开始 - break; - - default: - Logger.error('收到未知消息类型: "$messageType"'); - } - } catch (e) { - Logger.error('解析WebSocket消息失败: $e'); - } - } else { - Logger.error('收到非字符串类型的payload: ${payload.runtimeType}'); - } - } - - /// 处理音频RMS值 - void _handleAudioRms(dynamic rms) { - if (rms is double || rms is num) { - // 平滑处理RMS值,避免抖动太大,增大幅度 - final double rawValue = rms.toDouble().clamp(0.0, 1.0); - // 增强幅度:平方根函数减少小值,对数函数增强大值 - final double enhancedValue = (rawValue * 3.0).clamp(0.0, 1.0); - // 更强的平滑处理,减少抖动 - audioAmplitude.value = audioAmplitude.value * 0.85 + enhancedValue * 0.15; - } - } - - /// 处理错误 - void _handleError(String error) { - Logger.error('Realtime错误: $error'); - } - - /// 切换字幕模式 - void toggleSubtitleMode() { - isSubtitleMode.value = !isSubtitleMode.value; - } - - /// 清空聊天记录 - void clearChat() { - userSpeechText.value = ''; - isUserSpeechFinal.value = true; - aiResponseMessages.clear(); - latestUserMessage.value = ''; - latestAiMessage.value = ''; - partialUserMessage.value = ''; - streamingAiMessage.value = ''; - HapticFeedback.lightImpact(); - } - - /// 连接到AI服务 - Future connect() async { - if (isConnecting.value || isConnected.value) return; - - try { - HapticFeedback.lightImpact(); - - // 初始化Realtime服务 - bool initialized = await _realtimeService.initialize( - serverUrl: 'ws://192.168.1.11:8000/ws', - sampleRate: _sampleRate, - channels: 1, - bitsPerSample: 16, - ); - - if (!initialized) { - throw Exception('初始化Realtime服务失败'); - } - - // 连接服务器 - final connected = await _realtimeService.connect(); - if (!connected) { - throw Exception('连接Vocode服务器失败'); - } - - } catch (e) { - Logger.error('连接失败: $e'); - isConnecting.value = false; - _resetState(); - } - } - - /// 断开连接 - Future disconnect() async { - try { - HapticFeedback.mediumImpact(); - - if (isListening.value) { - await _realtimeService.stopRecording(); - } - - await _realtimeService.stopPlaying(); - await _realtimeService.disconnect(); - - } catch (e) { - Logger.error('断开连接失败: $e'); - } - } - - /// 开始聆听 - void startListening() { - if (!isConnected.value || isListening.value) return; - - HapticFeedback.lightImpact(); - - _realtimeService.startRecording().then((success) { - if (!success) { - Logger.error('开始录音失败'); - } - }); - } - - /// 停止聆听 - void stopListening() { - if (!isListening.value) return; - - _realtimeService.stopRecording().then((success) { - if (!success) { - Logger.error('停止录音失败'); - } - }); - } - - /// 切换聆听状态 - void toggleListening() { - if (!isConnected.value) return; - - if (isListening.value) { - stopListening(); - } else { - startListening(); - } - } - - /// 关闭并返回上一页 - Future closeAndGoBack() async { - Get.back(); - - // 异步清理资源 - Future.delayed(Duration.zero, () async { - try { - if (isListening.value) { - await _realtimeService.stopRecording(); - } - await _realtimeService.stopPlaying(); - await _realtimeService.disconnect(); - await _realtimeService.dispose(); - _resetState(); - } catch (e) { - Logger.error('关闭连接失败: $e'); - } - }); - } - - /// 重置状态 - void _resetState() { - isConnected.value = false; - isConnecting.value = false; - isListening.value = false; - isSpeaking.value = false; - userSpeechText.value = ''; - isUserSpeechFinal.value = true; - aiResponseMessages.clear(); - latestUserMessage.value = ''; - latestAiMessage.value = ''; - partialUserMessage.value = ''; - streamingAiMessage.value = ''; - audioAmplitude.value = 0.0; - } - - /// 清理资源 - void _cleanup() async { - _eventSubscription?.cancel(); - - try { - await _realtimeService.dispose(); - } catch (e) { - Logger.error('清理Realtime服务失败: $e'); - } - } -} \ No newline at end of file diff --git a/apps/client/lib/modules/realtime/views/realtime_view.dart b/apps/client/lib/modules/realtime/views/realtime_view.dart deleted file mode 100644 index cfe33cab..00000000 --- a/apps/client/lib/modules/realtime/views/realtime_view.dart +++ /dev/null @@ -1,332 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import '../controllers/realtime_controller.dart'; -import 'widgets/shader_mic_flow.dart'; - -class RealtimeView extends GetView { - const RealtimeView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - // 设置状态栏为黑色文字 - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: Brightness.dark, - statusBarBrightness: Brightness.light, - ), - ); - - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: Stack( - children: [ - // 中间音频可视化 - Positioned.fill( - child: Obx(() => AnimatedAlign( - alignment: controller.isSubtitleMode.value - ? const Alignment(0, -0.7) // 字幕模式时移到更上方 - : const Alignment(0, -0.25), - duration: const Duration(milliseconds: 600), - curve: Curves.easeInOutCubic, - child: LayoutBuilder( - builder: (context, constraints) { - final targetSize = controller.isSubtitleMode.value - ? MediaQuery.of(context).size.width * 0.55 // 字幕模式时较小 - : MediaQuery.of(context).size.width * 0.75; // 初始状态更大 - - return TweenAnimationBuilder( - tween: Tween(end: targetSize), - duration: const Duration(milliseconds: 600), - curve: Curves.easeInOutCubic, - builder: (context, animatedSize, child) { - return Stack( - alignment: Alignment.center, - children: [ - // 使用Shader动画 - Obx(() => ShaderMicFlow( - size: animatedSize, - amplitude: controller.audioAmplitude.value, - )), - ], - ); - }, - ); - }, - ), - )), - ), - - // 字幕显示区域 - 只有在字幕模式开启时才显示,位于圆形下方 - if (controller.isSubtitleMode.value) - Positioned( - left: 20, - right: 20, - top: MediaQuery.of(context).size.height * 0.35, // 在圆形下方 - bottom: 160, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Obx(() => AnimatedOpacity( - opacity: controller.isSubtitleMode.value ? 1.0 : 0.0, - duration: const Duration(milliseconds: 300), - child: TweenAnimationBuilder( - tween: Tween( - begin: 0.0, - end: controller.isSubtitleMode.value ? 1.0 : 0.0, - ), - duration: const Duration(milliseconds: 900), // 延迟显示,让圆圈先移动 - curve: Curves.easeInOut, - builder: (context, animationValue, child) { - return Transform.translate( - offset: Offset(0, 20 * (1 - animationValue)), // 从下方滑入 - child: Opacity( - opacity: animationValue, - child: _buildChatSubtitles(), - ), - ); - }, - ), - )), - ), - ), - - // 底部控制按钮 - Positioned( - bottom: 80, - left: 0, - right: 0, - child: Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - // 麦克风按钮 - Obx(() => _buildMicButton()), - // 字幕按钮 - Obx(() => _buildMainButton( - icon: controller.isSubtitleMode.value ? Icons.subtitles : Icons.subtitles_outlined, - onTap: controller.toggleSubtitleMode, - enabled: true, - isActive: controller.isSubtitleMode.value, - )), - // 清空聊天按钮 - _buildMainButton( - icon: Icons.clear_all, - onTap: controller.clearChat, - enabled: true, - isActive: false, - ), - // 关闭按钮 - _buildMainButton( - icon: Icons.close, - onTap: controller.closeAndGoBack, - enabled: true, - isActive: false, - ), - ], - ), - ), - ), - - // Home Indicator - Positioned( - bottom: 8, - left: 0, - right: 0, - child: Center( - child: Container( - width: 134, - height: 5, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(2.5), - ), - ), - ), - ), - ], - ), - ), - ); - } - - /// 构建聊天字幕界面 - 上下两行显示用户输入和AI回复 - Widget _buildChatSubtitles() { - return Obx(() { - // 组合用户消息:优先显示实时部分转录,否则显示最终消息 - final String displayUserMessage = controller.partialUserMessage.value.isNotEmpty - ? controller.partialUserMessage.value - : controller.latestUserMessage.value; - - // 组合AI消息:优先显示流式响应,否则显示最终消息 - final String displayAiMessage = controller.streamingAiMessage.value.isNotEmpty - ? controller.streamingAiMessage.value - : controller.latestAiMessage.value; - - final hasUserMessage = displayUserMessage.isNotEmpty; - final hasAiMessage = displayAiMessage.isNotEmpty; - - // 如果没有消息,显示空白 - if (!hasUserMessage && !hasAiMessage) { - return Container(); - } - - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, // 让Column紧凑排列 - children: [ - // 用户输入行 - 蓝色,动态扩展 - if (hasUserMessage) - Container( - width: double.infinity, - constraints: const BoxConstraints( - minHeight: 30, // 最小1行高度 - maxHeight: 104, // 最大4行高度 (26*4) - ), - padding: const EdgeInsets.symmetric(vertical: 4), - child: SingleChildScrollView( - reverse: true, - child: Text( - displayUserMessage, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - color: Colors.blue[600], - height: 1.4, - fontWeight: FontWeight.w500, - letterSpacing: 0.3, - ), - ), - ), - ), - - // 分隔线 - 只有在两个消息都存在时才显示 - if (hasUserMessage && hasAiMessage) - Container( - width: 60, - height: 1, - margin: const EdgeInsets.symmetric(vertical: 2), // 进一步减少分隔线间距 - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(0.5), - ), - ), - - // AI回复行 - 绿色,动态扩展 - if (hasAiMessage) - Container( - width: double.infinity, - constraints: const BoxConstraints( - minHeight: 30, // 最小1行高度 - maxHeight: 104, // 最大4行高度 (26*4) - ), - padding: const EdgeInsets.symmetric(vertical: 4), - child: SingleChildScrollView( - reverse: true, - child: Text( - displayAiMessage, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - color: Colors.green[600], - height: 1.4, - fontWeight: FontWeight.w500, - letterSpacing: 0.3, - ), - ), - ), - ), - ], - ); - }); - } - - /// 构建麦克风按钮 - 根据连接状态和录音状态显示不同的状态 - Widget _buildMicButton() { - // 获取当前状态 - final isConnected = controller.isConnected.value; - final isConnecting = controller.isConnecting.value; - final isListening = controller.isListening.value; - - // 确定按钮状态 - IconData icon; - Color? backgroundColor; - Color? iconColor; - VoidCallback? onTap; - - if (!isConnected && !isConnecting) { - // 未连接状态 - 禁用 - icon = Icons.mic_off; - backgroundColor = Colors.grey[300]; - iconColor = Colors.grey[600]; - onTap = null; - } else if (isConnecting) { - // 连接中状态 - 禁用但显示连接中 - icon = Icons.sync; - backgroundColor = Colors.orange[200]; - iconColor = Colors.orange[700]; - onTap = null; - } else if (isListening) { - // 录音中状态 - 激活状态 - icon = Icons.mic; - backgroundColor = Theme.of(Get.context!).primaryColor; - iconColor = Colors.white; - onTap = controller.toggleListening; - } else { - // 已连接但未录音状态 - 正常状态 - icon = Icons.mic_none; - backgroundColor = Colors.grey[200]; - iconColor = Colors.black; - onTap = controller.toggleListening; - } - - return GestureDetector( - onTap: onTap, - child: Container( - width: 60, - height: 60, - decoration: BoxDecoration( - color: backgroundColor, - shape: BoxShape.circle, - ), - child: Icon( - icon, - color: iconColor, - size: 26, - ), - ), - ); - } - - /// 构建主要控制按钮 - Widget _buildMainButton({ - required IconData icon, - VoidCallback? onTap, - required bool enabled, - required bool isActive, - }) { - return GestureDetector( - onTap: enabled ? onTap : null, - child: Container( - width: 60, - height: 60, - decoration: BoxDecoration( - color: enabled - ? (isActive ? Theme.of(Get.context!).primaryColor : Colors.grey[200]) - : Colors.grey[300], - shape: BoxShape.circle, - ), - child: Icon( - icon, - color: enabled - ? (isActive ? Colors.white : Colors.black) - : Colors.grey[600], - size: 26, - ), - ), - ); - } - -} \ No newline at end of file diff --git a/apps/client/lib/modules/realtime/views/widgets/shader_mic_flow.dart b/apps/client/lib/modules/realtime/views/widgets/shader_mic_flow.dart deleted file mode 100644 index bf21bd14..00000000 --- a/apps/client/lib/modules/realtime/views/widgets/shader_mic_flow.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'dart:ui'; -import 'package:flutter/material.dart'; - -/// 使用Shader实现的麦克风流动动画组件 -class ShaderMicFlow extends StatefulWidget { - final double size; - final double amplitude; - - const ShaderMicFlow({ - super.key, - required this.size, - this.amplitude = 0.0, - }); - - @override - State createState() => _ShaderMicFlowState(); -} - -class _ShaderMicFlowState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - FragmentShader? _shader; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - duration: const Duration(seconds: 60), - vsync: this, - )..repeat(); - - _loadShader(); - } - - Future _loadShader() async { - try { - final program = await FragmentProgram.fromAsset('shaders/mic_flow.frag'); - setState(() { - _shader = program.fragmentShader(); - }); - } catch (e) { - debugPrint('Failed to load shader: $e'); - } - } - - @override - void dispose() { - _controller.dispose(); - _shader?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - width: widget.size, - height: widget.size, - child: AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return CustomPaint( - painter: _shader != null - ? _ShaderPainter( - shader: _shader!, - time: _controller.value * 60.0, - amplitude: widget.amplitude, - ) - : null, - size: Size.square(widget.size), - ); - }, - ), - ); - } -} - -class _ShaderPainter extends CustomPainter { - final FragmentShader shader; - final double time; - final double amplitude; - - _ShaderPainter({ - required this.shader, - required this.time, - required this.amplitude, - }); - - @override - void paint(Canvas canvas, Size size) { - // 设置shader uniforms - shader.setFloat(0, size.width); // u_resolution.x - shader.setFloat(1, size.height); // u_resolution.y - shader.setFloat(2, time); // u_time - shader.setFloat(3, amplitude); // u_amp - - final paint = Paint()..shader = shader; - canvas.drawRect(Offset.zero & size, paint); - } - - @override - bool shouldRepaint(covariant _ShaderPainter oldDelegate) { - return oldDelegate.time != time || oldDelegate.amplitude != amplitude; - } -} \ No newline at end of file diff --git a/apps/client/lib/modules/speech_test/bindings/speech_test_binding.dart b/apps/client/lib/modules/speech_test/bindings/speech_test_binding.dart deleted file mode 100644 index 71e981a9..00000000 --- a/apps/client/lib/modules/speech_test/bindings/speech_test_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/speech_test_controller.dart'; - -class SpeechTestBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => SpeechTestController(), - ); - } -} \ No newline at end of file diff --git a/apps/client/lib/modules/speech_test/controllers/speech_test_controller.dart b/apps/client/lib/modules/speech_test/controllers/speech_test_controller.dart deleted file mode 100644 index a2f74d3b..00000000 --- a/apps/client/lib/modules/speech_test/controllers/speech_test_controller.dart +++ /dev/null @@ -1,426 +0,0 @@ -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import '../../../data/services/asr_service.dart'; -import '../../../data/services/tts_service.dart'; -import '../../../data/services/speech_factory.dart'; -import '../../../core/utils/logger.dart'; - -/// 语音测试控制器 -class SpeechTestController extends GetxController { - // 语音服务工厂 - final SpeechFactory _speechFactory = Get.find(); - - // 当前使用的TTS服务 - late TtsService _ttsService; - - // 当前使用的ASR服务 - late AsrService _asrService; - - // 状态变量 - final RxBool isInitialized = false.obs; - final RxBool isProcessing = false.obs; - final RxBool isListening = false.obs; - final RxBool isSpeaking = false.obs; - - // 识别结果 - final RxString intermediateResult = ''.obs; // 中间结果 - final RxString finalResult = ''.obs; // 最终结果 - final RxString recognitionStatus = '准备就绪'.obs; - final RxString detectedLanguage = ''.obs; // 检测到的语言 - - // TTS事件 - final RxString ttsStatus = '准备就绪'.obs; - - // 当前选择的语音服务类型 - final Rx selectedServiceType = SpeechServiceType.azure.obs; - // 音频输入源 - bool _audioSourceType = false; - // 连续识别结果流 - StreamSubscription? _recognitionSubscription; - - // TTS事件流 - StreamSubscription? _ttsEventSubscription; - - // 文本控制器 - final TextEditingController textController = TextEditingController(); - - // 连续播放的测试句子 - final List testSentences = [ - '这是第一句测试语音。', - '这是第二句测试语音,语音合成支持连续播放。', - '这是第三句测试语音,将在连续模式下播放完毕。' - ]; - - var _currsessionid = ''; - - @override - void onInit() { - super.onInit(); - // 设置默认待播放文本 - textController.text = '欢迎使用语音测试,这是一段默认的测试文本。您可以修改此文本,然后点击播放按钮测试语音合成效果。'; - _initServices(); - } - - /// 初始化服务 - Future _initServices() async { - try { - isProcessing.value = true; - - // 获取服务实例 - _ttsService = _speechFactory.getTtsService(); - _asrService = _speechFactory.getAsrService(); - - // 初始化服务 - await _ttsService.initialize(); - await _asrService.initialize(); - - // 监听TTS事件 - _setupTtsEventListener(); - // 获取识别事件流 - 这启动了异步识别过程 - final stream = await _asrService.recognizeCallback(); - // 设置事件监听 - 异步处理识别结果 - _setupAsrEventListener(stream); - isInitialized.value = true; - ttsStatus.value = '准备就绪'; - recognitionStatus.value = '准备就绪'; - } catch (e) { - ttsStatus.value = '初始化失败'; - recognitionStatus.value = '初始化失败'; - Logger.error('语音测试初始化失败: ${e.toString()}'); - } finally { - isProcessing.value = false; - } - } - - /// 设置TTS事件监听 - void _setupTtsEventListener() { - // 取消旧的订阅 - _ttsEventSubscription?.cancel(); - - // 设置新的订阅 - _ttsEventSubscription = _ttsService.onEvent.listen((event) { - switch (event.type) { - case TtsEventType.started: - isSpeaking.value = true; - ttsStatus.value = '正在播放...'; - break; - case TtsEventType.completed: - isSpeaking.value = false; - ttsStatus.value = '播放完成'; - break; - case TtsEventType.canceled: - isSpeaking.value = false; - ttsStatus.value = '播放已取消'; - break; - case TtsEventType.error: - isSpeaking.value = false; - ttsStatus.value = '播放错误: ${event.error}'; - break; - } - }); - } - - /// 设置ASR事件监听 - void _setupAsrEventListener(Stream stream) { - // 取消旧的订阅 - _recognitionSubscription?.cancel(); - - // 重置识别结果 - intermediateResult.value = ''; - finalResult.value = ''; - detectedLanguage.value = ''; - recognitionStatus.value = '正在等待语音输入...'; - - // 设置新的订阅 - _recognitionSubscription = stream.listen((event) { - switch (event.type) { - case RecognitionEventType.sessionStarted: - isListening.value = true; - recognitionStatus.value = '开始聆听...'; - break; - - case RecognitionEventType.sessionStopped: - isListening.value = false; - recognitionStatus.value = '聆听已结束'; - // 清空中间结果,保留最终结果 - intermediateResult.value = ''; - break; - - case RecognitionEventType.intermediateResult: - // 更新中间结果,显示为斜体或不同颜色 - intermediateResult.value = event.text; - if (event.detectedLanguage.isNotEmpty) { - detectedLanguage.value = event.detectedLanguage; - } - recognitionStatus.value = '正在识别...'; - break; - - case RecognitionEventType.finalResult: - // 更新最终结果,显示为正常文本 - finalResult.value = event.text; - intermediateResult.value = ''; // 清空中间结果 - if (event.detectedLanguage.isNotEmpty) { - detectedLanguage.value = event.detectedLanguage; - } - recognitionStatus.value = '识别完成'; - break; - case RecognitionEventType.finalResult1: - // 更新最终结果,显示为正常文本 - finalResult.value = event.text; - intermediateResult.value = ''; // 清空中间结果 - if (event.detectedLanguage.isNotEmpty) { - detectedLanguage.value = event.detectedLanguage; - } - recognitionStatus.value = '识别完成'; - break; - case RecognitionEventType.error: - isListening.value = false; - recognitionStatus.value = '识别错误: ${event.error}'; - break; - - case RecognitionEventType.canceled: - isListening.value = false; - recognitionStatus.value = '识别已取消'; - // 清空中间结果 - intermediateResult.value = ''; - break; - } - }, onError: (error) { - isListening.value = false; - recognitionStatus.value = '识别错误: $error'; - intermediateResult.value = ''; - }); - } - - /// 切换语音服务类型 - void switchServiceType(SpeechServiceType type) async { - if (selectedServiceType.value == type) return; - - try { - isProcessing.value = true; - - // 停止当前的所有服务状态 - await stopAll(); - - // 切换服务类型 - _speechFactory.setServiceType(type); - selectedServiceType.value = type; - - // 重新初始化服务 - await _initServices(); - } catch (e) { - Logger.error('切换语音服务类型失败: ${e.toString()}'); - } finally { - isProcessing.value = false; - } - } - - /// 停止所有活动 - Future stopAll() async { - // 停止ASR - if (isListening.value) { - await stopContinuousRecognition(); - } - - // 停止TTS - if (isSpeaking.value) { - await stopSpeaking(); - } - - // 取消所有订阅 - await _recognitionSubscription?.cancel(); - _recognitionSubscription = null; - } - - /// 单次播放文本 - Future speakOnce() async { - if (!isInitialized.value || isProcessing.value) return; - - final text = textController.text.trim(); - if (text.isEmpty) { - ttsStatus.value = '请输入要播放的文本'; - return; - } - - try { - isProcessing.value = true; - ttsStatus.value = '准备播放...'; - - final result = await _ttsService.speakOnce(_currsessionid, text); - - if (!result) { - ttsStatus.value = '播放失败'; - } - } catch (e) { - ttsStatus.value = '播放异常: $e'; - Logger.error('单次播放异常: ${e.toString()}'); - } finally { - isProcessing.value = false; - } - } - - /// 连续播放测试 - Future speakContinuously() async { - if (!isInitialized.value || isProcessing.value) return; - - try { - isProcessing.value = true; - ttsStatus.value = '准备连续播放...'; - - // 为避免阻塞,使用一个句子接一个句子的方式添加到队列 - for (final sentence in testSentences) { - if (!await _ttsService.speakStream(_currsessionid, sentence)) { - ttsStatus.value = '添加句子到队列失败'; - break; - } - // 给前一个句子一点处理时间 - await Future.delayed(const Duration(milliseconds: 50)); - } - - // 刷新队列,确保播放完成 - await _ttsService.flushStream(_currsessionid); - } catch (e) { - ttsStatus.value = '连续播放异常: $e'; - Logger.error('连续播放异常: ${e.toString()}'); - } finally { - isProcessing.value = false; - } - } - - /// 停止播放 - Future stopSpeaking() async { - if (!isInitialized.value) return; - - try { - await _ttsService.stop(); - } catch (e) { - ttsStatus.value = '停止播放异常: $e'; - Logger.error('停止播放异常: ${e.toString()}'); - } - } - - /// 一次性识别 - Future recognizeOnce() async { - if (!isInitialized.value || isProcessing.value || isListening.value) return; - - try { - isProcessing.value = true; - recognitionStatus.value = '开始识别...'; - - // 清空之前的结果 - intermediateResult.value = ''; - finalResult.value = ''; - detectedLanguage.value = ''; - - // 显示正在进行的状态 - recognitionStatus.value = '正在聆听...'; - - // 执行一次性识别 - 这是同步等待结果的操作 - final result = await _asrService.recognizeOnce(); - - if (result.isError) { - recognitionStatus.value = '识别错误: ${result.error}'; - Logger.error('一次性识别错误: ${result.error}'); - } else { - // 更新最终结果 - finalResult.value = result.text; - - // 更新语言信息 - if (result.detectedLanguage.isNotEmpty) { - detectedLanguage.value = result.detectedLanguage; - Logger.debug('检测到语言: ${result.detectedLanguage}'); - } - - recognitionStatus.value = '识别完成'; - } - } catch (e) { - recognitionStatus.value = '识别异常: $e'; - Logger.error('一次性识别异常: ${e.toString()}'); - } finally { - isProcessing.value = false; - } - } - - /// 开始连续识别 - Future startContinuousRecognition() async { - if (!isInitialized.value || isProcessing.value || isListening.value) return; - - try { - isProcessing.value = true; - - // 清空之前的结果 - intermediateResult.value = ''; - finalResult.value = ''; - detectedLanguage.value = ''; - recognitionStatus.value = '准备连续识别...'; - - // 获取识别事件流 - 这启动了异步识别过程 - final stream = - await _asrService.startContinuousRecognition(_audioSourceType); - } catch (e) { - recognitionStatus.value = '启动连续识别异常: $e'; - Logger.error('启动连续识别异常: ${e.toString()}'); - isListening.value = false; - intermediateResult.value = ''; - finalResult.value = ''; - } finally { - isProcessing.value = false; - } - } - - /// 停止连续识别 - Future stopContinuousRecognition() async { - if (!isInitialized.value || !isListening.value) return; - - try { - // 首先更新状态,防止重复调用 - isListening.value = false; - recognitionStatus.value = '正在停止识别...'; - - // 停止识别过程 - await _asrService.stopContinuousRecognition(); - - // 更新UI状态 - recognitionStatus.value = '连续识别已停止'; - intermediateResult.value = ''; - } catch (e) { - recognitionStatus.value = '停止连续识别异常: $e'; - Logger.error('停止连续识别异常: ${e.toString()}'); - } - } - - // 获取识别结果(用于UI显示) - String getDisplayResult() { - // 如果有最终结果,优先显示 - String displayText = ''; - - if (finalResult.value.isNotEmpty) { - displayText = finalResult.value; - } - - // 如果有中间结果,添加到最终结果后(可在UI中使用不同样式显示) - if (intermediateResult.value.isNotEmpty) { - if (displayText.isNotEmpty) { - displayText = '$displayText\n\n正在识别: ${intermediateResult.value}'; - } else { - displayText = '正在识别: ${intermediateResult.value}'; - } - } - - return displayText; - } - - @override - void onClose() { - // 取消所有订阅 - _recognitionSubscription?.cancel(); - _ttsEventSubscription?.cancel(); - - // 释放控制器 - textController.dispose(); - - super.onClose(); - } -} diff --git a/apps/client/lib/modules/speech_test/views/speech_test_view.dart b/apps/client/lib/modules/speech_test/views/speech_test_view.dart deleted file mode 100644 index 62a42e41..00000000 --- a/apps/client/lib/modules/speech_test/views/speech_test_view.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import '../controllers/speech_test_controller.dart'; -import '../../../data/services/speech_factory.dart'; - -class SpeechTestView extends GetView { - const SpeechTestView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('语音测试'), - actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: () => controller.switchServiceType(controller.selectedServiceType.value), - tooltip: '重新初始化', - ), - ], - ), - body: Obx(() { - return controller.isInitialized.value - ? _buildTestContent() - : _buildLoadingView(); - }), - ); - } - - Widget _buildLoadingView() { - return const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(), - SizedBox(height: 16), - Text('初始化语音服务中...'), - ], - ), - ); - } - - Widget _buildTestContent() { - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildServiceTypeSelector(), - const SizedBox(height: 24), - _buildTtsSection(), - const SizedBox(height: 24), - const Divider(), - const SizedBox(height: 24), - _buildAsrSection(), - ], - ), - ); - } - - Widget _buildServiceTypeSelector() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '选择语音服务', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Obx(() { - return Wrap( - spacing: 8, - children: [ - for (final type in SpeechServiceType.values) - ChoiceChip( - label: Text(_getServiceTypeName(type)), - selected: controller.selectedServiceType.value == type, - onSelected: (selected) { - if (selected) { - controller.switchServiceType(type); - } - }, - ), - ], - ); - }), - ], - ), - ), - ); - } - - String _getServiceTypeName(SpeechServiceType type) { - switch (type) { - case SpeechServiceType.azure: - return 'Azure语音'; - case SpeechServiceType.iflytek: - return '讯飞语音'; - case SpeechServiceType.google: - return 'Google语音'; - case SpeechServiceType.alibaba: - return '阿里语音(旧版)'; - case SpeechServiceType.alibaba35: - return '阿里语音3.5(声音复刻)'; - case SpeechServiceType.volcano: - return '豆包语音'; - case SpeechServiceType.flutter: - return 'Flutter本地'; - } - } - - Widget _buildTtsSection() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '语音合成测试', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - TextField( - controller: controller.textController, - decoration: const InputDecoration( - labelText: '输入要合成的文本', - border: OutlineInputBorder(), - ), - maxLines: 3, - ), - const SizedBox(height: 16), - Obx(() { - return Text( - '状态: ${controller.ttsStatus}', - style: TextStyle( - color: _getStatusColor(controller.ttsStatus.value), - fontWeight: FontWeight.bold, - ), - ); - }), - const SizedBox(height: 16), - Wrap( - spacing: 8, - runSpacing: 8, - alignment: WrapAlignment.center, - children: [ - Obx(() { - return ElevatedButton.icon( - icon: const Icon(Icons.play_arrow), - label: const Text('单次播放'), - onPressed: controller.isProcessing.value || - controller.isSpeaking.value - ? null - : controller.speakOnce, - ); - }), - Obx(() { - return ElevatedButton.icon( - icon: const Icon(Icons.playlist_play), - label: const Text('连续播放测试'), - onPressed: controller.isProcessing.value || - controller.isSpeaking.value - ? null - : controller.speakContinuously, - ); - }), - Obx(() { - return ElevatedButton.icon( - icon: const Icon(Icons.stop), - label: const Text('停止'), - onPressed: controller.isSpeaking.value - ? controller.stopSpeaking - : null, - ); - }), - ], - ), - const SizedBox(height: 8), - const Divider(), - const SizedBox(height: 8), - const Text( - '连续播放测试会依次播放以下句子:', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - for (int i = 0; i < controller.testSentences.length; i++) - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Text('${i + 1}. ${controller.testSentences[i]}'), - ), - ], - ), - ), - ); - } - - Widget _buildAsrSection() { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '语音识别测试', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Obx(() { - return Text( - '状态: ${controller.recognitionStatus}', - style: TextStyle( - color: _getStatusColor(controller.recognitionStatus.value), - fontWeight: FontWeight.bold, - ), - ); - }), - const SizedBox(height: 8), - Obx(() { - final hasLanguage = controller.detectedLanguage.value.isNotEmpty; - return Visibility( - visible: hasLanguage, - child: Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text( - '检测语言: ${controller.detectedLanguage.value}', - style: const TextStyle( - fontStyle: FontStyle.italic, - color: Colors.blue, - ), - ), - ), - ); - }), - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8), - ), - constraints: const BoxConstraints(minHeight: 100), - width: double.infinity, - child: Obx(() { - final displayText = controller.getDisplayResult(); - return RichText( - text: TextSpan( - style: const TextStyle(fontSize: 16, color: Colors.black), - children: [ - if (displayText.isEmpty) - const TextSpan( - text: '识别结果将显示在这里', - style: TextStyle(color: Colors.grey, fontStyle: FontStyle.italic), - ) - else if (displayText.contains('正在识别:')) - TextSpan( - text: displayText.split('正在识别:')[0], - children: [ - const TextSpan( - text: '\n\n正在识别: ', - style: TextStyle( - color: Colors.blue, - fontWeight: FontWeight.bold, - fontStyle: FontStyle.italic, - ), - ), - TextSpan( - text: displayText.split('正在识别:')[1].trim(), - style: const TextStyle( - color: Colors.blue, - fontStyle: FontStyle.italic, - ), - ), - ], - ) - else - TextSpan(text: displayText), - ], - ), - ); - }), - ), - const SizedBox(height: 16), - Wrap( - spacing: 8, - runSpacing: 8, - alignment: WrapAlignment.center, - children: [ - Obx(() { - return ElevatedButton.icon( - icon: const Icon(Icons.mic), - label: const Text('一次性识别'), - onPressed: controller.isProcessing.value || - controller.isListening.value || - controller.isSpeaking.value - ? null - : controller.recognizeOnce, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - ), - ); - }), - Obx(() { - return ElevatedButton.icon( - icon: Icon( - controller.isListening.value - ? Icons.mic_off - : Icons.mic_none, - ), - label: Text( - controller.isListening.value ? '停止连续识别' : '开始连续识别', - ), - onPressed: controller.isProcessing.value || - controller.isSpeaking.value - ? null - : controller.isListening.value - ? controller.stopContinuousRecognition - : controller.startContinuousRecognition, - style: ElevatedButton.styleFrom( - backgroundColor: controller.isListening.value - ? Colors.red - : Colors.blue, - ), - ); - }), - ], - ), - ], - ), - ), - ); - } - - Color _getStatusColor(String status) { - if (status.contains('错误') || status.contains('失败')) { - return Colors.red; - } else if (status.contains('完成') || status.contains('就绪')) { - return Colors.green; - } else if (status.contains('正在')) { - return Colors.blue; - } else { - return Colors.black; - } - } -} \ No newline at end of file diff --git a/apps/client/lib/modules/timbre/bindings/timbre_binding.dart b/apps/client/lib/modules/timbre/bindings/timbre_binding.dart deleted file mode 100644 index 15a9ecdb..00000000 --- a/apps/client/lib/modules/timbre/bindings/timbre_binding.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/timbre_controller.dart'; - -class TimbreBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => TimbreController()); - } -} diff --git a/apps/client/lib/modules/timbre/controllers/timbre_controller.dart b/apps/client/lib/modules/timbre/controllers/timbre_controller.dart deleted file mode 100644 index c5ebcc4b..00000000 --- a/apps/client/lib/modules/timbre/controllers/timbre_controller.dart +++ /dev/null @@ -1,291 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:crypto/crypto.dart'; -import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart'; -import 'package:record/record.dart'; - -class TimbreController extends GetxController { - final GetStorage _storage = GetStorage(); - final _record = AudioRecorder(); - - final String _appId = "43d87d22"; - final String _appKey = "2bd01ce06ac35957fc0c082f9b6284ec"; - - RxList dataList = [].obs; - RxString type = '1'.obs; - - RxString text = ''.obs; - - String _token = ''; - - int _textId = 0; - int _segId = 0; - String _taskId = ''; - - @override - void onInit() { - super.onInit(); - _getDataList(); - } - - @override - void onClose() { - _record.dispose(); - super.onClose(); - } - - void _getDataList() { - List? timbreList = _storage.read('timbreList'); - String? timbre = _storage.read('timbre'); - if (timbreList != null) { - dataList.value = timbreList; - } else { - dataList.add({ - 'type': '1', - 'name': 'defaultTimbre', - 'description': 'microsoftCloudTimbreService', - 'isUsable': true - }); - _storage.write('timbreList', [ - { - 'type': '1', - 'name': 'defaultTimbre', - 'description': 'microsoftCloudTimbreService', - 'isUsable': true - } - ]); - } - if (timbre != null) { - type.value = timbre; - } - } - - // 录制声音 - void startRecorder() async { - if (_token.isEmpty) { - await _getToken(); - } - await _getTraintext(); - await _addTask(); - final directory = await getApplicationDocumentsDirectory(); - final recordDir = Directory('${directory.path}/RecordSound'); - if (!await recordDir.exists()) { - await recordDir.create(recursive: true); - } - final path = - '${directory.path}/RecordSound/recording_${DateTime.now().millisecondsSinceEpoch}.wav'; - await _record.start( - const RecordConfig( - encoder: AudioEncoder.wav, - sampleRate: 24000, - numChannels: 1, - ), - path: path, - ); - } - - // 获取token - Future _getToken() async { - int timeStamp = DateTime.now().millisecondsSinceEpoch; - String data = - '{"base":{"appid":"$_appId","version":"v1","timestamp":"$timeStamp"},"model":"remote"}'; - String keySignInput = _appKey + timeStamp.toString(); - List keySignBytes = utf8.encode(keySignInput); - Digest keySignDigest = md5.convert(keySignBytes); - String keySign = keySignDigest.toString(); - String signInput = keySign + data; - List signBytes = utf8.encode(signInput); - Digest signDigest = md5.convert(signBytes); - String sign = signDigest.toString(); - - final response = await http.post( - Uri.parse('http://avatar-hci.xfyousheng.com/aiauth/v1/token'), - headers: { - 'Content-Type': 'application/json', - 'Authorization': sign, - }, - body: data, - ); - if (response.statusCode == 200) { - final body = json.decode(response.body); - if (body['retcode'] == '000000') { - _token = body['accesstoken']; - } - } - } - - // 获取训练文本 - Future _getTraintext() async { - String data = '{"textId":"5001"}'; - Map sign = _sign(data); - - final response = await http.post( - Uri.parse('http://opentrain.xfyousheng.com/voice_train/task/traintext'), - headers: { - 'Content-Type': 'application/json', - 'X-Sign': sign['sign'], - 'X-Token': _token, - 'X-AppId': _appId, - 'X-Time': sign['timeStamp'], - }, - body: data, - ); - if (response.statusCode == 200) { - final body = json.decode(response.body); - if (body['code'] == 0) { - _textId = body['data']['textId']; - Map textSegs = body['data']['textSegs'].first; - _segId = textSegs['segId']; - text.value = _decodeCorruptedText(textSegs['segText']); - } - } - } - - // 创建训练任务 - Future _addTask() async { - String data = '{"resourceType":"12","engineVersion":"omni_v1"}'; - Map sign = _sign(data); - - final response = await http.post( - Uri.parse('http://opentrain.xfyousheng.com/voice_train/task/add'), - headers: { - 'Content-Type': 'application/json', - 'X-Sign': sign['sign'], - 'X-Token': _token, - 'X-AppId': _appId, - 'X-Time': sign['timeStamp'], - }, - body: data, - ); - if (response.statusCode == 200) { - final body = json.decode(response.body); - if (body['code'] == 0) { - _taskId = body['data']; - } - } - } - - Map _sign(String data) { - int timeStamp = DateTime.now().millisecondsSinceEpoch; - String keySignInput = _appKey + timeStamp.toString(); - List keySignBytes = utf8.encode(keySignInput); - Digest keySignDigest = md5.convert(keySignBytes); - String keySign = keySignDigest.toString(); - String signInput = keySign + data; - List signBytes = utf8.encode(signInput); - Digest signDigest = md5.convert(signBytes); - String sign = signDigest.toString(); - return {'sign': sign, 'timeStamp': timeStamp.toString()}; - } - - String _decodeCorruptedText(String corruptedText) { - List latin1Bytes = - corruptedText.codeUnits.map((ch) => ch & 0xFF).toList(); - return utf8.decode(latin1Bytes); - } - - // 完成录制 - void completeRecorder() async { - final audioPath = await _record.stop(); - String data = - '{"taskId":"$_taskId","textId":"$_textId","textSegId":"$_segId"}'; - Map sign = _sign(data); - - final uri = Uri.parse( - 'http://opentrain.xfyousheng.com/voice_train/task/submitWithAudio'); - final request = http.MultipartRequest('POST', uri); - request.files.add(await http.MultipartFile.fromPath( - 'file', - audioPath!, - filename: path.basename(audioPath), - )); - request.fields.addAll({ - 'taskId': _taskId, - 'textId': '$_textId', - 'textSegId': '$_segId', - }); - request.headers.addAll({ - "X-Sign": sign['sign'], - "X-Token": _token, - "X-AppId": _appId, - "X-Time": sign['timeStamp'], - }); - final response = await request.send(); - if (response.statusCode == 200) { - final responseBody = await response.stream.bytesToString(); - final body = json.decode(responseBody); - if (body['code'] == 0) { - text.value = ''; - dataList.add({ - 'type': _taskId, - 'name': '声音复刻${dataList.length}', - 'description': '复刻本人音色服务', - 'isUsable': false - }); - await _getTaskResult(_taskId); - } - } - } - - // 查询训练任务状态 - Future _getTaskResult(String id) async { - String data = '{"taskId":"$id"}'; - Map sign = _sign(data); - - final response = await http.post( - Uri.parse('http://opentrain.xfyousheng.com/voice_train/task/result'), - headers: { - 'Content-Type': 'application/json', - 'X-Sign': sign['sign'], - 'X-Token': _token, - 'X-AppId': _appId, - 'X-Time': sign['timeStamp'], - }, - body: data, - ); - if (response.statusCode == 200) { - final body = json.decode(response.body); - if (body['code'] == 0) { - if (body['data']['trainStatus'] == -1) { - await _getTaskResult(id); - } - if (body['data']['trainStatus'] == 1) { - Map? dataMap = - dataList.firstWhereOrNull((item) => item['type'] == id); - if (dataMap != null) { - dataMap['type'] = body['data']['assetId']; - dataMap['isUsable'] = true; - dataList.refresh(); - _storage.write('timbreList', dataList); - } - } - if (body['data']['trainStatus'] == 0) { - Get.snackbar( - 'error'.tr, // 错误 - _decodeCorruptedText(body['data']['failedDesc']), - snackPosition: SnackPosition.TOP, - ); - } - } - } - } - - // 取消 - void cancel() { - text.value = ''; - _record.stop(); - Get.back(); - } - - // 选择音色 - void selectTone(Map data) { - if (!data['isUsable'] || type.value == data['type']) return; - type.value = data['type']; - _storage.write('timbre', data['type']); - } -} diff --git a/apps/client/lib/modules/timbre/views/timbre_view.dart b/apps/client/lib/modules/timbre/views/timbre_view.dart deleted file mode 100644 index 876d10a9..00000000 --- a/apps/client/lib/modules/timbre/views/timbre_view.dart +++ /dev/null @@ -1,242 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import '../../../core/theme/app_colors.dart'; -import '../controllers/timbre_controller.dart'; - -class TimbreView extends GetView { - const TimbreView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - appBar: _buildAppBar(isDarkMode), - body: _buildBody(isDarkMode), - ); - } - - // 构建应用栏 - PreferredSizeWidget _buildAppBar(bool isDarkMode) { - return AppBar( - backgroundColor: isDarkMode ? Colors.black : Colors.white, - elevation: 0.5, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black, - size: 20.sp, - ), - onPressed: () => Get.back(), - ), - title: Text( - 'selecttimbre'.tr, // 选择音色 - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - centerTitle: true, - ); - } - - // 构建主体内容 - Widget _buildBody(bool isDarkMode) { - return Column( - children: [ - Expanded( - child: Obx( - () => ListView.builder( - itemCount: controller.dataList.length, - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), - itemBuilder: (BuildContext context, int index) { - Map data = controller.dataList[index]; - return _item(isDarkMode, data); - }, - ), - ), - ), - GestureDetector( - onTap: () { - controller.startRecorder(); - Get.dialog( - barrierDismissible: false, - AlertDialog( - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, - title: Text( - 'pleaseRead'.tr, // 请大声朗读 - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - content: Obx( - () => controller.text.value.isNotEmpty - ? Text( - controller.text.value, - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ) - : Container( - width: 40.w, - height: 40.w, - alignment: Alignment.center, - child: CircularProgressIndicator( - strokeWidth: 2.w, - valueColor: const AlwaysStoppedAnimation( - Colors.blue), - ), - ), - ), - actions: [ - TextButton( - onPressed: () { - controller.cancel(); - }, - child: Text( - 'cancel'.tr, // 取消 - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ), - Obx(() => controller.text.value.isNotEmpty - ? TextButton( - onPressed: () { - Get.back(); - controller.completeRecorder(); - }, - child: Text( - 'completeRecording'.tr, // 完成录音 - style: TextStyle( - color: isDarkMode - ? Colors.red[300] - : Colors.red[600], - ), - ), - ) - : const SizedBox()), - ], - ), - ); - }, - child: Container( - //width: 130.w, - height: 48.w, - margin: EdgeInsets.symmetric(vertical: 20.w), - padding: EdgeInsets.symmetric(horizontal: 14.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.primary : Colors.blue, - borderRadius: BorderRadius.circular(12.w), - ), - child: Row( - mainAxisSize: MainAxisSize.min, // 添加这行使Row宽度自适应内容 - //mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.keyboard_voice, color: Colors.white), - 5.horizontalSpace, - Text( - 'voiceClone'.tr, - style: TextStyle(color: Colors.white, fontSize: 16.sp), - ), - ], - ), - ), - ), - ], - ); - } - - Widget _item(bool isDarkMode, Map data) { - return InkWell( - onTap: () { - controller.selectTone(data); - }, - child: Obx( - () => Container( - margin: EdgeInsets.symmetric(vertical: 6.h), - padding: EdgeInsets.all(12.w), - decoration: BoxDecoration( - color: controller.type.value == data['type'] - ? (isDarkMode - ? Colors.teal[900]!.withOpacity(0.3) - : Colors.teal[50]!) - : (isDarkMode - ? Colors.white.withOpacity(0.05) - : Colors.grey[100]!), - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: controller.type.value == data['type'] - ? (isDarkMode ? Colors.teal[300]! : Colors.teal[600]!) - : Colors.transparent, - width: 1, - ), - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${data['name']}'.tr, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.black87, - ), - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 4.h), - Text( - '${data['description']}'.tr, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - ], - ), - ), - data['isUsable'] - ? Container( - width: 24.w, - height: 24.w, - margin: EdgeInsets.only(left: 12.w), - decoration: BoxDecoration( - color: controller.type.value == data['type'] - ? (isDarkMode ? Colors.teal[300] : Colors.teal[600]) - : (isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[300]), - shape: BoxShape.circle, - ), - child: controller.type.value == data['type'] - ? Icon( - Icons.check, - size: 16.w, - color: Colors.white, - ) - : null, - ) - : SizedBox( - width: 24.w, - height: 24.w, - child: CircularProgressIndicator( - strokeWidth: 2.w, - valueColor: - const AlwaysStoppedAnimation(Colors.blue), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/apps/client/lib/modules/usage/bindings/usage_binding.dart b/apps/client/lib/modules/usage/bindings/usage_binding.dart deleted file mode 100644 index e03af755..00000000 --- a/apps/client/lib/modules/usage/bindings/usage_binding.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/usage_controller.dart'; -import '../controllers/meeting_stats_controller.dart'; -import '../controllers/translation_stats_controller.dart'; -import '../controllers/aichat_stats_controller.dart'; -import '../../../data/services/usage_stats_service.dart'; - -class UsageBinding implements Bindings { - @override - void dependencies() { - if (!Get.isRegistered()) { - Get.put(UsageStatsService(), permanent: true); - } - - Get.lazyPut(() => UsageController()); - - Get.lazyPut(() => AiChatStatsController()); - - Get.lazyPut(() => TranslationStatsController()); - - Get.lazyPut(() => MeetingStatsController()); - } -} diff --git a/apps/client/lib/modules/usage/controllers/aichat_stats_controller.dart b/apps/client/lib/modules/usage/controllers/aichat_stats_controller.dart deleted file mode 100644 index efc285a6..00000000 --- a/apps/client/lib/modules/usage/controllers/aichat_stats_controller.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; -import '../../../data/models/user_Info.dart'; - -class AiChatStatsController extends GetxController { - final isLoading = false.obs; - - int get currentIntegral => User.instance.aichatintegral; - int get totalIntegral => User.instance.aichattotalintegral; -} - diff --git a/apps/client/lib/modules/usage/controllers/meeting_stats_controller.dart b/apps/client/lib/modules/usage/controllers/meeting_stats_controller.dart deleted file mode 100644 index 2aad0d20..00000000 --- a/apps/client/lib/modules/usage/controllers/meeting_stats_controller.dart +++ /dev/null @@ -1,226 +0,0 @@ -import 'package:get/get.dart'; -import '../../../data/services/usage_stats_service.dart'; -import '../../../data/models/meeting_usage_stats.dart'; -import '../../../core/utils/logger.dart'; - -class MeetingStatsController extends GetxController { - final UsageStatsService _usageService = Get.find(); - - final isLoading = false.obs; - final selectedTabIndex = 0.obs; - final selectedTimeRange = 'today'.tr.obs; // 今日 - - final Rx todayStats = MeetingAsrHistoryStats( - userId: '', - allAsrCalls: 0, - allAudioSeconds: 0, - allAudioFileBytes: 0, - allTranscriptBytes: 0, - ).obs; - final Rx historyStats = MeetingAsrHistoryStats( - userId: '', - allAsrCalls: 0, - allAudioSeconds: 0, - allAudioFileBytes: 0, - allTranscriptBytes: 0, - ).obs; - - final RxList historyDaily = - [].obs; - - final Rx todayStorageStats = - MeetingStorageHistoryStats( - userId: '', - allStorageUploads: 0, - allStorageBytes: 0, - allTransferBytes: 0, - ).obs; - - final Rx historyStorageStats = - MeetingStorageHistoryStats( - userId: '', - allStorageUploads: 0, - allStorageBytes: 0, - allTransferBytes: 0, - ).obs; - - final Rx rangeStart = Rx(null); - final Rx rangeEnd = Rx(null); - final Rx rangeStats = MeetingAsrHistoryStats( - userId: '', - allAsrCalls: 0, - allAudioSeconds: 0, - allAudioFileBytes: 0, - allTranscriptBytes: 0, - ).obs; - final Rx rangeStorageStats = - MeetingStorageHistoryStats( - userId: '', - allStorageUploads: 0, - allStorageBytes: 0, - allTransferBytes: 0, - ).obs; - - @override - void onInit() { - super.onInit(); - loadAllStats(); - } - - Future loadAllStats() async { - isLoading.value = true; - try { - final now = DateTime.now(); - - final todayList = await _usageService.getMeetingHistoryStats( - startDate: DateTime(now.year, now.month, now.day), - endDate: DateTime(now.year, now.month, now.day), - ); - todayStats.value = _usageService.aggregateMeetingStats(todayList); - - final historyStart = now.subtract(const Duration(days: 7)); - historyDaily.value = await _usageService.getMeetingHistoryStats( - startDate: - DateTime(historyStart.year, historyStart.month, historyStart.day), - endDate: DateTime(now.year, now.month, now.day), - ); - historyStats.value = - _usageService.aggregateMeetingStats(historyDaily.value); - - final todayStorageDaily = - await _usageService.getMeetingStorageHistoryStats( - startDate: DateTime(now.year, now.month, now.day), - endDate: DateTime(now.year, now.month, now.day), - ); - todayStorageStats.value = - _usageService.aggregateMeetingStorageStats(todayStorageDaily); - - final historyStorageDaily = - await _usageService.getMeetingStorageHistoryStats( - startDate: - DateTime(historyStart.year, historyStart.month, historyStart.day), - endDate: DateTime(now.year, now.month, now.day), - ); - historyStorageStats.value = - _usageService.aggregateMeetingStorageStats(historyStorageDaily); - - if (rangeStart.value != null && rangeEnd.value != null) { - final customAsrDaily = await _usageService.getMeetingHistoryStats( - startDate: DateTime(rangeStart.value!.year, rangeStart.value!.month, - rangeStart.value!.day), - endDate: DateTime( - rangeEnd.value!.year, rangeEnd.value!.month, rangeEnd.value!.day), - ); - rangeStats.value = _usageService.aggregateMeetingStats(customAsrDaily); - final customStorageDaily = - await _usageService.getMeetingStorageHistoryStats( - startDate: DateTime(rangeStart.value!.year, rangeStart.value!.month, - rangeStart.value!.day), - endDate: DateTime( - rangeEnd.value!.year, rangeEnd.value!.month, rangeEnd.value!.day), - ); - rangeStorageStats.value = - _usageService.aggregateMeetingStorageStats(customStorageDaily); - } else { - rangeStats.value = historyStats.value; - rangeStorageStats.value = historyStorageStats.value; - } - - Logger.info('会议统计数据加载完成'); - } catch (e) { - Logger.error('加载会议统计数据失败: $e'); - } finally { - isLoading.value = false; - } - } - - void switchTimeRange(int index) { - selectedTabIndex.value = index; - switch (index) { - case 0: - selectedTimeRange.value = 'today'.tr; // 今日 - break; - case 1: - selectedTimeRange.value = 'history'.tr; // 历史 - break; - default: - selectedTimeRange.value = 'today'.tr; // 今日 - break; - } - } - - MeetingAsrHistoryStats get currentStats { - switch (selectedTabIndex.value) { - case 0: - return todayStats.value; - case 1: - return rangeStats.value; - default: - return todayStats.value; - } - } - - String formatDuration(int seconds) { - if (seconds < 60) { - return '$seconds${'seconds'.tr}'; // 秒 - } else if (seconds < 3600) { - final minutes = seconds ~/ 60; - final remainingSeconds = seconds % 60; - return '$minutes${'minutes'.tr}$remainingSeconds${'seconds'.tr}'; // 分钟秒 - } else { - final hours = seconds ~/ 3600; - final minutes = (seconds % 3600) ~/ 60; - return '$hours${'hours'.tr}$minutes${'minutes'.tr}'; // 小时分钟 - } - } - - String formatBytes(int bytes) { - if (bytes < 1024) return '$bytes B'; - final kb = bytes / 1024; - if (kb < 1024) return '${kb.toStringAsFixed(1)} KB'; - final mb = kb / 1024; - if (mb < 1024) return '${mb.toStringAsFixed(1)} MB'; - final gb = mb / 1024; - return '${gb.toStringAsFixed(1)} GB'; - } - - String formatNumber(int number) { - if (number < 1000) return number.toString(); - if (number < 1000000) return '${(number / 1000).toStringAsFixed(1)}K'; - return '${(number / 1000000).toStringAsFixed(1)}M'; - } - - Future refreshData() async { - await loadAllStats(); - } - - Future setCustomRangeDates(DateTime start, DateTime end) async { - rangeStart.value = start; - rangeEnd.value = end; - isLoading.value = true; - try { - final asrDaily = await _usageService.getMeetingHistoryStats( - startDate: DateTime(start.year, start.month, start.day), - endDate: DateTime(end.year, end.month, end.day), - ); - rangeStats.value = _usageService.aggregateMeetingStats(asrDaily); - final storageDaily = await _usageService.getMeetingStorageHistoryStats( - startDate: DateTime(start.year, start.month, start.day), - endDate: DateTime(end.year, end.month, end.day), - ); - rangeStorageStats.value = - _usageService.aggregateMeetingStorageStats(storageDaily); - } catch (e) { - Logger.error('设置自定义日期范围失败: $e'); - } finally { - isLoading.value = false; - } - } - - String formatDate(DateTime d) { - final y = d.year.toString(); - final m = d.month.toString().padLeft(2, '0'); - final day = d.day.toString().padLeft(2, '0'); - return '$y-$m-$day'; - } -} diff --git a/apps/client/lib/modules/usage/controllers/translation_cost_controller.dart b/apps/client/lib/modules/usage/controllers/translation_cost_controller.dart deleted file mode 100644 index 934a709e..00000000 --- a/apps/client/lib/modules/usage/controllers/translation_cost_controller.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:get/get.dart'; -import 'package:flutter/material.dart'; -import '../../../data/services/usage_stats_service.dart'; -import '../../../core/utils/logger.dart'; - -class TranslationCostController extends GetxController { - final UsageStatsService _usageService = Get.find(); - final RxMap costConfig = {}.obs; - final RxDouble dailyCostThreshold = 10.0.obs; - final RxDouble monthlyCostThreshold = 300.0.obs; - final translationCostController = TextEditingController(); - final asrCostController = TextEditingController(); - final ttsCostController = TextEditingController(); - final aiSummaryCostController = TextEditingController(); - final dailyThresholdController = TextEditingController(); - final monthlyThresholdController = TextEditingController(); - final isLoading = false.obs; - final isSaving = false.obs; - - @override - void onInit() { - super.onInit(); - } - - @override - void onClose() { - translationCostController.dispose(); - asrCostController.dispose(); - ttsCostController.dispose(); - aiSummaryCostController.dispose(); - dailyThresholdController.dispose(); - monthlyThresholdController.dispose(); - super.onClose(); - } - - void _loadCurrentConfig() {} - - Future saveConfig() async {} - - void resetToDefault() {} - - bool _validateInputs() { - try { - final translationCost = double.parse(translationCostController.text); - final asrCost = double.parse(asrCostController.text); - final ttsCost = double.parse(ttsCostController.text); - final aiSummaryCost = double.parse(aiSummaryCostController.text); - final dailyThreshold = double.parse(dailyThresholdController.text); - final monthlyThreshold = double.parse(monthlyThresholdController.text); - if (translationCost < 0 || - asrCost < 0 || - ttsCost < 0 || - aiSummaryCost < 0) { - _showErrorSnackbar('成本不能为负数'); - return false; - } - if (dailyThreshold < 0 || monthlyThreshold < 0) { - _showErrorSnackbar('预警阈值不能为负数'); - return false; - } - if (dailyThreshold > monthlyThreshold) { - _showErrorSnackbar('日预警阈值不能大于月预警阈值'); - return false; - } - return true; - } catch (e) { - _showErrorSnackbar('请输入有效的数字'); - return false; - } - } - - double calculateEstimatedMonthlyCost() { - return 0.0; - } - - List getCostSavingTips() { - return []; - } - - void _showSuccessSnackbar(String message) { - Get.snackbar( - '成功', - message, - snackPosition: SnackPosition.TOP, - backgroundColor: Get.theme.colorScheme.primary.withValues(alpha: 0.1), - colorText: Get.theme.colorScheme.primary, - duration: const Duration(seconds: 3), - ); - } - - void _showErrorSnackbar(String message) { - Get.snackbar( - '错误', - message, - snackPosition: SnackPosition.TOP, - backgroundColor: Get.theme.colorScheme.error.withValues(alpha: 0.1), - colorText: Get.theme.colorScheme.error, - duration: const Duration(seconds: 3), - ); - } - - void _showInfoSnackbar(String message) { - Get.snackbar( - '提示', - message, - snackPosition: SnackPosition.TOP, - backgroundColor: Get.theme.colorScheme.surface.withValues(alpha: 0.9), - colorText: Get.theme.colorScheme.onSurface, - duration: const Duration(seconds: 3), - ); - } - - String formatCost(double cost) { - return '¥${cost.toStringAsFixed(2)}'; - } - - String formatPercentage(double percentage) { - return '${percentage.toStringAsFixed(1)}%'; - } -} diff --git a/apps/client/lib/modules/usage/controllers/translation_stats_controller.dart b/apps/client/lib/modules/usage/controllers/translation_stats_controller.dart deleted file mode 100644 index 7a326613..00000000 --- a/apps/client/lib/modules/usage/controllers/translation_stats_controller.dart +++ /dev/null @@ -1,328 +0,0 @@ -import 'package:get/get.dart'; -import '../../../data/services/usage_stats_service.dart'; -import '../../../data/models/translation_usage_stats.dart'; -import '../../../core/utils/logger.dart'; -import 'usage_controller.dart'; - -class TranslationStatsController extends GetxController { - final UsageStatsService _usageService = Get.find(); - - final isLoading = false.obs; - final selectedTabIndex = 0.obs; - final selectedTimeRange = 'today'.tr.obs; // 今日 - - final Rx todayStats = - TranslationUsageStats.empty('').obs; - final Rx weekStats = - TranslationUsageStats.empty('').obs; - final Rx monthStats = - TranslationUsageStats.empty('').obs; - final RxList historyStats = - [].obs; - - RealtimeUsageStats get realtimeStats => - _usageService.realtimeTranslationStats.value; - - final RxMap costConfig = {}.obs; - final RxDouble dailyCostThreshold = 10.0.obs; - final RxDouble monthlyCostThreshold = 300.0.obs; - - final RxList> chartData = >[].obs; - - @override - void onInit() { - super.onInit(); - _initializeData(); - } - - void _initializeData() { - ever(_usageService.todayTranslationStats, (stats) { - todayStats.value = stats; - _updateChartData(); - }); - - loadAllStats(); - } - - Future loadAllStats() async { - isLoading.value = true; - - try { - todayStats.value = _usageService.todayTranslationStats.value; - - await _loadWeekStats(); - - await _loadMonthStats(); - - await _loadHistoryStats(); - - _updateChartData(); - - Logger.info('统计数据加载完成'); - } catch (e) { - Logger.error('加载统计数据失败: $e'); - Get.snackbar( - 'loadFailed'.tr, // 加载失败 - 'statsLoadFailedTryLater'.tr, // 统计数据加载失败,请稍后重试 - snackPosition: SnackPosition.TOP, - backgroundColor: Get.theme.colorScheme.error.withValues(alpha: 0.1), - colorText: Get.theme.colorScheme.error, - ); - } finally { - isLoading.value = false; - } - } - - Future _loadWeekStats() async { - try { - final now = DateTime.now(); - final weekStart = now.subtract(Duration(days: now.weekday - 1)); - final weekEnd = weekStart.add(const Duration(days: 6)); - - final dailyStats = await _usageService.getHistoryStats( - startDate: weekStart, - endDate: weekEnd, - ); - - weekStats.value = _aggregateStats(dailyStats, 'week'); - } catch (e) { - Logger.error('加载本周统计失败: $e'); - } - } - - Future _loadMonthStats() async { - try { - final now = DateTime.now(); - monthStats.value = - await _usageService.getMonthlyStats(now.year, now.month); - } catch (e) { - Logger.error('加载本月统计失败: $e'); - } - } - - Future _loadHistoryStats() async { - try { - final endDate = DateTime.now(); - final startDate = endDate.subtract(const Duration(days: 7)); - - historyStats.value = await _usageService.getHistoryStats( - startDate: startDate, - endDate: endDate, - ); - - historyStats.sort((a, b) => b.date.compareTo(a.date)); - } catch (e) { - Logger.error('加载历史统计失败: $e'); - } - } - - TranslationUsageStats _aggregateStats( - List statsList, String period) { - if (statsList.isEmpty) { - return TranslationUsageStats.empty(_usageService.getCurrentUserId()); - } - - int totalTranslationCalls = 0; - int totalAsrCalls = 0; - int totalTtsCalls = 0; - int totalAiSummaryCalls = 0; - int totalSourceChars = 0; - int totalTargetChars = 0; - int totalRecordingDuration = 0; - int totalTtsPlayDuration = 0; - int totalSessions = 0; - - final Map combinedLanguagePairUsage = {}; - final Map combinedModeUsage = {}; - - for (final stats in statsList) { - totalTranslationCalls += stats.translationApiCalls; - totalAsrCalls += stats.asrApiCalls; - totalTtsCalls += stats.ttsApiCalls; - totalAiSummaryCalls += stats.aiSummaryApiCalls; - totalSourceChars += stats.sourceTextCharacters; - totalTargetChars += stats.ttsTextCharacters; - totalRecordingDuration += stats.effectiveAudioDuration; - totalTtsPlayDuration += stats.totalSessionDuration; - totalSessions += stats.sessionCount; - - stats.languagePairUsage.forEach((key, value) { - combinedLanguagePairUsage[key] = - (combinedLanguagePairUsage[key] ?? 0) + value; - }); - - stats.modeUsage.forEach((key, value) { - combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value; - }); - } - - return TranslationUsageStats( - id: '${period}_${DateTime.now().millisecondsSinceEpoch}', - date: DateTime.now(), - userId: 'current_user', - timeGranularity: period, - translationApiCalls: totalTranslationCalls, - asrApiCalls: totalAsrCalls, - ttsApiCalls: totalTtsCalls, - aiSummaryApiCalls: totalAiSummaryCalls, - sourceTextCharacters: totalSourceChars, - ttsTextCharacters: totalTargetChars, - effectiveAudioDuration: totalRecordingDuration, - totalSessionDuration: totalTtsPlayDuration, - sessionCount: totalSessions, - estimatedCost: 0.0, - ); - } - - void _updateChartData() { - final List> data = []; - - switch (selectedTabIndex.value) { - case 0: - data.add({ - 'label': 'today'.tr, // 今日 - 'apiCalls': todayStats.value.totalApiCalls, - 'cost': 0.0, - 'characters': todayStats.value.totalCharacterCount, - }); - break; - case 1: - data.add({ - 'label': 'thisWeek'.tr, // 本周 - 'apiCalls': weekStats.value.totalApiCalls, - 'cost': 0.0, - 'characters': weekStats.value.totalCharacterCount, - }); - break; - case 2: - data.add({ - 'label': 'thisMonth'.tr, // 本月 - 'apiCalls': monthStats.value.totalApiCalls, - 'cost': 0.0, - 'characters': monthStats.value.totalCharacterCount, - }); - break; - case 3: - for (final stats in historyStats.take(7)) { - data.add({ - 'label': '${stats.date.month}/${stats.date.day}', - 'apiCalls': stats.totalApiCalls, - 'cost': 0.0, - 'characters': stats.totalCharacterCount, - }); - } - break; - } - - chartData.value = data; - } - - void switchTimeRange(int index) { - selectedTabIndex.value = index; - - switch (index) { - case 0: - selectedTimeRange.value = 'today'.tr; // 今日 - break; - case 1: - selectedTimeRange.value = 'thisWeek'.tr; // 本周 - break; - case 2: - selectedTimeRange.value = 'thisMonth'.tr; // 本月 - break; - case 3: - selectedTimeRange.value = 'history'.tr; // 历史 - break; - } - - _updateChartData(); - } - - TranslationUsageStats get currentStats { - switch (selectedTabIndex.value) { - case 0: - return todayStats.value; - case 1: - return weekStats.value; - case 2: - return monthStats.value; - default: - return todayStats.value; - } - } - - Future refreshData() async { - await loadAllStats(); - } - - void cleanupOldData() { - _usageService.cleanupOldData(); - Get.snackbar( - 'cleanupComplete'.tr, // 清理完成 - 'cleanedOldData'.tr, // 已清理90天前的过期数据 - snackPosition: SnackPosition.TOP, - backgroundColor: Get.theme.colorScheme.primary.withValues(alpha: 0.1), - colorText: Get.theme.colorScheme.primary, - ); - } - - Future exportStats() async { - try { - Get.snackbar( - 'exportFeature'.tr, // 导出功能 - 'exportInDevelopment'.tr, // 数据导出功能正在开发中 - snackPosition: SnackPosition.TOP, - backgroundColor: Get.theme.colorScheme.primary.withValues(alpha: 0.1), - colorText: Get.theme.colorScheme.primary, - ); - } catch (e) { - Logger.error('导出统计数据失败: $e'); - } - } - - String formatDuration(int seconds) { - if (seconds < 60) { - return '$seconds${'seconds'.tr}'; // 秒 - } else if (seconds < 3600) { - final minutes = seconds ~/ 60; - final remainingSeconds = seconds % 60; - return '$minutes${'minutes'.tr}$remainingSeconds${'seconds'.tr}'; // 分钟秒 - } else { - final hours = seconds ~/ 3600; - final minutes = (seconds % 3600) ~/ 60; - return '$hours${'hours'.tr}$minutes${'minutes'.tr}'; // 小时分钟 - } - } - - String formatCost(double cost) { - return '¥${cost.toStringAsFixed(2)}'; - } - - int _titleTapCount = 0; - int _lastTitleTapTs = 0; - - void onTitleTap() { - final now = DateTime.now().millisecondsSinceEpoch; - if (now - _lastTitleTapTs <= 800) { - _titleTapCount++; - } else { - _titleTapCount = 1; - } - _lastTitleTapTs = now; - if (_titleTapCount >= 3) { - _titleTapCount = 0; - final uc = Get.find(); - uc.toggleAiChat(!uc.showAiChat.value); - } - } - - String formatNumber(int number) { - if (number < 1000) { - return number.toString(); - } else if (number < 1000000) { - return '${(number / 1000).toStringAsFixed(1)}K'; - } else { - return '${(number / 1000000).toStringAsFixed(1)}M'; - } - } -} diff --git a/apps/client/lib/modules/usage/controllers/usage_controller.dart b/apps/client/lib/modules/usage/controllers/usage_controller.dart deleted file mode 100644 index 7407d5e0..00000000 --- a/apps/client/lib/modules/usage/controllers/usage_controller.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../views/translation_stats_view.dart'; -import '../views/meeting_stats_view.dart'; -import '../views/aichat_stats_view.dart'; - -class UsageController extends GetxController { - late PageController pageController; - final RxInt pageIndex = 0.obs; - - final RxBool showAiChat = false.obs; - - List get pageList => showAiChat.value - ? const [ - AiChatStatsView(), - TranslationStatsView(), - MeetingStatsView(), - ] - : const [ - TranslationStatsView(), - MeetingStatsView(), - ]; - - @override - void onInit() { - super.onInit(); - pageController = PageController(initialPage: 0); - } - - void toggleAiChat(bool enable) { - if (showAiChat.value == enable) return; - showAiChat.value = enable; - final int maxIndex = pageList.length - 1; - if (pageIndex.value > maxIndex) { - pageIndex.value = maxIndex; - pageController.jumpToPage(maxIndex); - } else { - pageController.jumpToPage(pageIndex.value); - } - } -} diff --git a/apps/client/lib/modules/usage/views/aichat_stats_view.dart b/apps/client/lib/modules/usage/views/aichat_stats_view.dart deleted file mode 100644 index 728bc180..00000000 --- a/apps/client/lib/modules/usage/views/aichat_stats_view.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import '../../../core/theme/app_colors.dart'; -import '../controllers/aichat_stats_controller.dart'; -import '../../../data/models/user_Info.dart'; - -class AiChatStatsView extends GetView { - const AiChatStatsView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], - appBar: _buildAppBar(isDarkMode), - body: _buildBody(isDarkMode), - ); - } - - PreferredSizeWidget _buildAppBar(bool isDarkMode) { - return AppBar( - leading: IconButton( - icon: Icon(Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black, size: 20.sp), - onPressed: () => Get.back(), - ), - backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white, - elevation: 0.5, - title: Text( - 'aiChat'.tr, // AI聊天 - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black), - ), - centerTitle: true, - ); - } - - Widget _buildBody(bool isDarkMode) { - return Column( - children: [_buildIntegralRow(isDarkMode)], - ); - } - - Widget _buildIntegralRow(bool isDarkMode) { - return Obx(() { - final _ = User.refreshTick.value; - final u = User.instance; - return Padding( - padding: EdgeInsets.all(16.w), - child: Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2)), - ], - ), - child: Row( - children: [ - Icon(Icons.chat_bubble_outline, - color: AppColors.primary, size: 20.sp), - SizedBox(width: 8.w), - Expanded( - child: Text( - 'remainingQuota'.tr, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black), - ), - ), - Text( - '${u.aichatintegral}/${u.aichattotalintegral}', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black), - ), - ], - ), - ), - ); - }); - } -} diff --git a/apps/client/lib/modules/usage/views/meeting_stats_view.dart b/apps/client/lib/modules/usage/views/meeting_stats_view.dart deleted file mode 100644 index 0702a4aa..00000000 --- a/apps/client/lib/modules/usage/views/meeting_stats_view.dart +++ /dev/null @@ -1,266 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import '../../../core/theme/app_colors.dart'; -import '../controllers/meeting_stats_controller.dart'; -import '../../../data/models/user_Info.dart'; -import '../../../data/models/meeting_usage_stats.dart'; - -class MeetingStatsView extends GetView { - const MeetingStatsView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], - appBar: _buildAppBar(context, isDarkMode), - body: Obx(() => controller.isLoading.value - ? const Center(child: CircularProgressIndicator()) - : _buildBody(isDarkMode)), - ); - } - - PreferredSizeWidget _buildAppBar(BuildContext context, bool isDarkMode) { - return AppBar( - leading: IconButton( - icon: Icon(Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black, size: 20.sp), - onPressed: () => Get.back(), - ), - backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white, - elevation: 0.5, - title: Text( - 'meetingStats'.tr, // 会议统计 - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black), - ), - centerTitle: true, - actions: [ - IconButton( - icon: Icon(Icons.date_range, - color: isDarkMode ? Colors.white : Colors.black), - onPressed: () async { - final now = DateTime.now(); - final initialStart = controller.rangeStart.value ?? - DateTime(now.year, now.month, now.day); - final initialEnd = controller.rangeEnd.value ?? - DateTime(now.year, now.month, now.day); - final picked = await showDateRangePicker( - context: context, - firstDate: DateTime(now.year - 1), - lastDate: DateTime(now.year + 1), - initialDateRange: - DateTimeRange(start: initialStart, end: initialEnd), - ); - if (picked != null) { - await controller.setCustomRangeDates(picked.start, picked.end); - } - }, - ), - IconButton( - icon: Icon(Icons.refresh, - color: isDarkMode ? Colors.white : Colors.black), - onPressed: controller.refreshData, - ), - ], - ); - } - - Widget _buildBody(bool isDarkMode) { - final hasRange = controller.rangeStart.value != null && - controller.rangeEnd.value != null; - final historyTitle = hasRange - ? "${'history'.tr} (${controller.formatDate(controller.rangeStart.value!)} ~ ${controller.formatDate(controller.rangeEnd.value!)})" - : 'history'.tr; - return Column( - children: [ - _buildIntegralRow(isDarkMode), - Expanded( - child: SingleChildScrollView( - padding: EdgeInsets.all(16.w), - child: Column( - children: [ - _buildOverviewCard( - 'today'.tr, // 今日 - isDarkMode, - controller.todayStats.value, - controller.todayStorageStats.value, - ), - SizedBox(height: 16.h), - _buildOverviewCard( - historyTitle, - isDarkMode, - controller.rangeStats.value, - controller.rangeStorageStats.value, - ), - ], - ), - ), - ), - ], - ); - } - - Widget _buildIntegralRow(bool isDarkMode) { - return Obx(() { - final _ = User.refreshTick.value; - final u = User.instance; - return Padding( - padding: EdgeInsets.all(16.w), - child: Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2)), - ], - ), - child: Row( - children: [ - Icon(Icons.people, color: AppColors.primary, size: 20.sp), - SizedBox(width: 8.w), - Expanded( - child: Text( - 'remainingQuota'.tr, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black), - ), - ), - Text( - '${u.meetintegral}/${u.meettotalintegral}', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black), - ), - ], - ), - ), - ); - }); - } - - Widget _buildOverviewCard( - String title, - bool isDarkMode, - MeetingAsrHistoryStats asrStats, - MeetingStorageHistoryStats storageStats, - ) { - return Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2)), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.dashboard, color: AppColors.primary, size: 20.sp), - SizedBox(width: 8.w), - Text( - '$title${'overview'.tr}', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black), - ), - ], - ), - SizedBox(height: 16.h), - Row( - children: [ - Expanded( - child: _buildStatItem( - 'asrApiCalls'.tr, // ASR调用次数 - controller.formatNumber(asrStats.allAsrCalls), - Icons.api, - isDarkMode)), - Expanded( - child: _buildStatItem( - 'totalAudioDuration'.tr, // 音频总时长 - controller.formatDuration(asrStats.allAudioSeconds), - Icons.mic, - isDarkMode)), - ], - ), - SizedBox(height: 12.h), - Row( - children: [ - Expanded( - child: _buildStatItem( - 'audioFileSize'.tr, // 音频文件大小 - controller.formatBytes(asrStats.allAudioFileBytes), - Icons.folder, - isDarkMode)), - Expanded( - child: _buildStatItem( - 'transcriptBytes'.tr, // 转写字节 - controller.formatNumber(asrStats.allTranscriptBytes), - Icons.text_snippet, - isDarkMode)), - ], - ), - SizedBox(height: 12.h), - Row( - children: [ - Expanded( - child: _buildStatItem( - 'storageUploads'.tr, // 对象存储上传次数 - controller.formatNumber(storageStats.allStorageUploads), - Icons.cloud_upload, - isDarkMode)), - Expanded( - child: _buildStatItem( - 'storageBytes'.tr, // 对象存储总大小 - controller.formatBytes(storageStats.allStorageBytes), - Icons.cloud, - isDarkMode)), - ], - ), - ], - ), - ); - } - - Widget _buildStatItem( - String label, String value, IconData icon, bool isDarkMode) { - return Column( - children: [ - Icon(icon, color: AppColors.primary, size: 24.sp), - SizedBox(height: 8.h), - Text( - value, - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black), - ), - SizedBox(height: 4.h), - Text( - label, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54), - ), - ], - ); - } -} diff --git a/apps/client/lib/modules/usage/views/translation_stats_view.dart b/apps/client/lib/modules/usage/views/translation_stats_view.dart deleted file mode 100644 index 3600000b..00000000 --- a/apps/client/lib/modules/usage/views/translation_stats_view.dart +++ /dev/null @@ -1,807 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import '../controllers/translation_stats_controller.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../data/models/user_Info.dart'; - -class TranslationStatsView extends GetView { - const TranslationStatsView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], - appBar: _buildAppBar(isDarkMode), - body: Obx(() => controller.isLoading.value - ? _buildLoadingWidget() - : _buildBody(isDarkMode)), - ); - } - - PreferredSizeWidget _buildAppBar(bool isDarkMode) { - return AppBar( - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black, - size: 20.sp, - ), - onPressed: () => Get.back(), - ), - backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white, - elevation: 0.5, - title: GestureDetector( - onTap: controller.onTitleTap, - child: Text( - 'translationStats'.tr, // - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - centerTitle: true, - actions: [ - IconButton( - icon: Icon( - Icons.refresh, - color: isDarkMode ? Colors.white : Colors.black, - ), - onPressed: controller.refreshData, - ), - // PopupMenuButton( - // icon: Icon( - // Icons.more_vert, - // color: isDarkMode ? Colors.white : Colors.black, - // ), - // onSelected: (value) { - // switch (value) { - // case 'cleanup': - // controller.cleanupOldData(); - // break; - // // case 'export': - // // controller.exportStats(); - // // break; - // } - // }, - // itemBuilder: (context) => const [ - // PopupMenuItem( - // value: 'cleanup', - // child: Text('清理过期数据'), - // ), - // // PopupMenuItem( - // // value: 'export', - // // child: Text('导出数据'), - // // ), - // ], - // ), - ], - ); - } - - Widget _buildLoadingWidget() { - return const Center( - child: CircularProgressIndicator(), - ); - } - - Widget _buildBody(bool isDarkMode) { - return Column( - children: [ - _buildIntegralRow(isDarkMode), - _buildTimeRangeSelector(isDarkMode), - Expanded( - child: SingleChildScrollView( - padding: EdgeInsets.all(16.w), - child: Column( - children: [ - // _buildIntegralCard(isDarkMode), - // SizedBox(height: 16.h), - if (controller.selectedTabIndex.value == 0) - _buildRealtimeStatsCard(isDarkMode), - SizedBox(height: 16.h), - _buildOverviewCard(isDarkMode), - SizedBox(height: 16.h), - _buildApiCallsCard(isDarkMode), - SizedBox(height: 16.h), - _buildDataVolumeCard(isDarkMode), - SizedBox(height: 16.h), - _buildUsageBehaviorCard(isDarkMode), - ], - ), - ), - ), - ], - ); - } - - Widget _buildIntegralRow(bool isDarkMode) { - return Obx(() { - final _ = User.refreshTick.value; - final u = User.instance; - return Padding( - padding: EdgeInsets.all(16.w), - child: Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2)), - ], - ), - child: Row( - children: [ - Icon(Icons.translate, color: AppColors.primary, size: 20.sp), - SizedBox(width: 8.w), - Expanded( - child: Text( - 'remainingQuota'.tr, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black), - ), - ), - Text( - '${u.tradeintegral}/${u.tradetotalintegral}', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black), - ), - ], - ), - ), - ); - }); - } - - Widget _buildTimeRangeSelector(bool isDarkMode) { - return Container( - margin: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Obx(() => Row( - children: [ - _buildTimeRangeTab('today'.tr, 0, isDarkMode), // 今日 - _buildTimeRangeTab('thisWeek'.tr, 1, isDarkMode), // 本周 - _buildTimeRangeTab('thisMonth'.tr, 2, isDarkMode), // 本月 - _buildTimeRangeTab('history'.tr, 3, isDarkMode), // 历史 - ], - )), - ); - } - - Widget _buildTimeRangeTab(String title, int index, bool isDarkMode) { - final isSelected = controller.selectedTabIndex.value == index; - - return Expanded( - child: GestureDetector( - onTap: () => controller.switchTimeRange(index), - child: Container( - padding: EdgeInsets.symmetric(vertical: 12.h), - decoration: BoxDecoration( - color: isSelected - ? (isDarkMode ? AppColors.primary : AppColors.primary) - : Colors.transparent, - borderRadius: BorderRadius.circular(12.r), - ), - child: Text( - title, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.sp, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, - color: isSelected - ? Colors.white - : (isDarkMode ? Colors.white70 : Colors.black54), - ), - ), - ), - ), - ); - } - - Widget _buildRealtimeStatsCard(bool isDarkMode) { - return Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.access_time, - color: AppColors.primary, - size: 20.sp, - ), - SizedBox(width: 8.w), - Text( - 'currentSessionStats'.tr, // 当前会话统计 - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ], - ), - SizedBox(height: 16.h), - Obx(() => Row( - children: [ - Expanded( - child: _buildStatItem( - 'translationCount'.tr, // 翻译次数 - controller.realtimeStats.translationCount.toString(), - Icons.translate, - isDarkMode, - ), - ), - Expanded( - child: _buildStatItem( - 'sessionDuration'.tr, // 会话时长 - '${controller.realtimeStats.sessionDurationMinutes}${'minutes'.tr}', - Icons.timer, - isDarkMode, - ), - ), - ], - )), - SizedBox(height: 12.h), - Obx(() => Row( - children: [ - Expanded( - child: _buildStatItem( - 'characters'.tr, // 字符数 - controller.formatNumber( - controller.realtimeStats.sourceCharacters + - controller.realtimeStats.targetCharacters, - ), - Icons.text_fields, - isDarkMode, - ), - ), - Expanded( - child: _buildStatItem( - 'recordingDuration'.tr, // 录音时长 - controller.formatDuration( - controller.realtimeStats.recordingDuration), - Icons.mic, - isDarkMode, - ), - ), - ], - )), - ], - ), - ); - } - - Widget _buildOverviewCard(bool isDarkMode) { - return Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.dashboard, - color: AppColors.primary, - size: 20.sp, - ), - SizedBox(width: 8.w), - Obx(() => Text( - '${controller.selectedTimeRange.value}${'overview'.tr}', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - )), - ], - ), - SizedBox(height: 16.h), - Obx(() => Row( - children: [ - Expanded( - child: _buildStatItem( - 'totalApiCalls'.tr, // 总调用次数 - controller - .formatNumber(controller.currentStats.totalApiCalls), - Icons.api, - isDarkMode, - ), - ), - ], - )), - SizedBox(height: 12.h), - Obx(() => Row( - children: [ - Expanded( - child: _buildStatItem( - 'sessionCount'.tr, // 会话数量 - controller.currentStats.sessionCount.toString(), - Icons.chat, - isDarkMode, - ), - ), - Expanded( - child: _buildStatItem( - 'totalCharacters'.tr, // 总字符数 - controller.formatNumber( - controller.currentStats.totalCharacterCount), - Icons.text_snippet, - isDarkMode, - ), - ), - ], - )), - ], - ), - ); - } - - Widget _buildStatItem( - String label, String value, IconData icon, bool isDarkMode) { - return Column( - children: [ - Icon( - icon, - color: AppColors.primary, - size: 24.sp, - ), - SizedBox(height: 8.h), - Text( - value, - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - SizedBox(height: 4.h), - Text( - label, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ], - ); - } - - Widget _buildApiCallsCard(bool isDarkMode) { - return Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.api, - color: AppColors.primary, - size: 20.sp, - ), - SizedBox(width: 8.w), - Text( - 'apiCallStats'.tr, // API调用统计 - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ], - ), - SizedBox(height: 16.h), - Obx(() => Column( - children: [ - _buildApiCallRow( - 'translationApi'.tr, // 翻译API - controller.currentStats.translationApiCalls, - Icons.translate, - isDarkMode), - SizedBox(height: 12.h), - _buildApiCallRow( - 'asr'.tr, - controller.currentStats.asrApiCalls, - Icons.mic, - isDarkMode), - SizedBox(height: 12.h), - _buildApiCallRow( - 'tts'.tr, - controller.currentStats.ttsApiCalls, - Icons.volume_up, - isDarkMode), - SizedBox(height: 12.h), - _buildApiCallRow( - 'aiSummary'.tr, // AI总结 - controller.currentStats.aiSummaryApiCalls, - Icons.summarize, - isDarkMode), - ], - )), - ], - ), - ); - } - - Widget _buildApiCallRow( - String label, int count, IconData icon, bool isDarkMode) { - return Row( - children: [ - Icon( - icon, - color: AppColors.primary, - size: 18.sp, - ), - SizedBox(width: 12.w), - Expanded( - child: Text( - label, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - Text( - controller.formatNumber(count), - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ], - ); - } - - Widget _buildDataVolumeCard(bool isDarkMode) { - return Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.data_usage, - color: AppColors.primary, - size: 20.sp, - ), - SizedBox(width: 8.w), - Text( - 'dataVolumeStats'.tr, // 数据量统计 - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ], - ), - SizedBox(height: 16.h), - Obx(() => Column( - children: [ - Row( - children: [ - Expanded( - child: _buildStatItem( - 'sourceTextCharacters'.tr, // 源文本字符 - controller.formatNumber( - controller.currentStats.sourceTextCharacters), - Icons.text_fields, - isDarkMode, - ), - ), - Expanded( - child: _buildStatItem( - 'ttsTextCharacters'.tr, // TTS文本字符 - controller.formatNumber( - controller.currentStats.ttsTextCharacters), - Icons.translate, - isDarkMode, - ), - ), - ], - ), - SizedBox(height: 16.h), - Row( - children: [ - Expanded( - child: _buildStatItem( - 'effectiveAudioDuration'.tr, // 有效音频时长 - controller.formatDuration( - controller.currentStats.effectiveAudioDuration), - Icons.mic, - isDarkMode, - ), - ), - Expanded( - child: _buildStatItem( - 'totalSessionDuration'.tr, // 总会话时长 - controller.formatDuration( - controller.currentStats.totalSessionDuration), - Icons.volume_up, - isDarkMode, - ), - ), - ], - ), - ], - )), - ], - ), - ); - } - - Widget _buildCostCard(bool isDarkMode) { - return Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.attach_money, - color: AppColors.primary, - size: 20.sp, - ), - SizedBox(width: 8.w), - Text( - 'costStats'.tr, // 成本统计 - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ], - ), - SizedBox(height: 16.h), - Obx(() => Column( - children: [ - Container( - padding: EdgeInsets.all(12.w), - decoration: BoxDecoration( - color: AppColors.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8.r), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${controller.selectedTimeRange.value}${'estimatedCost'.tr}', - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - Text( - controller.formatCost( - controller.currentStats.estimatedCost), - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.bold, - color: AppColors.primary, - ), - ), - ], - ), - ), - SizedBox(height: 12.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'dailyCostThreshold'.tr, // 日成本预警阈值 - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - Text( - controller - .formatCost(controller.dailyCostThreshold.value), - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ], - ), - SizedBox(height: 4.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'monthlyCostThreshold'.tr, // 月成本预警阈值 - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - Text( - controller - .formatCost(controller.monthlyCostThreshold.value), - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ], - ), - ], - )), - ], - ), - ); - } - - Widget _buildUsageBehaviorCard(bool isDarkMode) { - return Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.analytics, - color: AppColors.primary, - size: 20.sp, - ), - SizedBox(width: 8.w), - Text( - 'usageBehaviorStats'.tr, // 使用行为统计 - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ], - ), - SizedBox(height: 16.h), - Obx(() => Column( - children: [ - _buildUsageBehaviorRow( - 'mostUsedLanguagePair'.tr, // 最常用语言对 - controller.currentStats.mostUsedLanguagePair, - Icons.language, - isDarkMode, - ), - SizedBox(height: 12.h), - _buildUsageBehaviorRow( - 'mostUsedMode'.tr, // 最常用模式 - controller.currentStats.mostUsedMode, - Icons.settings, - isDarkMode, - ), - ], - )), - ], - ), - ); - } - - Widget _buildUsageBehaviorRow( - String label, String value, IconData icon, bool isDarkMode) { - return Row( - children: [ - Icon( - icon, - color: AppColors.primary, - size: 18.sp, - ), - SizedBox(width: 12.w), - Expanded( - child: Text( - label, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), - Text( - value, - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ], - ); - } -} diff --git a/apps/client/lib/modules/usage/views/usage_view.dart b/apps/client/lib/modules/usage/views/usage_view.dart deleted file mode 100644 index 7bf1c8ea..00000000 --- a/apps/client/lib/modules/usage/views/usage_view.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import '../../../core/theme/app_colors.dart'; -import '../controllers/usage_controller.dart'; - -class UsageView extends GetView { - const UsageView({super.key}); - - // 2. 颜色配置集中管理 - Color _getBgColor(bool isDark) => - isDark ? Colors.black : const Color(0xFFF9FAFB); - Color _getUnselectedColor(bool isDark) => - isDark ? Colors.grey[500]! : const Color(0xFF6B7280); - Color _getSelectedColor(bool isDark) => - isDark ? Colors.blue[200]! : const Color(0xFF3B82F6); - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], - body: Obx(() => PageView( - physics: const NeverScrollableScrollPhysics(), - controller: controller.pageController, - children: controller.pageList, - )), - bottomNavigationBar: Obx( - () { - final currentIndex = controller.pageIndex.value; - - return Container( - decoration: BoxDecoration( - border: Border( - top: BorderSide( - color: isDarkMode ? Colors.grey[800]! : Colors.grey[200]!, - width: 0.5, - )), - ), - // 3. 添加安全区域适配(针对刘海屏) - child: BottomNavigationBar( - // 4. 使用生成的导航项 - items: (controller.showAiChat.value - ? const [ - _NavItem(Icons.chat_bubble_outline, 'aiChat'), - _NavItem(Icons.translate, 'interpretation'), - _NavItem(Icons.people, 'meeting'), - ] - : const [ - _NavItem(Icons.translate, 'interpretation'), - _NavItem(Icons.people, 'meeting'), - ]) - .map((item) => BottomNavigationBarItem( - icon: Icon(item.icon), - activeIcon: Icon(item.icon, - color: _getSelectedColor(isDarkMode)), // 5. 激活态图标 - label: item.label.tr, // 统一处理国际化 - )) - .toList(), - - backgroundColor: _getBgColor(isDarkMode), - currentIndex: currentIndex, - unselectedItemColor: _getUnselectedColor(isDarkMode), - selectedItemColor: _getSelectedColor(isDarkMode), - unselectedFontSize: 10.sp, - selectedFontSize: 10.sp, - - // 6. 添加动画过渡效果 - onTap: (index) => _onItemTapped(index, currentIndex), - - type: BottomNavigationBarType.fixed, - // 7. 增加图标和文字的间距 - iconSize: 22.w, // 响应式图标尺寸 - selectedLabelStyle: TextStyle(height: 1.5.sp), // 文字垂直间距 - unselectedLabelStyle: TextStyle(height: 1.5.sp), - ), - ); - }, - ), - ); - } - - // 9. 抽离点击逻辑 - void _onItemTapped(int index, int currentIndex) { - if (index == currentIndex) return; - - // 10. 添加页面切换动画 - controller.pageController.animateToPage( - index, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - controller.pageIndex.value = index; - } -} - -// 11. 使用数据类管理导航项配置 -class _NavItem { - final IconData icon; - final String label; - - const _NavItem(this.icon, this.label); -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/.gitignore b/apps/client/local_plugins/asr_tr_tts_manager/.gitignore deleted file mode 100644 index b9d7f25b..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ -.dart_tool/ -.flutter-plugins-dependencies -/build/ -/coverage/ diff --git a/apps/client/local_plugins/asr_tr_tts_manager/.metadata b/apps/client/local_plugins/asr_tr_tts_manager/.metadata deleted file mode 100644 index 89d2197c..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/.metadata +++ /dev/null @@ -1,33 +0,0 @@ -# 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: "f6ff1529fd6d8af5f706051d9251ac9231c83407" - channel: "stable" - -project_type: plugin - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - - platform: android - create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - - platform: ios - create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - - # 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/apps/client/local_plugins/asr_tr_tts_manager/CHANGELOG.md b/apps/client/local_plugins/asr_tr_tts_manager/CHANGELOG.md deleted file mode 100644 index 41cc7d81..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* TODO: Describe initial release. diff --git a/apps/client/local_plugins/asr_tr_tts_manager/LICENSE b/apps/client/local_plugins/asr_tr_tts_manager/LICENSE deleted file mode 100644 index ba75c69f..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/apps/client/local_plugins/asr_tr_tts_manager/README.md b/apps/client/local_plugins/asr_tr_tts_manager/README.md deleted file mode 100644 index b2c95838..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# asr_tr_tts_manager - -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/to/develop-plugins), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev), which offers tutorials, -samples, guidance on mobile development, and a full API reference. - diff --git a/apps/client/local_plugins/asr_tr_tts_manager/analysis_options.yaml b/apps/client/local_plugins/asr_tr_tts_manager/analysis_options.yaml deleted file mode 100644 index a5744c1c..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/analysis_options.yaml +++ /dev/null @@ -1,4 +0,0 @@ -include: package:flutter_lints/flutter.yaml - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/.gitignore b/apps/client/local_plugins/asr_tr_tts_manager/android/.gitignore deleted file mode 100644 index 161bdcda..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -.cxx diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/build.gradle b/apps/client/local_plugins/asr_tr_tts_manager/android/build.gradle deleted file mode 100644 index 39edae14..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/build.gradle +++ /dev/null @@ -1,76 +0,0 @@ -group = "com.example.asr_tr_tts_manager" -version = "1.0-SNAPSHOT" - -buildscript { - ext.kotlin_version = "2.1.0" - repositories { - google() - mavenCentral() - } - - dependencies { - classpath("com.android.tools.build:gradle:8.9.1") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: "com.android.library" -apply plugin: "kotlin-android" - -android { - namespace = "com.example.asr_tr_tts_manager" - - compileSdk = 36 - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11 - } - - sourceSets { - main.java.srcDirs += "src/main/kotlin" - test.java.srcDirs += "src/test/kotlin" - } - - defaultConfig { - // 微软语音 SDK 传递依赖的 azure-core 使用了 MethodHandle.invoke/invokeExact, - // 仅 Android 8.0 (API 26) 起支持,故本模块要求 minSdk >= 26。 - minSdk = 26 - consumerProguardFiles "proguard-rules.pro" - } - - dependencies { - testImplementation("org.jetbrains.kotlin:kotlin-test") - testImplementation("org.mockito:mockito-core:5.0.0") - } - - testOptions { - unitTests.all { - useJUnitPlatform() - - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} - -dependencies { - implementation(project(":bluetooth_manager")) - implementation(project(":audio_recorder")) - implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.50.0") - implementation("com.squareup.okhttp3:okhttp:4.12.0") -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/proguard-rules.pro b/apps/client/local_plugins/asr_tr_tts_manager/android/proguard-rules.pro deleted file mode 100644 index fc42f85e..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/proguard-rules.pro +++ /dev/null @@ -1,2 +0,0 @@ --keep class com.microsoft.cognitiveservices.speech.** { *; } --keep class com.microsoft.cognitiveservices.speech.audio.** { *; } \ No newline at end of file diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/settings.gradle b/apps/client/local_plugins/asr_tr_tts_manager/android/settings.gradle deleted file mode 100644 index 88ed34f5..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'asr_tr_tts_manager' diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/AndroidManifest.xml b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/AndroidManifest.xml deleted file mode 100644 index 8cd27b71..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/AsrTrTtsHelper.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/AsrTrTtsHelper.kt deleted file mode 100644 index 74abdc3f..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/AsrTrTtsHelper.kt +++ /dev/null @@ -1,177 +0,0 @@ -package com.example.asr_tr_tts_manager - -import android.content.Context -import com.example.asr_tr_tts_manager.asr.* -import com.example.asr_tr_tts_manager.translation.* -import com.example.asr_tr_tts_manager.tts.* -import java.util.UUID - -/** - * ASR + 翻译 + TTS 一体化助手。 - * - * @param context Android 上下文 - * @param asrProvider 语音识别服务提供商 - * @param translationProvider 翻译服务提供商 - * @param ttsProvider 语音合成服务提供商 - * @param languages 语言列表,第一个为源语言,最后一个为目标语言 - * @param autoPlay 是否在合成完成后自动播放音频 - * @param autoDetectLanguage 是否自动检测输入语言并动态切换源/目标语言 - * @param forceBuiltInSpeaker 是否强制通过手机内置扬声器播放(忽略蓝牙等外部设备) - */ -class AsrTrTtsHelper( - context: Context, - asrProvider: AsrProvider = AsrProvider.AZURE, - translationProvider: TranslationProvider = TranslationProvider.AZURE, - ttsProvider: TtsProvider = TtsProvider.AZURE, - languages: List = listOf("zh-CN", "en-US"), - autoPlay: Boolean = false, - autoDetectLanguage: Boolean = false, - forceBuiltInSpeaker: Boolean = false, -) { - @Volatile - private var currentLanguages: List = languages - - private val asrManager = AsrManager(asrProvider, languages, autoDetectLanguage) - private val translationManager = TranslationManager(translationProvider) - private val ttsManager = TtsManager(context, ttsProvider, autoPlay, forceBuiltInSpeaker) - - @Volatile - private var sessionId = UUID.randomUUID().toString() - - @Volatile - private var lastRecognizingResult = AsrResult("", "") - - @Volatile - var callback: ((Map) -> Unit)? = null - - @Volatile - var onSynthesisEncoded: ((ByteArray) -> Unit)? = null - set(value) { - field = value - ttsManager.onSynthesisEncoded = value - } - - @Volatile - var autoPlay: Boolean = autoPlay - set(value) { - field = value - ttsManager.autoPlay = value - } - - @Volatile - var forceBuiltInSpeaker: Boolean = forceBuiltInSpeaker - set(value) { - field = value - ttsManager.forceBuiltInSpeaker = value - } - - @Volatile - var autoDetectLanguage: Boolean = autoDetectLanguage - - init { - asrManager.onRecognizing = { result -> - lastRecognizingResult = result - val sid = sessionId - val cb = callback - cb?.invoke(mapOf("sessionId" to sid, "type" to "recognizing", "recognizing" to result.text, "language" to result.language)) - } - - asrManager.onRecognized = handler@{ result -> - val sid = sessionId - val finalResult = if (result.text.isEmpty()) lastRecognizingResult else result - lastRecognizingResult = AsrResult("", "") - sessionId = UUID.randomUUID().toString() - - if (finalResult.text.isEmpty()) return@handler - - val languagesSnapshot = currentLanguages - val sourceLanguage: LanguageInfo? - val targetLanguage: LanguageInfo? - - if (autoDetectLanguage) { - val detectedLanguage = LanguageManager.findByCode(finalResult.language) - if (detectedLanguage != null) { - sourceLanguage = detectedLanguage - val other = languagesSnapshot.firstOrNull { - !it.equals(finalResult.language, ignoreCase = true) - } - targetLanguage = LanguageManager.findByCode(other) - } else { - sourceLanguage = LanguageManager.findByCode(languagesSnapshot.firstOrNull()) - targetLanguage = LanguageManager.findByCode(languagesSnapshot.lastOrNull()) - } - } else { - sourceLanguage = LanguageManager.findByCode(languagesSnapshot.firstOrNull()) - targetLanguage = LanguageManager.findByCode(languagesSnapshot.lastOrNull()) - } - - val translationResult = - translationManager.translateText( - text = finalResult.text, - sourceLanguage = sourceLanguage?.language ?: "", - targetLanguage = targetLanguage?.language ?: "", - ) - val translated = translationResult.translatedText.orEmpty() - val cb = callback - cb?.invoke(mapOf("sessionId" to sid, "type" to "recognized", "recognized" to finalResult.text, "translated" to translated, "language" to finalResult.language)) - if (translated.isNotEmpty()) { - ttsManager.speakText( - translated, - targetLanguage?.code ?: "", - targetLanguage?.voiceName ?: "" - ) - } - } - } - - fun switchProvider( - asrProvider: AsrProvider? = null, - translationProvider: TranslationProvider? = null, - ttsProvider: TtsProvider? = null, - languages: List? = null, - ) { - if (languages != null) { - currentLanguages = languages - } - - if (asrProvider != null && languages != null) { - asrManager.switchProvider(provider = asrProvider, languages = languages) - } else if (asrProvider != null) { - asrManager.switchProvider(provider = asrProvider) - } else if (languages != null) { - asrManager.switchProvider(languages = languages) - } - - if (translationProvider != null) { - translationManager.switchProvider(provider = translationProvider) - } - - if (ttsProvider != null) { - ttsManager.switchProvider(provider = ttsProvider) - } - } - - fun startRecognition() { - asrManager.startRecognition() - } - - fun stopRecognition() { - asrManager.stopRecognition() - } - - fun pushStreamWrite(bytes: ByteArray) { - asrManager.pushStreamWrite(bytes) - } - - fun pushStreamClose() { - asrManager.pushStreamClose() - } - - fun dispose() { - callback = null - onSynthesisEncoded = null - - ttsManager.dispose() - asrManager.dispose() - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/AsrTrTtsManagerPlugin.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/AsrTrTtsManagerPlugin.kt deleted file mode 100644 index e9592f22..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/AsrTrTtsManagerPlugin.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.example.asr_tr_tts_manager - -import android.app.Activity -import android.content.Intent -import android.media.AudioManager -import android.os.Build -import com.example.asr_tr_tts_manager.translation_option.TranslationHelperManager -import com.example.asr_tr_tts_manager.translation_option.TranslationHelperProvider -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.EventChannel.EventSink -import io.flutter.plugin.common.EventChannel.StreamHandler -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result - -/** AsrTrTtsManagerPlugin */ -class AsrTrTtsManagerPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { - // The MethodChannel that will the communication between Flutter and native Android - // - // This local reference serves to register the plugin with the Flutter Engine and unregister it - // when the Flutter Engine is detached from the Activity - private lateinit var channel: MethodChannel - private lateinit var translationChannel: EventChannel - private var translationChannelSink: EventSink? = null - private var appContext: android.content.Context? = null - - private var translationHelper: TranslationHelperManager? = null - private var currentProvider: TranslationHelperProvider? = null - - // 面对面翻译外放走 USAGE_VOICE_COMMUNICATION(通话流),硬件音量键默认却控制媒体流, - // 导致用户调不动扬声器音量。会话期间把音量键绑定到通话流,结束后还原。 - private var activity: Activity? = null - - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - appContext = flutterPluginBinding.applicationContext - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "asr_tr_tts_manager") - translationChannel = EventChannel(flutterPluginBinding.binaryMessenger, "asr_tr_tts_manager/translation") - channel.setMethodCallHandler(this) - translationChannel.setStreamHandler(object : StreamHandler { - override fun onCancel(arguments: Any?) { - translationChannelSink = null - } - - override fun onListen(arguments: Any?, events: EventSink?) { - translationChannelSink = events - } - }) - } - - override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "translationInit" -> translationInit(call, result) - "translationStart" -> translationStart(call, result) - "translationStop" -> translationStop(result) - "translationDispose" -> translationDispose(result) - "enableRecord" -> { - val filePath = call.argument("filePath") ?: "" - translationHelper?.enableRecord(filePath) - result.success(true) - } - "stopRecord" -> { - val isSave = call.argument("isSave") ?: false - translationHelper?.stopRecord(isSave) - result.success(true) - } - else -> result.notImplemented() - } - } - - private fun translationInit(call: MethodCall, result: Result) { - val providerStr = call.argument("provider") ?: "CALL" - val provider = when (providerStr.uppercase()) { - "SIMULTANEOUS" -> TranslationHelperProvider.SIMULTANEOUS - "FACETOFACE" -> TranslationHelperProvider.FACETOFACE - else -> TranslationHelperProvider.CALL - } - translationHelper?.dispose() - currentProvider = provider - translationHelper = TranslationHelperManager(appContext!!, provider) - translationHelper?.callback = { data -> - translationChannelSink?.success(data) - } - result.success(true) - } - - private fun translationStart(call: MethodCall, result: Result) { - val sourceLanguage = call.argument("sourceLanguage") ?: "zh-CN" - val targetLanguage = call.argument("targetLanguage") ?: "en-US" - startForegroundService() - translationHelper?.start(sourceLanguage, targetLanguage) - applyVolumeControlStream() - result.success(true) - } - - private fun translationStop(result: Result) { - translationHelper?.stop() - restoreVolumeControlStream() - stopForegroundService() - result.success(true) - } - - private fun translationDispose(result: Result) { - translationHelper?.dispose() - translationHelper = null - currentProvider = null - restoreVolumeControlStream() - stopForegroundService() - result.success(true) - } - - /** 仅面对面翻译外放走通话流,把音量键绑定到 STREAM_VOICE_CALL,让用户能调扬声器音量。 */ - private fun applyVolumeControlStream() { - if (currentProvider == TranslationHelperProvider.FACETOFACE) { - activity?.volumeControlStream = AudioManager.STREAM_VOICE_CALL - } - } - - private fun restoreVolumeControlStream() { - activity?.volumeControlStream = AudioManager.USE_DEFAULT_STREAM_TYPE - } - - private fun startForegroundService() { - val ctx = appContext ?: return - val intent = Intent(ctx, TranslationForegroundService::class.java) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - ctx.startForegroundService(intent) - } else { - ctx.startService(intent) - } - } - - private fun stopForegroundService() { - val ctx = appContext ?: return - ctx.stopService(Intent(ctx, TranslationForegroundService::class.java)) - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } - - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activity = binding.activity - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activity = binding.activity - } - - override fun onDetachedFromActivityForConfigChanges() { - activity = null - } - - override fun onDetachedFromActivity() { - activity = null - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/LanguageManager.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/LanguageManager.kt deleted file mode 100644 index 760501f1..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/LanguageManager.kt +++ /dev/null @@ -1,222 +0,0 @@ -package com.example.asr_tr_tts_manager - -import java.util.Locale - -data class LanguageInfo( - val code: String, - val language: String, - val voiceName: String, -) - -object LanguageManager { - val all: List = - listOf( - LanguageInfo( - code = "zh-CN", - language = "zh", - voiceName = "zh-CN-XiaoxiaoNeural", - ), - LanguageInfo( - code = "zh-TW", - language = "zh-Hant", - voiceName = "zh-TW-HsiaoChenNeural", - ), - LanguageInfo( - code = "zh-HK", - language = "zh-Hant-hk", - voiceName = "zh-HK-HiuMaanNeural", - ), - LanguageInfo( - code = "en-US", - language = "en", - voiceName = "en-US-AriaNeural", - ), - LanguageInfo( - code = "ja-JP", - language = "ja", - voiceName = "ja-JP-NanamiNeural", - ), - LanguageInfo( - code = "ko-KR", - language = "ko", - voiceName = "ko-KR-SunHiNeural", - ), - LanguageInfo( - code = "fr-FR", - language = "fr", - voiceName = "fr-FR-DeniseNeural", - ), - LanguageInfo( - code = "es-ES", - language = "es", - voiceName = "es-ES-ElviraNeural", - ), - LanguageInfo( - code = "pt-BR", - language = "pt", - voiceName = "pt-BR-FranciscaNeural", - ), - LanguageInfo( - code = "it-IT", - language = "it", - voiceName = "it-IT-ElsaNeural", - ), - LanguageInfo( - code = "de-DE", - language = "de", - voiceName = "de-DE-KatjaNeural", - ), - LanguageInfo( - code = "ru-RU", - language = "ru", - voiceName = "ru-RU-SvetlanaNeural", - ), - LanguageInfo( - code = "pl-PL", - language = "pl", - voiceName = "pl-PL-AgnieszkaNeural", - ), - LanguageInfo( - code = "nl-NL", - language = "nl", - voiceName = "nl-NL-ColetteNeural", - ), - LanguageInfo( - code = "sv-SE", - language = "sv", - voiceName = "sv-SE-SofieNeural", - ), - LanguageInfo( - code = "cs-CZ", - language = "cs", - voiceName = "cs-CZ-VlastaNeural", - ), - LanguageInfo( - code = "el-GR", - language = "el", - voiceName = "el-GR-AthinaNeural", - ), - LanguageInfo( - code = "ro-RO", - language = "ro", - voiceName = "ro-RO-AlinaNeural", - ), - LanguageInfo( - code = "hu-HU", - language = "hu", - voiceName = "hu-HU-NoemiNeural", - ), - LanguageInfo( - code = "uk-UA", - language = "uk", - voiceName = "uk-UA-PolinaNeural", - ), - LanguageInfo( - code = "da-DK", - language = "da", - voiceName = "da-DK-ChristelNeural", - ), - LanguageInfo( - code = "fi-FI", - language = "fi", - voiceName = "fi-FI-NooraNeural", - ), - LanguageInfo( - code = "nb-NO", - language = "nb", - voiceName = "nb-NO-IselinNeural", - ), - LanguageInfo( - code = "hr-HR", - language = "hr", - voiceName = "hr-HR-GabrijelaNeural", - ), - LanguageInfo( - code = "ca-ES", - language = "ca", - voiceName = "ca-ES-JoanaNeural", - ), - LanguageInfo( - code = "ar-EG", - language = "ar", - voiceName = "ar-EG-SalmaNeural", - ), - LanguageInfo( - code = "th-TH", - language = "th", - voiceName = "th-TH-PremwadeeNeural", - ), - LanguageInfo( - code = "vi-VN", - language = "vi", - voiceName = "vi-VN-HoaiMyNeural", - ), - LanguageInfo( - code = "id-ID", - language = "id", - voiceName = "id-ID-GadisNeural", - ), - LanguageInfo( - code = "hi-IN", - language = "hi", - voiceName = "hi-IN-SwaraNeural", - ), - LanguageInfo( - code = "tr-TR", - language = "tr", - voiceName = "tr-TR-EmelNeural", - ), - LanguageInfo( - code = "ms-MY", - language = "ms", - voiceName = "ms-MY-YasminNeural", - ), - LanguageInfo( - code = "bn-IN", - language = "bn", - voiceName = "bn-IN-TanishaaNeural", - ), - LanguageInfo( - code = "ta-IN", - language = "ta", - voiceName = "ta-IN-PallaviNeural", - ), - LanguageInfo( - code = "te-IN", - language = "te", - voiceName = "te-IN-ShrutiNeural", - ), - LanguageInfo( - code = "mr-IN", - language = "mr", - voiceName = "mr-IN-AarohiNeural", - ), - LanguageInfo( - code = "ur-IN", - language = "ur", - voiceName = "ur-IN-GulNeural", - ), - LanguageInfo( - code = "fa-IR", - language = "fa", - voiceName = "fa-IR-DilaraNeural", - ), - LanguageInfo( - code = "sw-KE", - language = "sw", - voiceName = "sw-KE-ZuriNeural", - ), - ) - - private val byCode: Map = - all.associateBy { normalizeCode(it.code) } - - fun findByCode(code: String?): LanguageInfo? { - if (code.isNullOrBlank()) return null - return byCode[normalizeCode(code)] - } - - private fun normalizeCode(value: String): String { - return value.trim().replace('_', '-').lowercase(Locale.ROOT) - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/TranslationForegroundService.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/TranslationForegroundService.kt deleted file mode 100644 index f6711b7e..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/TranslationForegroundService.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.example.asr_tr_tts_manager - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.Service -import android.content.Intent -import android.content.pm.ServiceInfo -import android.os.Build -import android.os.IBinder -import android.os.PowerManager -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC - -class TranslationForegroundService : Service() { - companion object { - private const val CHANNEL_ID = "TranslationServiceChannel" - private const val NOTIFICATION_ID = 2 - } - - private var wakeLock: PowerManager.WakeLock? = null - - override fun onBind(intent: Intent?): IBinder? = null - - override fun onCreate() { - super.onCreate() - createNotificationChannel() - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - val notification = createNotification() - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE) - } else { - startForeground(NOTIFICATION_ID, notification) - } - - // 获取 WakeLock 防止 CPU 休眠 - if (wakeLock == null) { - val pm = getSystemService(POWER_SERVICE) as PowerManager - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "DeapVoice::Translation").apply { - acquire() - } - } - - return START_NOT_STICKY - } - - override fun onDestroy() { - super.onDestroy() - - wakeLock?.let { - if (it.isHeld) it.release() - } - wakeLock = null - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - stopForeground(STOP_FOREGROUND_REMOVE) - } else { - @Suppress("DEPRECATION") - stopForeground(true) - } - } - - private fun createNotificationChannel() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - CHANNEL_ID, - "翻译服务", - NotificationManager.IMPORTANCE_LOW - ) - val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - manager.createNotificationChannel(channel) - } - } - - private fun createNotification(): Notification { - return NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("翻译服务") - .setContentText("翻译进行中...") - .setSmallIcon(android.R.drawable.ic_btn_speak_now) - .setSilent(true) - .setOngoing(true) - .setVisibility(VISIBILITY_PUBLIC) - .build() - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/VolcengineHelper.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/VolcengineHelper.kt deleted file mode 100644 index 60271b17..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/VolcengineHelper.kt +++ /dev/null @@ -1,718 +0,0 @@ -package com.example.asr_tr_tts_manager - -import android.os.Handler -import android.os.Looper -import android.util.Log -import com.example.bluetooth_manager.G722Codec -import okhttp3.* -import okio.ByteString -import okio.ByteString.Companion.toByteString -import java.io.ByteArrayOutputStream -import java.util.UUID -import java.util.concurrent.TimeUnit - -class VolcengineHelper( - languages: List = listOf("zh-CN", "en-US"), -) { - companion object { - private const val TAG = "VolcengineHelper" - private const val WS_URL = "wss://openspeech.bytedance.com/api/v4/ast/v2/translate" - - // TODO: 替换为实际的凭证 - private const val APP_KEY = "8595121680" - private const val ACCESS_KEY = "ERAUYbJQBOv6M0ooFnUwo-tTWs08HWZn" - private const val RESOURCE_ID = "volc.service_type.10053" - - // Event types from events.proto - // Session 级别事件(AST 2.0 使用 Session 而非 Connection) - private const val EVENT_START_SESSION = 100 - private const val EVENT_FINISH_SESSION = 102 - private const val EVENT_SESSION_STARTED = 150 - private const val EVENT_SESSION_CANCELED = 151 - private const val EVENT_SESSION_FINISHED = 152 - private const val EVENT_SESSION_FAILED = 153 - private const val EVENT_TASK_REQUEST = 200 - private const val EVENT_SOURCE_SUBTITLE_START = 650 - private const val EVENT_SOURCE_SUBTITLE_RESPONSE = 651 - private const val EVENT_SOURCE_SUBTITLE_END = 652 - private const val EVENT_TRANSLATION_SUBTITLE_START = 653 - private const val EVENT_TRANSLATION_SUBTITLE_RESPONSE = 654 - private const val EVENT_TRANSLATION_SUBTITLE_END = 655 - private const val EVENT_TTS_SENTENCE_START = 350 - private const val EVENT_TTS_RESPONSE = 352 - private const val EVENT_TTS_SENTENCE_END = 351 - - // G722 编码常量 - private const val FRAME_PCM_BYTES = 640 // 16kHz × 20ms × 2 bytes - - // 重连策略 - private const val MAX_RECONNECT_ATTEMPTS = 3 - private const val RECONNECT_BASE_DELAY_MS = 1000L // 1s, 2s, 4s 指数退避 - - // FinishSession 超时:服务端未响应时兜底关闭 - private const val FINISH_SESSION_TIMEOUT_MS = 5000L - - // 多实例共享同一个 OkHttpClient(线程池和连接池) - private val httpClient = OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(0, TimeUnit.MILLISECONDS) // no read timeout for streaming - .writeTimeout(30, TimeUnit.SECONDS) - .pingInterval(15, TimeUnit.SECONDS) - .build() - } - - @Volatile - private var currentLanguages: List = languages - - @Volatile - var callback: ((Map) -> Unit)? = null - - @Volatile - var onSynthesisEncoded: ((ByteArray) -> Unit)? = null - - @Volatile - private var sessionId = UUID.randomUUID().toString() - - @Volatile - private var connectionId = UUID.randomUUID().toString() - - @Volatile - private var webSocket: WebSocket? = null - - @Volatile - private var isRecognizing = false - - @Volatile - private var isConnected = false - - // 用于上层区分不同翻译段的 ID(不影响协议层 sessionId) - @Volatile - private var segmentId = UUID.randomUUID().toString() - - // 当前识别中的原文(用于 recognizing 事件) - @Volatile - private var currentSourceText = "" - - // 重连相关 - @Volatile - private var reconnectAttempts = 0 - private val reconnectHandler = Handler(Looper.getMainLooper()) - - // sendTaskRequest 预构建的固定 header 部分(event=200 的 tag+varint + audio field 14 的外层 tag) - // 在 audioData 长度已知后,只需拼接 length + payload,避免每次创建 ProtoWriter - // 格式: [event field] [source_audio tag] [source_audio length] [binary_data tag] [binary_data length] [payload] - private val taskRequestHeader: ByteArray = run { - val pw = ProtoWriter() - pw.writeVarint(2, EVENT_TASK_REQUEST) - pw.toByteArray() - } - - // G722 编码相关(通过 audioLock 保护) - private val audioLock = Any() - private var g722Handle: Long = 0L - private var stashBuffer = ByteArray(8192) - private var stashSize = 0 - private val frameBuffer = ByteArray(FRAME_PCM_BYTES) // 复用的帧缓冲区 - - private val wsListener = object : WebSocketListener() { - override fun onOpen(webSocket: WebSocket, response: Response) { - Log.d(TAG, "WebSocket connected") - sendStartSession() - } - - override fun onMessage(webSocket: WebSocket, bytes: ByteString) { - val raw = bytes.toByteArray() - parseResponse(raw) - } - - override fun onMessage(webSocket: WebSocket, text: String) { - Log.d(TAG, "Received text: $text") - } - - override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { - Log.e(TAG, "WebSocket failure: ${t.message}", t) - isConnected = false - // 如果仍在识别中,尝试指数退避重连 - if (isRecognizing && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { - val delay = RECONNECT_BASE_DELAY_MS shl reconnectAttempts - reconnectAttempts++ - Log.d(TAG, "Reconnect attempt $reconnectAttempts after ${delay}ms...") - reconnectHandler.postDelayed({ - if (isRecognizing) connectWebSocket() - }, delay) - } else { - callback?.invoke( - mapOf("type" to "error", "message" to (t.message ?: "connection failed")) - ) - } - } - - override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { - Log.d(TAG, "WebSocket closing: $code $reason") - webSocket.close(code, reason) - } - - override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { - Log.d(TAG, "WebSocket closed: $code $reason") - isConnected = false - } - } - - fun switchProvider(languages: List? = null) { - if (languages != null) { - currentLanguages = languages - } - } - - fun startRecognition() { - if (isRecognizing) return - isRecognizing = true - isConnected = false - reconnectAttempts = 0 - reconnectHandler.removeCallbacksAndMessages(null) - sessionId = UUID.randomUUID().toString() - connectionId = UUID.randomUUID().toString() - segmentId = UUID.randomUUID().toString() - currentSourceText = "" - synchronized(audioLock) { - releaseG722() - stashSize = 0 - } - connectWebSocket() - } - - fun stopRecognition() { - if (!isRecognizing) return - isRecognizing = false - reconnectHandler.removeCallbacksAndMessages(null) - sendFinishSession() - // 等待服务端返回 SESSION_FINISHED/SESSION_CANCELED 后关闭,超时兜底 - reconnectHandler.postDelayed({ - if (webSocket != null) { - Log.w(TAG, "FinishSession timeout, force closing WebSocket") - closeWebSocket() - } - }, FINISH_SESSION_TIMEOUT_MS) - } - - fun pushStreamWrite(bytes: ByteArray) { - if (!isRecognizing || !isConnected) return - sendTaskRequest(bytes) - } - - fun dispose() { - isRecognizing = false - isConnected = false - callback = null - onSynthesisEncoded = null - closeWebSocket() - reconnectHandler.removeCallbacksAndMessages(null) - synchronized(audioLock) { - releaseG722() - } - } - - // ============ WebSocket 连接管理 ============ - - private fun connectWebSocket() { - closeWebSocket() - val request = Request.Builder() - .url(WS_URL) - .header("X-Api-App-Key", APP_KEY) - .header("X-Api-Access-Key", ACCESS_KEY) - .header("X-Api-Resource-Id", RESOURCE_ID) - .build() - webSocket = httpClient.newWebSocket(request, wsListener) - } - - private fun closeWebSocket() { - val ws = webSocket ?: return - webSocket = null - isConnected = false - try { - ws.close(1000, "normal close") - } catch (_: Exception) { - // 连接可能已断开,强制取消 - try { ws.cancel() } catch (_: Exception) {} - } - } - - // ============ 发送 Protobuf 消息 ============ - - /** - * 发送 StartConnection (event=1) 建联请求 - * TranslateRequest 包含 request_meta, event, user, source_audio, target_audio, request - */ - private fun sendStartSession() { - val langs = currentLanguages - if (langs.size < 2) { - Log.e(TAG, "Need at least 2 languages, got: $langs") - isRecognizing = false - isConnected = false - callback?.invoke( - mapOf("type" to "error", "message" to "Need source and target language") - ) - closeWebSocket() - return - } - val sourceCode = langs[0] - val targetCode = langs[1] - val sourceLang = LanguageManager.findByCode(sourceCode)?.language ?: "zh" - val targetLang = LanguageManager.findByCode(targetCode)?.language ?: "en" - - val pw = ProtoWriter() - - // field 1: request_meta (RequestMeta) - val metaPw = ProtoWriter() - metaPw.writeString(2, APP_KEY) // AppKey - metaPw.writeString(4, RESOURCE_ID) // ResourceID - metaPw.writeString(5, connectionId) // ConnectionID - metaPw.writeString(6, sessionId) // SessionID - pw.writeBytes(1, metaPw.toByteArray()) - - // field 2: event (enum Type = StartSession = 100) - pw.writeVarint(2, EVENT_START_SESSION) - - // field 3: user (User) - val userPw = ProtoWriter() - userPw.writeString(1, "deapvoice_android") // uid - userPw.writeString(3, "android") // platform - pw.writeBytes(3, userPw.toByteArray()) - - // field 4: source_audio (Audio) - val srcAudioPw = ProtoWriter() - srcAudioPw.writeString(4, "pcm") // format - srcAudioPw.writeVarint(7, 16000) // rate - srcAudioPw.writeVarint(8, 16) // bits - srcAudioPw.writeVarint(9, 1) // channel - pw.writeBytes(4, srcAudioPw.toByteArray()) - - // field 5: target_audio (Audio) - val tgtAudioPw = ProtoWriter() - tgtAudioPw.writeString(4, "pcm") // format - tgtAudioPw.writeVarint(7, 16000) // rate - tgtAudioPw.writeVarint(8, 16) // bits - tgtAudioPw.writeVarint(9, 1) // channel - pw.writeBytes(5, tgtAudioPw.toByteArray()) - - // field 6: request (ast.ReqParams) - val reqPw = ProtoWriter() - reqPw.writeString(1, "s2s") // mode - reqPw.writeString(2, sourceLang) // source_language - reqPw.writeString(3, targetLang) // target_language - pw.writeBytes(6, reqPw.toByteArray()) - - val data = pw.toByteArray() - webSocket?.send(data.toByteString()) - Log.d(TAG, "Sent StartSession (${data.size} bytes), session=$sessionId, $sourceLang→$targetLang") - } - - /** - * 发送 TaskRequest (event=200) 包含音频数据 - */ - private fun sendTaskRequest(audioData: ByteArray) { - // 手动拼接避免每次创建 ProtoWriter: - // [taskRequestHeader] + field 4 (source_audio) { field 14 (binary_data) = audioData } - val innerSize = 1 + varintSize(audioData.size.toLong()) + audioData.size // tag(14) + len + payload - val outerSize = 1 + varintSize(innerSize.toLong()) + innerSize // tag(4) + len + inner - val buf = ByteArray(taskRequestHeader.size + outerSize) - var pos = 0 - - // 复制预构建的 header(event=200) - System.arraycopy(taskRequestHeader, 0, buf, 0, taskRequestHeader.size) - pos += taskRequestHeader.size - - // field 4: source_audio, wire type 2 - buf[pos++] = ((4 shl 3) or 2).toByte() - pos = writeRawVarintTo(buf, pos, innerSize.toLong()) - - // field 14: binary_data, wire type 2 - buf[pos++] = ((14 shl 3) or 2).toByte() - pos = writeRawVarintTo(buf, pos, audioData.size.toLong()) - - System.arraycopy(audioData, 0, buf, pos, audioData.size) - - val sent = webSocket?.send(buf.toByteString()) ?: false - if (!sent) { - Log.w(TAG, "sendTaskRequest failed, connection may be closed") - isConnected = false - } - } - - private fun varintSize(value: Long): Int { - var v = value - var size = 1 - while (v and 0x7FL.inv() != 0L) { size++; v = v ushr 7 } - return size - } - - private fun writeRawVarintTo(buf: ByteArray, offset: Int, value: Long): Int { - var pos = offset - var v = value - while (v and 0x7FL.inv() != 0L) { - buf[pos++] = ((v.toInt() and 0x7F) or 0x80).toByte() - v = v ushr 7 - } - buf[pos++] = (v.toInt() and 0x7F).toByte() - return pos - } - - /** - * 发送 FinishSession (event=102) 结束会话 - */ - private fun sendFinishSession() { - val pw = ProtoWriter() - pw.writeVarint(2, EVENT_FINISH_SESSION) - val data = pw.toByteArray() - webSocket?.send(data.toByteString()) - Log.d(TAG, "Sent FinishSession") - } - - // ============ 解析 Protobuf 响应 ============ - - /** - * 解析 TranslateResponse protobuf 消息 - */ - private fun parseResponse(data: ByteArray) { - try { - val pr = ProtoReader(data) - var event = 0 - var responseData: ByteArray? = null - var text = "" - var statusCode = 0 - var statusMessage = "" - - while (pr.hasMore()) { - val tag = pr.readTag() - val fieldNumber = tag shr 3 - val wireType = tag and 0x7 - - when (fieldNumber) { - 1 -> { // response_meta (ResponseMeta) - val metaBytes = pr.readLengthDelimited() - val metaPr = ProtoReader(metaBytes) - while (metaPr.hasMore()) { - val metaTag = metaPr.readTag() - val metaField = metaTag shr 3 - val metaWire = metaTag and 0x7 - when (metaField) { - 3 -> statusCode = metaPr.readVarint().toInt() - 4 -> statusMessage = metaPr.readStringField(metaWire) - else -> metaPr.skip(metaWire) - } - } - } - 2 -> event = pr.readVarint().toInt() // event (enum) - 3 -> responseData = pr.readLengthDelimited() // data (bytes) - 4 -> text = pr.readStringField(wireType) // text (string) - else -> pr.skip(wireType) - } - } - - handleEvent(event, text, responseData, statusCode, statusMessage) - } catch (e: Exception) { - Log.e(TAG, "Parse response failed: ${e.message}", e) - } - } - - private fun handleEvent( - event: Int, - text: String, - audioData: ByteArray?, - statusCode: Int, - statusMessage: String - ) { - when (event) { - EVENT_SESSION_STARTED -> { - Log.d(TAG, "Session started") - isConnected = true - reconnectAttempts = 0 - } - - EVENT_SESSION_FAILED -> { - Log.e(TAG, "Session failed: $statusCode $statusMessage") - isConnected = false - reconnectHandler.removeCallbacksAndMessages(null) - closeWebSocket() - callback?.invoke( - mapOf("type" to "error", "message" to "Session failed: $statusCode $statusMessage") - ) - } - - EVENT_SESSION_FINISHED -> { - Log.d(TAG, "Session finished") - isConnected = false - reconnectHandler.removeCallbacksAndMessages(null) - closeWebSocket() - } - - EVENT_SESSION_CANCELED -> { - Log.d(TAG, "Session canceled") - isConnected = false - reconnectHandler.removeCallbacksAndMessages(null) - closeWebSocket() - } - - EVENT_SOURCE_SUBTITLE_START -> { - currentSourceText = "" - } - - EVENT_SOURCE_SUBTITLE_RESPONSE -> { - // 原文识别中(部分结果) - currentSourceText = text - if (text.isNotEmpty()) { - val sourceCode = currentLanguages.firstOrNull() ?: "zh-CN" - callback?.invoke( - mapOf( - "sessionId" to segmentId, - "type" to "recognizing", - "recognizing" to text, - "language" to sourceCode, - ) - ) - } - } - - EVENT_SOURCE_SUBTITLE_END -> { - // 原文识别结束(最终结果) - if (text.isNotEmpty()) { - currentSourceText = text - } - } - - EVENT_TRANSLATION_SUBTITLE_RESPONSE -> { - if (audioData != null && audioData.isNotEmpty()) { - handleAudioData(audioData) - } - } - - EVENT_TRANSLATION_SUBTITLE_END -> { - // 翻译结束时可能还有音频 - if (audioData != null && audioData.isNotEmpty()) { - handleAudioData(audioData) - } - // 刷新 G722 缓冲区中残留的不足一帧的数据 - flushAudioStash() - - // 发出 recognized 事件 - val sourceCode = currentLanguages.firstOrNull() ?: "zh-CN" - val sourceText = currentSourceText - val translatedText = text - if (sourceText.isNotEmpty() || translatedText.isNotEmpty()) { - callback?.invoke( - mapOf( - "sessionId" to segmentId, - "type" to "recognized", - "recognized" to sourceText, - "translated" to translatedText, - "language" to sourceCode, - ) - ) - // 更新 segmentId 用于下一段(不影响协议层 sessionId) - segmentId = UUID.randomUUID().toString() - currentSourceText = "" - } - } - - EVENT_TTS_RESPONSE -> { - // TTS 音频数据 - if (audioData != null && audioData.isNotEmpty()) { - handleAudioData(audioData) - } - } - - EVENT_TTS_SENTENCE_END -> { - // TTS 句子结束,刷新残留音频 - flushAudioStash() - } - - else -> { - if (statusCode != 0 && statusMessage.isNotEmpty()) { - Log.w(TAG, "Event $event with status $statusCode: $statusMessage") - } - } - } - } - - // ============ 音频 G722 编码 ============ - - private fun handleAudioData(pcmData: ByteArray) { - val encodedCb = onSynthesisEncoded ?: return - if (pcmData.isEmpty()) return - - synchronized(audioLock) { - if (g722Handle == 0L) { - g722Handle = G722Codec.init(16000) - } - - val required = stashSize + pcmData.size - if (required > stashBuffer.size) { - var newCap = stashBuffer.size - while (newCap < required) newCap = newCap shl 1 - val newBuf = ByteArray(newCap) - System.arraycopy(stashBuffer, 0, newBuf, 0, stashSize) - stashBuffer = newBuf - } - - System.arraycopy(pcmData, 0, stashBuffer, stashSize, pcmData.size) - stashSize += pcmData.size - - var cursor = 0 - while (stashSize - cursor >= FRAME_PCM_BYTES) { - System.arraycopy(stashBuffer, cursor, frameBuffer, 0, FRAME_PCM_BYTES) - val encoded = try { - G722Codec.encodeSync(g722Handle, frameBuffer) - } catch (_: Exception) { - null - } - if (encoded != null) { - encodedCb.invoke(encoded) - } - cursor += FRAME_PCM_BYTES - } - - if (cursor > 0) { - val remaining = stashSize - cursor - if (remaining > 0) { - System.arraycopy(stashBuffer, cursor, stashBuffer, 0, remaining) - } - stashSize = remaining - } - } - } - - /** - * 刷新 G722 缓冲区中残留的不足一帧的 PCM 数据, - * 用零填充到完整帧后编码发出。 - */ - private fun flushAudioStash() { - val encodedCb = onSynthesisEncoded ?: return - - synchronized(audioLock) { - if (stashSize <= 0) return - if (g722Handle == 0L) return - - // 零填充复用的 frameBuffer - java.util.Arrays.fill(frameBuffer, stashSize, FRAME_PCM_BYTES, 0.toByte()) - System.arraycopy(stashBuffer, 0, frameBuffer, 0, stashSize) - stashSize = 0 - - val encoded = try { - G722Codec.encodeSync(g722Handle, frameBuffer) - } catch (_: Exception) { - null - } - if (encoded != null) { - encodedCb.invoke(encoded) - } - } - } - - private fun releaseG722() { - if (g722Handle != 0L) { - try { - G722Codec.release(g722Handle) - } catch (_: Exception) { - } - g722Handle = 0L - } - } - - // ============ Protobuf 编码工具 ============ - - private class ProtoWriter { - private val buffer = ByteArrayOutputStream() - - fun writeVarint(fieldNumber: Int, value: Int) { - writeVarint(fieldNumber, value.toLong()) - } - - fun writeVarint(fieldNumber: Int, value: Long) { - writeRawVarint((fieldNumber shl 3).toLong()) // wire type 0 - writeRawVarint(value) - } - - fun writeString(fieldNumber: Int, value: String) { - if (value.isEmpty()) return // proto3 默认值不写入 - val bytes = value.toByteArray(Charsets.UTF_8) - writeRawVarint(((fieldNumber shl 3) or 2).toLong()) // wire type 2 - writeRawVarint(bytes.size.toLong()) - buffer.write(bytes) - } - - fun writeBytes(fieldNumber: Int, value: ByteArray) { - if (value.isEmpty()) return - writeRawVarint(((fieldNumber shl 3) or 2).toLong()) // wire type 2 - writeRawVarint(value.size.toLong()) - buffer.write(value) - } - - private fun writeRawVarint(value: Long) { - var v = value - while (v and 0x7FL.inv() != 0L) { - buffer.write(((v.toInt() and 0x7F) or 0x80)) - v = v ushr 7 - } - buffer.write(v.toInt() and 0x7F) - } - - fun toByteArray(): ByteArray = buffer.toByteArray() - } - - // ============ Protobuf 解码工具 ============ - - private class ProtoReader(private val data: ByteArray) { - private var pos = 0 - - fun hasMore(): Boolean = pos < data.size - - fun readTag(): Int = readRawVarint().toInt() - - fun readVarint(): Long = readRawVarint() - - fun readLengthDelimited(): ByteArray { - val length = readRawVarint().toInt() - if (length < 0 || pos + length > data.size) { - throw IllegalStateException("Invalid length: $length at pos $pos, data size ${data.size}") - } - val result = data.copyOfRange(pos, pos + length) - pos += length - return result - } - - fun readStringField(wireType: Int): String { - return if (wireType == 2) { - String(readLengthDelimited(), Charsets.UTF_8) - } else { - readRawVarint() // skip varint - "" - } - } - - fun skip(wireType: Int) { - when (wireType) { - 0 -> readRawVarint() // varint - 1 -> pos += 8 // 64-bit - 2 -> { // length-delimited - val len = readRawVarint().toInt() - pos += len - } - 5 -> pos += 4 // 32-bit - else -> throw IllegalStateException("Unknown wire type: $wireType") - } - } - - private fun readRawVarint(): Long { - var result = 0L - var shift = 0 - while (pos < data.size) { - val b = data[pos++].toInt() and 0xFF - result = result or ((b.toLong() and 0x7F) shl shift) - if (b and 0x80 == 0) break - shift += 7 - if (shift >= 64) throw IllegalStateException("Varint too long") - } - return result - } - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/AsrInterface.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/AsrInterface.kt deleted file mode 100644 index 91451934..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/AsrInterface.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.asr_tr_tts_manager.asr - -enum class AsrProvider { - AZURE, - VOLCENGINE, -} - -data class AsrResult(val text: String, val language: String) - -interface AsrInterface { - var onRecognizing: ((AsrResult) -> Unit)? - var onRecognized: (suspend (AsrResult) -> Unit)? - - fun startRecognition() - fun stopRecognition() - fun pushStreamWrite(bytes: ByteArray) - fun pushStreamClose() - fun dispose() -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/AsrManager.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/AsrManager.kt deleted file mode 100644 index 17147439..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/AsrManager.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.asr_tr_tts_manager.asr - -class AsrManager( - provider: AsrProvider = AsrProvider.AZURE, - languages: List = listOf("zh-CN", "en-US"), - private var autoDetectLanguage: Boolean = false, -) : AsrInterface { - private var currentProvider: AsrProvider = provider - private var currentLanguages: List = languages - private var delegate: AsrInterface? = null - - override var onRecognizing: ((AsrResult) -> Unit)? = null - set(value) { - field = value - delegate?.onRecognizing = value - } - - override var onRecognized: (suspend (AsrResult) -> Unit)? = null - set(value) { - field = value - delegate?.onRecognized = value - } - - init { - switchProvider() - } - - fun switchProvider(provider: AsrProvider = currentProvider, languages: List = currentLanguages, autoDetectLanguage: Boolean = this.autoDetectLanguage) { - try { - delegate?.dispose() - } catch (_: Exception) { - } - - currentProvider = provider - currentLanguages = languages - this.autoDetectLanguage = autoDetectLanguage - - delegate = - when (provider) { - AsrProvider.AZURE -> AzureAsr(languages, autoDetectLanguage) - AsrProvider.VOLCENGINE -> VolcengineAsr(languages, autoDetectLanguage) - }.also { - it.onRecognizing = onRecognizing - it.onRecognized = onRecognized - } - } - - fun getProvider(): AsrProvider = currentProvider - - override fun startRecognition() { - delegate?.startRecognition() - } - - override fun stopRecognition() { - delegate?.stopRecognition() - } - - override fun pushStreamWrite(bytes: ByteArray) { - delegate?.pushStreamWrite(bytes) - } - - override fun pushStreamClose() { - delegate?.pushStreamClose() - } - - override fun dispose() { - onRecognizing = null - onRecognized = null - - val d = delegate - delegate = null - d?.dispose() - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/AzureAsr.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/AzureAsr.kt deleted file mode 100644 index 9fd1331e..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/AzureAsr.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.example.asr_tr_tts_manager.asr - -import android.util.Log -import com.microsoft.cognitiveservices.speech.* -import com.microsoft.cognitiveservices.speech.audio.* -import kotlinx.coroutines.launch - -class AzureAsr( - private val languages: List = listOf("zh-CN", "en-US"), - private val autoDetectLanguage: Boolean = false, -) : AsrInterface { - override var onRecognizing: ((AsrResult) -> Unit)? = null - override var onRecognized: (suspend (AsrResult) -> Unit)? = null - - private var pushStream: PushAudioInputStream? = null - private var recognizer: SpeechRecognizer? = null - private val SUBSCRIPTION_KEY: String = - "rLPdyGgD1v7z7khzpdrd4YNWUinjw6GzAbA1pPXBRmUjitfjDxdsJQQJ99BGAC3pKaRXJ3w3AAAYACOGAHZZ" - private val REGION: String = "eastasia" - - private val job = kotlinx.coroutines.SupervisorJob() - private val coroutineScope = - kotlinx.coroutines.CoroutineScope(job + kotlinx.coroutines.Dispatchers.IO) - - init { - val audioFormat = AudioStreamFormat.getWaveFormatPCM(16000L, 16, 1) - pushStream = AudioInputStream.createPushStream(audioFormat) - - val audioConfig = AudioConfig.fromStreamInput(pushStream) - val speechConfig = SpeechConfig.fromSubscription(SUBSCRIPTION_KEY, REGION) - - if (autoDetectLanguage) { - speechConfig.setProperty(PropertyId.SpeechServiceConnection_LanguageIdMode, "Continuous") - val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(languages) - recognizer = SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig) - } else { - speechConfig.speechRecognitionLanguage = languages.first() - recognizer = SpeechRecognizer(speechConfig, audioConfig) - } - - recognizer?.recognizing?.addEventListener { _, event -> - val text = event.result.text.trim() - if (text.isEmpty()) return@addEventListener - - val language = if (autoDetectLanguage) { - AutoDetectSourceLanguageResult.fromResult(event.result).language ?: "" - } else { - languages.first() - } - onRecognizing?.invoke(AsrResult(text, language)) - } - - recognizer?.recognized?.addEventListener { _, event -> - if (event.result.reason != ResultReason.RecognizedSpeech) return@addEventListener - - val text = event.result.text.trim() - val language = if (autoDetectLanguage) { - AutoDetectSourceLanguageResult.fromResult(event.result).language ?: "" - } else { - languages.first() - } - coroutineScope.launch { - onRecognized?.invoke(AsrResult(text, language)) - } - } - } - - override fun startRecognition() { - recognizer?.startContinuousRecognitionAsync()?.get() - } - - override fun stopRecognition() { - recognizer?.stopContinuousRecognitionAsync()?.get() - } - - override fun pushStreamWrite(bytes: ByteArray) { - pushStream?.write(bytes) - } - - override fun pushStreamClose() { - pushStream?.close() - } - - override fun dispose() { - try { - recognizer?.stopContinuousRecognitionAsync()?.get() - } catch (e: Exception) { - Log.w("AzureAsr", "stop failed: ${e.message}") - } finally { - pushStream?.close() - recognizer?.close() - job.cancel() - } - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/VolcengineAsr.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/VolcengineAsr.kt deleted file mode 100644 index 8991b480..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/asr/VolcengineAsr.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.example.asr_tr_tts_manager.asr - -import android.util.Log -import com.microsoft.cognitiveservices.speech.* -import com.microsoft.cognitiveservices.speech.audio.* -import kotlinx.coroutines.launch - -class VolcengineAsr( - private val languages: List = listOf("zh-CN", "en-US"), - private val autoDetectLanguage: Boolean = false, -) : AsrInterface { - override var onRecognizing: ((AsrResult) -> Unit)? = null - override var onRecognized: (suspend (AsrResult) -> Unit)? = null - - private var pushStream: PushAudioInputStream? = null - private var recognizer: SpeechRecognizer? = null - private val SUBSCRIPTION_KEY: String = - "rLPdyGgD1v7z7khzpdrd4YNWUinjw6GzAbA1pPXBRmUjitfjDxdsJQQJ99BGAC3pKaRXJ3w3AAAYACOGAHZZ" - private val REGION: String = "eastasia" - - private val job = kotlinx.coroutines.SupervisorJob() - private val coroutineScope = - kotlinx.coroutines.CoroutineScope(job + kotlinx.coroutines.Dispatchers.IO) - - init { - val audioFormat = AudioStreamFormat.getWaveFormatPCM(16000L, 16, 1) - pushStream = AudioInputStream.createPushStream(audioFormat) - - val audioConfig = AudioConfig.fromStreamInput(pushStream) - val speechConfig = SpeechConfig.fromSubscription(SUBSCRIPTION_KEY, REGION) - - if (autoDetectLanguage) { - val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(languages) - recognizer = SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig) - } else { - speechConfig.speechRecognitionLanguage = languages.first() - recognizer = SpeechRecognizer(speechConfig, audioConfig) - } - - recognizer?.recognizing?.addEventListener { _, event -> - val text = event.result.text.trim() - if (text.isEmpty()) return@addEventListener - - val language = if (autoDetectLanguage) { - AutoDetectSourceLanguageResult.fromResult(event.result).language ?: "" - } else { - languages.first() - } - onRecognizing?.invoke(AsrResult(text, language)) - } - - recognizer?.recognized?.addEventListener { _, event -> - if (event.result.reason != ResultReason.RecognizedSpeech) return@addEventListener - - val text = event.result.text.trim() - val language = if (autoDetectLanguage) { - AutoDetectSourceLanguageResult.fromResult(event.result).language ?: "" - } else { - languages.first() - } - coroutineScope.launch { - onRecognized?.invoke(AsrResult(text, language)) - } - } - } - - override fun startRecognition() { - recognizer?.startContinuousRecognitionAsync()?.get() - } - - override fun stopRecognition() { - recognizer?.stopContinuousRecognitionAsync()?.get() - } - - override fun pushStreamWrite(bytes: ByteArray) { - pushStream?.write(bytes) - } - - override fun pushStreamClose() { - pushStream?.close() - } - - override fun dispose() { - try { - recognizer?.stopContinuousRecognitionAsync()?.get() - } catch (e: Exception) { - Log.w("VolcengineAsr", "stop failed: ${e.message}") - } finally { - pushStream?.close() - recognizer?.close() - job.cancel() - } - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/recorder/AstStereoRecorder.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/recorder/AstStereoRecorder.kt deleted file mode 100644 index 1099688e..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/recorder/AstStereoRecorder.kt +++ /dev/null @@ -1,170 +0,0 @@ -package com.example.asr_tr_tts_manager.recorder - -/** - * 通话翻译模式立体声录音器 - * - * 由本端麦克风 PCM(来自 BLE 输入的左声道)作为时钟,按 16k/16bit 的节奏 - * 持续写入录音文件;对方翻译后的合成 PCM(B 路 AST 输出)异步排入右声道 FIFO, - * 与左声道按相同样本数交错合成立体声后写出。 - * - * 输出格式:16 kHz / 16-bit PCM / 2 channels WAV。 - */ -class AstStereoRecorder { - - private val lock = Any() - private var recordFile: RecordFile? = null - private val rightFifo = ArrayDeque() - - @Volatile - private var running = false - - @Volatile - private var paused = false - - /** - * 调用点用的轻量级开关:调用方可在不加锁的情况下快速判定是否需要把音频灌进录音器。 - * 在 start/pause/resume/stop 中同步更新;接受 start/stop 瞬间一两帧的偏差。 - */ - val isActive: Boolean - get() = running && !paused - - /** - * 开始录音 - * @param filePath 输出文件路径(.wav) - */ - fun start(filePath: String): Boolean { - synchronized(lock) { - // 已在录音,先停掉旧的 - recordFile?.closeFile(false) - recordFile = null - rightFifo.clear() - paused = false - - val file = RecordFile() - file.setAudioConfig(16000, 2) - file.creatingFiles(filePath) - recordFile = file - running = true - return true - } - } - - fun pause(): Boolean { - paused = true - return true - } - - fun resume(): Boolean { - paused = false - return true - } - - fun stop(isSave: Boolean): Boolean { - synchronized(lock) { - val file = recordFile ?: return false - running = false - paused = false - val ok = file.closeFile(isSave) - recordFile = null - rightFifo.clear() - return ok - } - } - - /** - * 排入右声道(B 路对方原音 / AST 合成出的对方翻译 PCM) - * @param pcm 16 kHz / 16-bit PCM mono - */ - fun enqueueRightChannel(pcm: ByteArray) { - if (pcm.isEmpty()) return - synchronized(lock) { - if (!running || paused) return - for (b in pcm) { - rightFifo.addLast(b) - } - // 左声道(时钟)长时间缺席时右声道 FIFO 会无限增长;超出上限丢弃最旧字节,避免 OOM - while (rightFifo.size > MAX_RIGHT_FIFO_BYTES) { - rightFifo.removeFirst() - } - } - } - - /** - * 写入一帧右声道(对方/远端 PCM),与 enqueueRightChannel 等价的别名, - * 让调用方按 writeLeftChannel / writeRightChannel 成对调用更直观。 - * @param pcm 16 kHz / 16-bit PCM mono - */ - fun writeRightChannel(pcm: ByteArray) { - enqueueRightChannel(pcm) - } - - /** - * 写入一帧左声道(本端麦克风 PCM)并消费等量右声道字节,交错后落盘 - * @param mono 16 kHz / 16-bit PCM mono(来自 BLE 输入解出的 leftBuffer) - */ - fun writeLeftChannel(mono: ByteArray) { - if (mono.isEmpty()) return - // 左声道字节数必须是偶数(16-bit 对齐) - val leftBytes = mono.size - (mono.size % 2) - if (leftBytes == 0) return - - val stereo: ByteArray - val fileToWrite: RecordFile? - synchronized(lock) { - if (!running || paused) return - val file = recordFile ?: return - - val right = ByteArray(leftBytes) - val popCount = minOf(rightFifo.size, leftBytes) - for (i in 0 until popCount) { - right[i] = rightFifo.removeFirst() - } - // 不足部分 right 数组初始为 0(静音填充) - - stereo = ByteArray(leftBytes * 2) - val sampleCount = leftBytes / 2 - for (i in 0 until sampleCount) { - val srcIdx = i * 2 - val dstIdx = i * 4 - // 左声道:本端麦克风原音,应用 LEFT_GAIN 拉响 - val lOutU = gainSoftClip(mono[srcIdx], mono[srcIdx + 1], LEFT_GAIN) - stereo[dstIdx] = (lOutU and 0xFF).toByte() - stereo[dstIdx + 1] = ((lOutU shr 8) and 0xFF).toByte() - // 右声道:对方原音 / B 路 TTS 合成音,应用 RIGHT_GAIN 压低 - val rOutU = gainSoftClip(right[srcIdx], right[srcIdx + 1], RIGHT_GAIN) - stereo[dstIdx + 2] = (rOutU and 0xFF).toByte() - stereo[dstIdx + 3] = ((rOutU shr 8) and 0xFF).toByte() - } - fileToWrite = file - } - - fileToWrite?.saveAudioDataToWav(stereo) - } - - /** - * 对一个 16-bit little-endian PCM 样本施加线性增益并 tanh 软削波。 - * 返回 [0, 0xFFFF] 区间的 Int(即 UInt16 的 bit pattern),交给调用方拆成两个字节。 - * gain==1.0 时短路返回原始 bit pattern,避免无谓浮点。 - */ - private fun gainSoftClip(lo: Byte, hi: Byte, gain: Float): Int { - val unsigned = (lo.toInt() and 0xFF) or ((hi.toInt() and 0xFF) shl 8) - if (gain == 1.0f) return unsigned - val s = unsigned.toShort().toInt() // 还原为带符号 Int16 - val x = (s.toFloat() / 32767.0f) * gain - val y = Math.tanh(x.toDouble()).toFloat() - var v = y * 32767.0f - if (v > 32767.0f) v = 32767.0f - if (v < -32768.0f) v = -32768.0f - return v.toInt() and 0xFFFF - } - - companion object { - // 通话翻译录音中麦克风原音偏小、对方偏响,对两路分别做线性增益再 tanh 软削波。 - // 左(麦克风) +12dB ≈ 3.98x,右(对方) -4dB ≈ 0.63x,与 iOS 端 AstStereoRecorder 保持一致。 - private const val LEFT_GAIN = 3.98f - private const val RIGHT_GAIN = 0.63f - - // 右声道 FIFO 上限(约 5 秒 @16kHz/16bit 单声道),防止无限增长导致 OOM - private const val MAX_RIGHT_FIFO_BYTES = 16000 * 2 * 5 - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/recorder/RecordFile.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/recorder/RecordFile.kt deleted file mode 100644 index f887cc25..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/recorder/RecordFile.kt +++ /dev/null @@ -1,151 +0,0 @@ -package com.example.asr_tr_tts_manager.recorder - -import android.util.Log -import java.io.File -import java.io.FileOutputStream -import java.io.RandomAccessFile -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean - -/** - * WAV 录音文件写入器(16-bit PCM,单/双声道可配)。 - * - * 流程:creatingFiles() 先写占位 WAV 头并启动后台写线程;saveAudioDataToWav() - * 把每帧 PCM 入队由后台线程追加到文件尾;closeFile() 停线程并回填真实长度, - * 得到合法 WAV(isSave=false 或只有头部则删除文件)。 - * - * 单声道:同声/设备辅助翻译直接使用。双声道:通话翻译由 AstStereoRecorder - * 先把左右声道交织成立体声帧后再调本类写入。 - */ -class RecordFile { - private var currentAudioFile: File? = null - private var fos: FileOutputStream? = null - - // 后台异步写入,避免在音频实时回调线程上做文件 I/O - private val writeQueue = LinkedBlockingQueue() - private val isWriting = AtomicBoolean(false) - private var writeThread: Thread? = null - - private var totalBytesWritten = 0 - - private var sampleRate = 16000 - private var channels = 1 // 1=单声道, 2=立体声 - - // 是否接收新帧。closeFile 时先置 false:保证写队列有限(join() 不会永久阻塞), - // 且关闭期间/之后回调再写入会被直接丢弃(防 use-after-close 与头长度错乱) - @Volatile - private var accepting = false - - fun setAudioConfig(sampleRate: Int, channels: Int) { - this.sampleRate = sampleRate - this.channels = channels - } - - fun creatingFiles(filePath: String) { - if (fos != null || currentAudioFile != null) return - - val file = File(filePath) - file.parentFile?.let { if (!it.exists()) it.mkdirs() } - currentAudioFile = file - file.createNewFile() - - // 先写一个 dataLength=0 的占位 WAV 头 - FileOutputStream(file).use { it.write(generateWavHeader(0)) } - - fos = FileOutputStream(file, true) - totalBytesWritten = 0 - startWriteThread() - accepting = true - } - - fun saveAudioDataToWav(buffer: ByteArray) { - if (!accepting) return - writeQueue.offer(buffer.copyOf()) - totalBytesWritten += buffer.size - } - - fun closeFile(isSave: Boolean): Boolean { - accepting = false // 先停止接收新帧,使写队列有限,join() 不会永久阻塞 - try { - if (fos == null || currentAudioFile == null) return false - isWriting.set(false) - // accepting=false 后队列有限,写线程通常毫秒级排空退出; - // 5s 兜底:极端 I/O 卡死时也不会永久阻塞调用线程 - writeThread?.join(5000) - fos?.close() - - val file = currentAudioFile ?: return false - return if (file.length() <= 44 || !isSave) { - file.delete() - false - } else { - // 回填真实的 RIFF/data 长度 - RandomAccessFile(file, "rw").use { raf -> - raf.seek(0) - raf.write(generateWavHeader(totalBytesWritten)) - } - true - } - } catch (e: Exception) { - Log.e(TAG, "关闭/回填 WAV 失败: ${e.message}") - return false - } finally { - fos = null - currentAudioFile = null - writeThread = null - totalBytesWritten = 0 - } - } - - private fun startWriteThread() { - if (isWriting.get()) return - isWriting.set(true) - writeThread = Thread { - try { - while (isWriting.get() || writeQueue.isNotEmpty()) { - // 阻塞等待最多 100ms,避免无超时 poll 的忙等空转 - val data = writeQueue.poll(100, TimeUnit.MILLISECONDS) ?: continue - fos?.write(data) - } - } catch (e: InterruptedException) { - // 收尾时被中断,正常退出 - } catch (e: Exception) { - Log.e(TAG, "异步写入音频失败: ${e.message}") - } - } - writeThread?.start() - } - - // 标准 16-bit PCM WAV 头(44 字节,little-endian) - private fun generateWavHeader(dataLength: Int): ByteArray { - val totalLength = 36 + dataLength - val bytesPerSample = 2 - val byteRate = sampleRate * channels * bytesPerSample - val blockAlign = channels * bytesPerSample - - return byteArrayOf( - 'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(), - (totalLength and 0xFF).toByte(), ((totalLength shr 8) and 0xFF).toByte(), - ((totalLength shr 16) and 0xFF).toByte(), ((totalLength shr 24) and 0xFF).toByte(), - 'W'.code.toByte(), 'A'.code.toByte(), 'V'.code.toByte(), 'E'.code.toByte(), - 'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(), - 16, 0, 0, 0, // PCM 头长度 - 1, 0, // PCM 格式 - (channels and 0xFF).toByte(), ((channels shr 8) and 0xFF).toByte(), - (sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), - ((sampleRate shr 16) and 0xFF).toByte(), ((sampleRate shr 24) and 0xFF).toByte(), - (byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), - ((byteRate shr 16) and 0xFF).toByte(), ((byteRate shr 24) and 0xFF).toByte(), - (blockAlign and 0xFF).toByte(), ((blockAlign shr 8) and 0xFF).toByte(), - 16, 0, // 样本位数 - 'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(), - (dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(), - ((dataLength shr 16) and 0xFF).toByte(), ((dataLength shr 24) and 0xFF).toByte() - ) - } - - companion object { - private const val TAG = "RecordFile" - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/AzureTranslation.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/AzureTranslation.kt deleted file mode 100644 index 9b64adb6..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/AzureTranslation.kt +++ /dev/null @@ -1,306 +0,0 @@ -package com.example.asr_tr_tts_manager.translation - -import kotlinx.coroutines.* -import java.util.concurrent.TimeUnit -import okhttp3.* -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.MediaType.Companion.toMediaType -import org.json.JSONObject -import org.json.JSONArray -import java.net.URLEncoder -import java.security.MessageDigest -import java.text.SimpleDateFormat -import java.util.* -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec - -class AzureTranslation : TranslationInterface { - private val BASE_URL = "https://translate.volcengineapi.com" - private val ENDPOINT = "/" - private val SERVICE = "translate" - private val VERSION = "2020-06-01" - private val ACTION = "TranslateText" - private val ALGORITHM = "HMAC-SHA256" - - private var accessKey: String = "AKLTZjJmODdkODQwN2RjNDcyMWI4NWM2ZWRmNDkxY2RiYzI" - private var secretKey: String = "T0Roa09EZGlNalZpTjJObE5HVmlZMkV3WVRkbVpUTTBORGMzTW1FNFlqYw==" - private var region: String = "cn-north-1" - private var maxRetryAttempts: Int = 3 - private var timeout: Long = 10000L - - private val httpClient = OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .build() - - override suspend fun translateText( - text: String, - sourceLanguage: String, - targetLanguage: String - ): TranslationResult { - return withContext(Dispatchers.IO) { - performTranslationWithRetry(text, sourceLanguage, targetLanguage) - } - } - - private suspend fun performTranslationWithRetry( - text: String, - sourceLanguage: String, - targetLanguage: String - ): TranslationResult { - var lastException: Exception? = null - - repeat(maxRetryAttempts) { attempt -> - try { - val result = performTranslation(text, sourceLanguage, targetLanguage) - if (result.success) { - return result - } - lastException = Exception(result.error) - } catch (e: Exception) { - lastException = e - - // 重试延迟 - if (attempt < maxRetryAttempts - 1) { - delay(500L * (attempt + 1)) - } - } - } - - return TranslationResult( - success = false, - error = "翻译失败: ${lastException?.message ?: "未知错误"}" - ) - } - - private suspend fun performTranslation( - text: String, - sourceLanguage: String, - targetLanguage: String - ): TranslationResult { - try { - // 构建请求体 - val requestBody = JSONObject().apply { - put("SourceLanguage", sourceLanguage) - put("TargetLanguage", targetLanguage) - put("TextList", JSONArray().put(text)) - } - - // 查询参数 - val queryParams = mapOf( - "Action" to ACTION, - "Version" to VERSION, - "Region" to region, - "Service" to SERVICE - ) - - // 生成签名 - val headers = generateSignature("POST", requestBody, queryParams) - - // 构建URL - val urlBuilder = BASE_URL.toHttpUrl().newBuilder() - queryParams.forEach { (key, value) -> - urlBuilder.addQueryParameter(key, value) - } - val url = urlBuilder.build() - - // 构建请求 - val requestBodyObj = RequestBody.create( - "application/json; charset=utf-8".toMediaType(), - requestBody.toString() - ) - - val request = Request.Builder() - .url(url) - .post(requestBodyObj) - .apply { - headers.forEach { (key, value) -> - addHeader(key, value) - } - } - .build() - - // 发送请求 - val response = httpClient.newCall(request).execute() - - if (response.isSuccessful) { - val responseBody = response.body?.string() - if (responseBody != null) { - val jsonResponse = JSONObject(responseBody) - val translation = extractTranslation(jsonResponse) - - if (translation != null) { - return TranslationResult( - success = true, - translatedText = translation, - confidence = 0.9f - ) - } else { - val error = extractError(jsonResponse) - return TranslationResult( - success = false, - error = error ?: "翻译结果为空" - ) - } - } - } - - return TranslationResult( - success = false, - error = "HTTP错误: ${response.code}" - ) - - } catch (e: Exception) { - return TranslationResult( - success = false, - error = "请求异常: ${e.message}" - ) - } - } - - private fun extractTranslation(jsonResponse: JSONObject): String? { - try { - // 1. 标准响应结构 - if (jsonResponse.has("TranslationList")) { - val translationList = jsonResponse.getJSONArray("TranslationList") - if (translationList.length() > 0) { - val translationItem = translationList.getJSONObject(0) - if (translationItem.has("Translation")) { - return translationItem.getString("Translation") - } - } - } - - // 2. 其他可能的响应结构 - if (jsonResponse.has("Translation")) { - return jsonResponse.getString("Translation") - } - - if (jsonResponse.has("Result")) { - val result = jsonResponse.getJSONObject("Result") - if (result.has("Translation")) { - return result.getString("Translation") - } - } - - if (jsonResponse.has("Data")) { - val data = jsonResponse.getJSONObject("Data") - if (data.has("Translation")) { - return data.getString("Translation") - } - if (data.has("TranslationList")) { - val translationList = data.getJSONArray("TranslationList") - if (translationList.length() > 0) { - val translationItem = translationList.getJSONObject(0) - if (translationItem.has("Translation")) { - return translationItem.getString("Translation") - } - } - } - } - } catch (_: Exception) { - - } - - return null - } - - private fun extractError(jsonResponse: JSONObject): String? { - try { - if (jsonResponse.has("ResponseMetadata")) { - val metadata = jsonResponse.getJSONObject("ResponseMetadata") - if (metadata.has("Error")) { - val error = metadata.getJSONObject("Error") - return error.optString("Message", error.optString("Code", "未知错误")) - } - } - - if (jsonResponse.has("Error")) { - val error = jsonResponse.getJSONObject("Error") - return error.optString("Message", error.optString("Code", "未知错误")) - } - } catch (_: Exception) { - } - - return null - } - - private fun generateSignature( - method: String, - requestBody: JSONObject, - queryParams: Map - ): Map { - try { - // 1. 准备时间相关参数 - val now = Date() - val dateFormat = SimpleDateFormat("yyyyMMdd", Locale.US).apply { - timeZone = TimeZone.getTimeZone("UTC") - } - val timestampFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US).apply { - timeZone = TimeZone.getTimeZone("UTC") - } - - val date = dateFormat.format(now) - val timestamp = timestampFormat.format(now) - - // 2. 构建规范查询字符串 - val sortedParams = queryParams.toSortedMap() - val canonicalQueryString = sortedParams.map { (key, value) -> - "${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}" - }.joinToString("&") - - // 3. 创建规范请求 - val contentType = "application/json" - val payloadHash = sha256(requestBody.toString()) - val host = "translate.volcengineapi.com" - - val canonicalHeaders = "host:$host\nx-date:$timestamp\n" - val signedHeaders = "host;x-date" - - val canonicalRequest = - "$method\n$ENDPOINT\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash" - - // 4. 创建待签字符串 - val credentialScope = "$date/$region/$SERVICE/request" - val stringToSign = - "$ALGORITHM\n$timestamp\n$credentialScope\n${sha256(canonicalRequest)}" - - // 5. 计算签名 - val kSecret = secretKey.toByteArray(Charsets.UTF_8) - val kDate = hmacSha256(kSecret, date) - val kRegion = hmacSha256(kDate, region) - val kService = hmacSha256(kRegion, SERVICE) - val kSigning = hmacSha256(kService, "request") - val signature = - hmacSha256(kSigning, stringToSign).joinToString("") { "%02x".format(it) } - - // 6. 构建授权头 - val authorization = - "$ALGORITHM Credential=$accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature" - - return mapOf( - "Content-Type" to contentType, - "X-Date" to timestamp, - "Authorization" to authorization, - "Host" to host - ) - - } catch (e: Exception) { - throw e - } - } - - private fun sha256(input: String): String { - val digest = MessageDigest.getInstance("SHA-256") - val hash = digest.digest(input.toByteArray(Charsets.UTF_8)) - return hash.joinToString("") { "%02x".format(it) } - } - - private fun hmacSha256(key: ByteArray, data: String): ByteArray { - val mac = Mac.getInstance("HmacSHA256") - val secretKeySpec = SecretKeySpec(key, "HmacSHA256") - mac.init(secretKeySpec) - return mac.doFinal(data.toByteArray(Charsets.UTF_8)) - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/TranslationInterface.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/TranslationInterface.kt deleted file mode 100644 index e8842d50..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/TranslationInterface.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.asr_tr_tts_manager.translation - -enum class TranslationProvider { - AZURE, - VOLCENGINE, -} - -data class TranslationResult( - val success: Boolean, - val translatedText: String? = null, - val error: String? = null, - val confidence: Float = 0f -) - -interface TranslationInterface { - suspend fun translateText(text: String, sourceLanguage: String, targetLanguage: String): TranslationResult -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/TranslationManager.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/TranslationManager.kt deleted file mode 100644 index 6a2095a2..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/TranslationManager.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.asr_tr_tts_manager.translation - -class TranslationManager( - provider: TranslationProvider = TranslationProvider.AZURE, -) : TranslationInterface { - - private var currentProvider: TranslationProvider = provider - private var delegate: TranslationInterface? = null - - init { - switchProvider() - } - - fun switchProvider(provider: TranslationProvider = currentProvider) { - currentProvider = provider - - delegate = - when (provider) { - TranslationProvider.AZURE -> AzureTranslation() - TranslationProvider.VOLCENGINE -> VolcengineTranslation() - } - } - - fun getProvider(): TranslationProvider = currentProvider - - override suspend fun translateText( - text: String, - sourceLanguage: String, - targetLanguage: String, - ): TranslationResult { - return delegate?.translateText(text, sourceLanguage, targetLanguage) - ?: TranslationResult(success = false, error = "TranslationManager not initialized") - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/VolcengineTranslation.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/VolcengineTranslation.kt deleted file mode 100644 index eac18ba3..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation/VolcengineTranslation.kt +++ /dev/null @@ -1,306 +0,0 @@ -package com.example.asr_tr_tts_manager.translation - -import kotlinx.coroutines.* -import java.util.concurrent.TimeUnit -import okhttp3.* -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.MediaType.Companion.toMediaType -import org.json.JSONObject -import org.json.JSONArray -import java.net.URLEncoder -import java.security.MessageDigest -import java.text.SimpleDateFormat -import java.util.* -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec - -class VolcengineTranslation : TranslationInterface { - private val BASE_URL = "https://translate.volcengineapi.com" - private val ENDPOINT = "/" - private val SERVICE = "translate" - private val VERSION = "2020-06-01" - private val ACTION = "TranslateText" - private val ALGORITHM = "HMAC-SHA256" - - private var accessKey: String = "AKLTZjJmODdkODQwN2RjNDcyMWI4NWM2ZWRmNDkxY2RiYzI" - private var secretKey: String = "T0Roa09EZGlNalZpTjJObE5HVmlZMkV3WVRkbVpUTTBORGMzTW1FNFlqYw==" - private var region: String = "cn-north-1" - private var maxRetryAttempts: Int = 3 - private var timeout: Long = 10000L - - private val httpClient = OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .build() - - override suspend fun translateText( - text: String, - sourceLanguage: String, - targetLanguage: String - ): TranslationResult { - return withContext(Dispatchers.IO) { - performTranslationWithRetry(text, sourceLanguage, targetLanguage) - } - } - - private suspend fun performTranslationWithRetry( - text: String, - sourceLanguage: String, - targetLanguage: String - ): TranslationResult { - var lastException: Exception? = null - - repeat(maxRetryAttempts) { attempt -> - try { - val result = performTranslation(text, sourceLanguage, targetLanguage) - if (result.success) { - return result - } - lastException = Exception(result.error) - } catch (e: Exception) { - lastException = e - - // 重试延迟 - if (attempt < maxRetryAttempts - 1) { - delay(500L * (attempt + 1)) - } - } - } - - return TranslationResult( - success = false, - error = "翻译失败: ${lastException?.message ?: "未知错误"}" - ) - } - - private suspend fun performTranslation( - text: String, - sourceLanguage: String, - targetLanguage: String - ): TranslationResult { - try { - // 构建请求体 - val requestBody = JSONObject().apply { - put("SourceLanguage", sourceLanguage) - put("TargetLanguage", targetLanguage) - put("TextList", JSONArray().put(text)) - } - - // 查询参数 - val queryParams = mapOf( - "Action" to ACTION, - "Version" to VERSION, - "Region" to region, - "Service" to SERVICE - ) - - // 生成签名 - val headers = generateSignature("POST", requestBody, queryParams) - - // 构建URL - val urlBuilder = BASE_URL.toHttpUrl().newBuilder() - queryParams.forEach { (key, value) -> - urlBuilder.addQueryParameter(key, value) - } - val url = urlBuilder.build() - - // 构建请求 - val requestBodyObj = RequestBody.create( - "application/json; charset=utf-8".toMediaType(), - requestBody.toString() - ) - - val request = Request.Builder() - .url(url) - .post(requestBodyObj) - .apply { - headers.forEach { (key, value) -> - addHeader(key, value) - } - } - .build() - - // 发送请求 - val response = httpClient.newCall(request).execute() - - if (response.isSuccessful) { - val responseBody = response.body?.string() - if (responseBody != null) { - val jsonResponse = JSONObject(responseBody) - val translation = extractTranslation(jsonResponse) - - if (translation != null) { - return TranslationResult( - success = true, - translatedText = translation, - confidence = 0.9f - ) - } else { - val error = extractError(jsonResponse) - return TranslationResult( - success = false, - error = error ?: "翻译结果为空" - ) - } - } - } - - return TranslationResult( - success = false, - error = "HTTP错误: ${response.code}" - ) - - } catch (e: Exception) { - return TranslationResult( - success = false, - error = "请求异常: ${e.message}" - ) - } - } - - private fun extractTranslation(jsonResponse: JSONObject): String? { - try { - // 1. 标准响应结构 - if (jsonResponse.has("TranslationList")) { - val translationList = jsonResponse.getJSONArray("TranslationList") - if (translationList.length() > 0) { - val translationItem = translationList.getJSONObject(0) - if (translationItem.has("Translation")) { - return translationItem.getString("Translation") - } - } - } - - // 2. 其他可能的响应结构 - if (jsonResponse.has("Translation")) { - return jsonResponse.getString("Translation") - } - - if (jsonResponse.has("Result")) { - val result = jsonResponse.getJSONObject("Result") - if (result.has("Translation")) { - return result.getString("Translation") - } - } - - if (jsonResponse.has("Data")) { - val data = jsonResponse.getJSONObject("Data") - if (data.has("Translation")) { - return data.getString("Translation") - } - if (data.has("TranslationList")) { - val translationList = data.getJSONArray("TranslationList") - if (translationList.length() > 0) { - val translationItem = translationList.getJSONObject(0) - if (translationItem.has("Translation")) { - return translationItem.getString("Translation") - } - } - } - } - } catch (_: Exception) { - - } - - return null - } - - private fun extractError(jsonResponse: JSONObject): String? { - try { - if (jsonResponse.has("ResponseMetadata")) { - val metadata = jsonResponse.getJSONObject("ResponseMetadata") - if (metadata.has("Error")) { - val error = metadata.getJSONObject("Error") - return error.optString("Message", error.optString("Code", "未知错误")) - } - } - - if (jsonResponse.has("Error")) { - val error = jsonResponse.getJSONObject("Error") - return error.optString("Message", error.optString("Code", "未知错误")) - } - } catch (_: Exception) { - } - - return null - } - - private fun generateSignature( - method: String, - requestBody: JSONObject, - queryParams: Map - ): Map { - try { - // 1. 准备时间相关参数 - val now = Date() - val dateFormat = SimpleDateFormat("yyyyMMdd", Locale.US).apply { - timeZone = TimeZone.getTimeZone("UTC") - } - val timestampFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US).apply { - timeZone = TimeZone.getTimeZone("UTC") - } - - val date = dateFormat.format(now) - val timestamp = timestampFormat.format(now) - - // 2. 构建规范查询字符串 - val sortedParams = queryParams.toSortedMap() - val canonicalQueryString = sortedParams.map { (key, value) -> - "${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}" - }.joinToString("&") - - // 3. 创建规范请求 - val contentType = "application/json" - val payloadHash = sha256(requestBody.toString()) - val host = "translate.volcengineapi.com" - - val canonicalHeaders = "host:$host\nx-date:$timestamp\n" - val signedHeaders = "host;x-date" - - val canonicalRequest = - "$method\n$ENDPOINT\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash" - - // 4. 创建待签字符串 - val credentialScope = "$date/$region/$SERVICE/request" - val stringToSign = - "$ALGORITHM\n$timestamp\n$credentialScope\n${sha256(canonicalRequest)}" - - // 5. 计算签名 - val kSecret = secretKey.toByteArray(Charsets.UTF_8) - val kDate = hmacSha256(kSecret, date) - val kRegion = hmacSha256(kDate, region) - val kService = hmacSha256(kRegion, SERVICE) - val kSigning = hmacSha256(kService, "request") - val signature = - hmacSha256(kSigning, stringToSign).joinToString("") { "%02x".format(it) } - - // 6. 构建授权头 - val authorization = - "$ALGORITHM Credential=$accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature" - - return mapOf( - "Content-Type" to contentType, - "X-Date" to timestamp, - "Authorization" to authorization, - "Host" to host - ) - - } catch (e: Exception) { - throw e - } - } - - private fun sha256(input: String): String { - val digest = MessageDigest.getInstance("SHA-256") - val hash = digest.digest(input.toByteArray(Charsets.UTF_8)) - return hash.joinToString("") { "%02x".format(it) } - } - - private fun hmacSha256(key: ByteArray, data: String): ByteArray { - val mac = Mac.getInstance("HmacSHA256") - val secretKeySpec = SecretKeySpec(key, "HmacSHA256") - mac.init(secretKeySpec) - return mac.doFinal(data.toByteArray(Charsets.UTF_8)) - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/CallTranslationHelper.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/CallTranslationHelper.kt deleted file mode 100644 index 6c56f2e6..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/CallTranslationHelper.kt +++ /dev/null @@ -1,217 +0,0 @@ -package com.example.asr_tr_tts_manager.translation_option - -import android.content.Context -import android.os.Handler -import android.os.Looper -import com.example.asr_tr_tts_manager.VolcengineHelper -import com.example.asr_tr_tts_manager.recorder.AstStereoRecorder -import com.example.bluetooth_manager.BluetoothManager - -class CallTranslationHelper(private val context: Context) : TranslationHelperInterface { - private val mainHandler = Handler(Looper.getMainLooper()) - - private var asrTrTtsHelperSpk: VolcengineHelper? = null - private var asrTrTtsHelperMic: VolcengineHelper? = null - - override var callback: ((Map) -> Unit)? = null - - // 通话翻译录音:stereo。左=本端麦克风(micData),右=对方原音(spkData),均为解码后 16k/16bit PCM。 - @Volatile - private var stereoRecorder: AstStereoRecorder? = null - - @Volatile - private var isSending = false - private val spkQueue = java.util.concurrent.LinkedBlockingQueue(4096) - private val micQueue = java.util.concurrent.LinkedBlockingQueue(4096) - - private val sendScheduler = java.util.concurrent.ScheduledThreadPoolExecutor(1) - private var sendTask: java.util.concurrent.ScheduledFuture<*>? = null - private var sendInterval: Long = 20L - private val packetBuf = ByteArray(84) - private var isRecognitionStopped = true - private val silenceData = byteArrayOf( - 0x0A.toByte(), 0x8E.toByte(), 0x30.toByte(), 0xBD.toByte(), 0x3F.toByte(), 0x83.toByte(), 0xFF.toByte(), 0x00.toByte(), - 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), - 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), - 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), - 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte() - ) - - init { - asrTrTtsHelperSpk = VolcengineHelper() - asrTrTtsHelperMic = VolcengineHelper() - BluetoothManager.translationCallback = { data -> - when (data["type"]) { - "startCall" -> startRecognition() - "stopCall" -> stopRecognition() - "spkData" -> spkData(data["data"] as ByteArray) - "micData" -> micData(data["data"] as ByteArray) - "speedAdjustment" -> handleSpeedAdjustment(data["data"] as ByteArray) - } - } - asrTrTtsHelperSpk?.callback = { data -> - publishData(data + ("side" to "right")) - } - asrTrTtsHelperMic?.callback = { data -> - publishData(data + ("side" to "left")) - } - asrTrTtsHelperSpk?.onSynthesisEncoded = { bytes -> - if (!spkQueue.offer(bytes)) { - spkQueue.poll() - spkQueue.offer(bytes) - } - } - asrTrTtsHelperMic?.onSynthesisEncoded = { bytes -> - if (!micQueue.offer(bytes)) { - micQueue.poll() - micQueue.offer(bytes) - } - } - } - - override fun start(sourceLanguage: String, targetLanguage: String) { - asrTrTtsHelperSpk?.switchProvider( - languages = listOf(targetLanguage, sourceLanguage) - ) - asrTrTtsHelperMic?.switchProvider( - languages = listOf(sourceLanguage, targetLanguage) - ) - BluetoothManager.writeBytes( - byteArrayOf(0xaa.toByte(), 0x51.toByte()) - ) - } - - override fun stop() { - BluetoothManager.writeBytes( - byteArrayOf(0xaa.toByte(), 0x52.toByte()) - ) - } - - private fun startRecognition() { - if (!isRecognitionStopped) return - isRecognitionStopped = false - BluetoothManager.writeBytes(byteArrayOf(0xaa.toByte(), 0x57.toByte())) - asrTrTtsHelperSpk?.startRecognition() - asrTrTtsHelperMic?.startRecognition() - spkQueue.clear() - micQueue.clear() - isSending = true - sendTask?.cancel(false) - sendTask = sendScheduler.scheduleWithFixedDelay( - { sendAudioData() }, - 0, - sendInterval, - java.util.concurrent.TimeUnit.MILLISECONDS - ) - publishData(mapOf("type" to "startCall")) - } - - private fun stopRecognition() { - if (isRecognitionStopped) return - isRecognitionStopped = true - BluetoothManager.writeBytes(byteArrayOf(0xaa.toByte(), 0x58.toByte())) - asrTrTtsHelperSpk?.stopRecognition() - asrTrTtsHelperMic?.stopRecognition() - isSending = false - sendTask?.cancel(false) - publishData(mapOf("type" to "stopCall")) - } - - private fun handleSpeedAdjustment(data: ByteArray) { - if (data.isEmpty()) return - val newInterval = when (data[6]) { - 0x01.toByte() -> 16L - 0x02.toByte() -> 20L - 0x03.toByte() -> 25L - else -> 20L - } - - if (sendInterval != newInterval) { - sendInterval = newInterval - if (isSending) { - sendTask?.cancel(false) - sendTask = sendScheduler.scheduleWithFixedDelay( - { sendAudioData() }, - 0, - sendInterval, - java.util.concurrent.TimeUnit.MILLISECONDS - ) - } - } - } - - private fun sendAudioData() { - if (!isSending) return - var any = false - var fillP = 4 - for (k in 0 until 2) { - System.arraycopy(silenceData, 0, packetBuf, fillP, 40) - fillP += 40 - } - var offset = 4 - for (i in 0 until 1) { - val spk = spkQueue.poll() - val mic = micQueue.poll() - if (spk != null) { - val sLen = minOf(spk.size, 40) - System.arraycopy(spk, 0, packetBuf, offset, sLen) - any = true - } - if (mic != null) { - val mLen = minOf(mic.size, 40) - System.arraycopy(mic, 0, packetBuf, offset + 40, mLen) - any = true - } - offset += 80 - } - if (!any) return - packetBuf[0] = 0xAA.toByte() - packetBuf[1] = 0x56.toByte() - packetBuf[2] = 84.toByte() - packetBuf[3] = 0x03.toByte() - BluetoothManager.writeBytes(packetBuf) - } - - private fun spkData(data: ByteArray) { - asrTrTtsHelperSpk?.pushStreamWrite(data) - // 录音 tap(右声道):spkData=对方原音,已在 BluetoothManager.g722DecodeData 解码为 16k/16bit mono PCM - stereoRecorder?.writeRightChannel(data) - } - - private fun micData(data: ByteArray) { - asrTrTtsHelperMic?.pushStreamWrite(data) - // 录音 tap(左声道,作时钟):micData=本端麦克风,已在 BluetoothManager.g722DecodeData 解码为 16k/16bit mono PCM - stereoRecorder?.writeLeftChannel(data) - } - - private fun publishData(data: Map) { - val cb = callback ?: return - mainHandler.post { cb.invoke(data) } - } - - // 通话翻译为双声道录音:左=本端麦克风(micData),右=对方原音(spkData) - override fun enableRecord(filePath: String) { - // 防御:未 stopRecord 就重复 enableRecord 时先停旧的 - stereoRecorder?.stop(false) - val recorder = AstStereoRecorder() - recorder.start(filePath) - stereoRecorder = recorder - } - - override fun stopRecord(isSave: Boolean) { - stereoRecorder?.stop(isSave) - stereoRecorder = null - } - - override fun dispose() { - stop() - stopRecognition() - // 兜底:若上层没显式 stopRecord,dispose 时保存已录内容 - stereoRecorder?.stop(true) - stereoRecorder = null - asrTrTtsHelperSpk?.dispose() - asrTrTtsHelperMic?.dispose() - callback = null - BluetoothManager.translationCallback = null - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/FaceToFaceTranslationHelper.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/FaceToFaceTranslationHelper.kt deleted file mode 100644 index cb99181d..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/FaceToFaceTranslationHelper.kt +++ /dev/null @@ -1,229 +0,0 @@ -package com.example.asr_tr_tts_manager.translation_option - -import android.content.Context -import android.media.AudioDeviceInfo -import android.media.AudioManager -import android.os.Build -import android.os.Handler -import android.os.Looper -import com.example.asr_tr_tts_manager.AsrTrTtsHelper -import com.example.asr_tr_tts_manager.recorder.AstStereoRecorder -import com.example.bluetooth_manager.BluetoothManager - -class FaceToFaceTranslationHelper(private val context: Context) : TranslationHelperInterface { - private val mainHandler = Handler(Looper.getMainLooper()) - - private var asrTrTtsHelperSpk: AsrTrTtsHelper? = null - private var asrTrTtsHelperMic: AsrTrTtsHelper? = null - - override var callback: ((Map) -> Unit)? = null - - @Volatile - private var isSending = false - private val micQueue = java.util.concurrent.LinkedBlockingQueue(4096) - - private val sendScheduler = java.util.concurrent.ScheduledThreadPoolExecutor(1) - private var sendTask: java.util.concurrent.ScheduledFuture<*>? = null - private var sendInterval: Long = 20L - private val packetBuf = ByteArray(44) - private var isRecognitionStopped = true - private var previousAudioMode: Int = AudioManager.MODE_NORMAL - private var previousSpeakerphone: Boolean = false - private val silenceData = byteArrayOf( - 0x0A.toByte(), 0x8E.toByte(), 0x30.toByte(), 0xBD.toByte(), 0x3F.toByte(), 0x83.toByte(), 0xFF.toByte(), 0x00.toByte(), - 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), - 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), - 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), - 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte() - ) - - private var microphoneRecorder: MicrophoneRecorder? = null - - // 面对面翻译录音:mono。写入的是本端麦克风解码后 16k/16bit PCM。 - @Volatile - private var stereoRecorder: AstStereoRecorder? = null - - init { - asrTrTtsHelperSpk = AsrTrTtsHelper(context) - asrTrTtsHelperMic = AsrTrTtsHelper(context, autoPlay = true, forceBuiltInSpeaker = true) - BluetoothManager.translationCallback = { data -> - when (data["type"]) { - "startFaceToFace" -> startRecognition() - "stopFaceToFace" -> stopRecognition() - "spkData" -> micData(data["data"] as ByteArray) - "speedAdjustment" -> handleSpeedAdjustment(data["data"] as ByteArray) - } - } - microphoneRecorder = MicrophoneRecorder(context) { data -> - spkData(data) - } - asrTrTtsHelperSpk?.callback = { data -> - publishData(data + ("side" to "right")) - } - asrTrTtsHelperMic?.callback = { data -> - publishData(data + ("side" to "left")) - } - asrTrTtsHelperSpk?.onSynthesisEncoded = { bytes -> - if (!micQueue.offer(bytes)) { - micQueue.poll() - micQueue.offer(bytes) - } - } - } - - override fun start(sourceLanguage: String, targetLanguage: String) { - // 设置通信模式,让平台级AEC生效 - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - previousAudioMode = audioManager.mode - previousSpeakerphone = audioManager.isSpeakerphoneOn - audioManager.mode = AudioManager.MODE_IN_COMMUNICATION - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val speaker = audioManager.availableCommunicationDevices - .firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } - if (speaker != null) { - audioManager.setCommunicationDevice(speaker) - } - } else { - @Suppress("DEPRECATION") - audioManager.isSpeakerphoneOn = true - } - - asrTrTtsHelperSpk?.switchProvider( - languages = listOf(targetLanguage, sourceLanguage) - ) - asrTrTtsHelperMic?.switchProvider( - languages = listOf(sourceLanguage, targetLanguage) - ) - BluetoothManager.writeBytes( - byteArrayOf(0xaa.toByte(), 0x66.toByte()) - ) - microphoneRecorder?.start() - } - - override fun stop() { - BluetoothManager.writeBytes( - byteArrayOf(0xaa.toByte(), 0x67.toByte()) - ) - microphoneRecorder?.stop() - - // 恢复音频设置 - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - audioManager.clearCommunicationDevice() - } else { - @Suppress("DEPRECATION") - audioManager.isSpeakerphoneOn = previousSpeakerphone - } - audioManager.mode = previousAudioMode - } - - private fun startRecognition() { - if (!isRecognitionStopped) return - isRecognitionStopped = false - asrTrTtsHelperSpk?.startRecognition() - asrTrTtsHelperMic?.startRecognition() - micQueue.clear() - isSending = true - sendTask?.cancel(false) - sendTask = sendScheduler.scheduleWithFixedDelay( - { sendAudioData() }, - 0, - sendInterval, - java.util.concurrent.TimeUnit.MILLISECONDS - ) - publishData(mapOf("type" to "startCall")) - } - - private fun stopRecognition() { - if (isRecognitionStopped) return - isRecognitionStopped = true - asrTrTtsHelperSpk?.stopRecognition() - asrTrTtsHelperMic?.stopRecognition() - isSending = false - sendTask?.cancel(false) - publishData(mapOf("type" to "stopCall")) - } - - private fun handleSpeedAdjustment(data: ByteArray) { - if (data.isEmpty()) return - val newInterval = when (data[6]) { - 0x01.toByte() -> 15L - 0x02.toByte() -> 20L - 0x03.toByte() -> 25L - else -> 20L - } - - if (sendInterval != newInterval) { - sendInterval = newInterval - if (isSending) { - sendTask?.cancel(false) - sendTask = sendScheduler.scheduleWithFixedDelay( - { sendAudioData() }, - 0, - sendInterval, - java.util.concurrent.TimeUnit.MILLISECONDS - ) - } - } - } - - private fun sendAudioData() { - if (!isSending) return - // 先填充静音数据 - System.arraycopy(silenceData, 0, packetBuf, 4, 40) - val mic = micQueue.poll() ?: return - val mLen = minOf(mic.size, 40) - System.arraycopy(mic, 0, packetBuf, 4, mLen) - packetBuf[0] = 0xAA.toByte() - packetBuf[1] = 0x69.toByte() - packetBuf[2] = 44.toByte() - packetBuf[3] = 0x01.toByte() - BluetoothManager.writeBytes(packetBuf) - } - - private fun spkData(data: ByteArray) { - asrTrTtsHelperSpk?.pushStreamWrite(data) - // 录音 tap(左声道,作时钟):spkData=本端手机麦克风,由 MicrophoneRecorder(PCMReader) - // 喂入,确认为 16k/16bit mono PCM - stereoRecorder?.writeLeftChannel(data) - } - - private fun micData(data: ByteArray) { - asrTrTtsHelperMic?.pushStreamWrite(data) - // 录音 tap(右声道):micData=对方(BLE 设备),已在 BluetoothManager 解码为 16k/16bit mono PCM - stereoRecorder?.writeRightChannel(data) - } - - private fun publishData(data: Map) { - val cb = callback ?: return - mainHandler.post { cb.invoke(data) } - } - - // 面对面翻译为双声道录音:左=本端手机麦克风(spkData),右=对方/BLE 设备(micData) - override fun enableRecord(filePath: String) { - // 防御:未 stopRecord 就重复 enableRecord 时先停旧的 - stereoRecorder?.stop(false) - val recorder = AstStereoRecorder() - recorder.start(filePath) - stereoRecorder = recorder - } - - override fun stopRecord(isSave: Boolean) { - stereoRecorder?.stop(isSave) - stereoRecorder = null - } - - override fun dispose() { - stop() - stopRecognition() - microphoneRecorder?.dispose() - microphoneRecorder = null - // 兜底:若上层没显式 stopRecord,dispose 时保存已录内容 - stereoRecorder?.stop(true) - stereoRecorder = null - asrTrTtsHelperSpk?.dispose() - asrTrTtsHelperMic?.dispose() - callback = null - BluetoothManager.translationCallback = null - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/MicrophoneRecorder.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/MicrophoneRecorder.kt deleted file mode 100644 index 9469d88d..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/MicrophoneRecorder.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.asr_tr_tts_manager.translation_option - -import android.content.Context -import android.media.MediaFormat -import android.media.MediaRecorder -import com.llfbandit.record.record.PCMReader -import com.llfbandit.record.record.RecordConfig -import java.util.concurrent.atomic.AtomicBoolean - -class MicrophoneRecorder( - private val context: Context, - private val sampleRate: Int = 16000, - private val onAudioData: (ByteArray) -> Unit -) { - private val isRecording = AtomicBoolean(false) - private var recordThread: Thread? = null - private var pcmReader: PCMReader? = null - - fun start() { - if (isRecording.get()) return - - // device 设为 null,不用 setPreferredDevice 指定录音设备 - // 由 SimultaneousTranslationHelper 的 setCommunicationDevice 统一管理音频路由 - // 避免与平台级 AEC 的参考信号路径冲突 - val config = RecordConfig( - path = null, - encoder = "pcm16bits", - bitRate = 128000, - sampleRate = sampleRate, - numChannels = 1, - device = null, - autoGain = true, - echoCancel = true, - noiseSuppress = true, - manageBluetooth = false, - audioSource = MediaRecorder.AudioSource.VOICE_COMMUNICATION, - audioInterruption = 0, - streamBufferSize = null - ) - - val mediaFormat = MediaFormat.createAudioFormat( - MediaFormat.MIMETYPE_AUDIO_RAW, sampleRate, 1 - ) - - val reader = try { - PCMReader(config, mediaFormat) - } catch (e: Exception) { - return - } - - pcmReader = reader - isRecording.set(true) - reader.start() - - recordThread = Thread({ - while (isRecording.get()) { - try { - val data = reader.read() - if (data.isNotEmpty()) { - onAudioData(data) - } - } catch (_: Exception) { - break - } - } - }, "MicrophoneRecorder") - recordThread?.start() - } - - fun stop() { - if (!isRecording.getAndSet(false)) return - - recordThread?.join(1000) - recordThread = null - - pcmReader?.stop() - pcmReader?.release() - pcmReader = null - } - - fun dispose() { - stop() - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/SimultaneousTranslationHelper.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/SimultaneousTranslationHelper.kt deleted file mode 100644 index 2cbf26f4..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/SimultaneousTranslationHelper.kt +++ /dev/null @@ -1,332 +0,0 @@ -package com.example.asr_tr_tts_manager.translation_option - -import android.content.Context -import android.media.AudioDeviceCallback -import android.media.AudioDeviceInfo -import android.media.AudioManager -import android.os.Build -import android.os.Handler -import android.os.Looper -import com.example.asr_tr_tts_manager.AsrTrTtsHelper -import com.example.asr_tr_tts_manager.recorder.RecordFile -import com.example.bluetooth_manager.BluetoothManager -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit - -class SimultaneousTranslationHelper(private val context: Context) : TranslationHelperInterface { - private val mainHandler = Handler(Looper.getMainLooper()) - - /** - * 串行执行路由切换 / 录音重启,避免主线程被 recordThread.join(1000) 阻塞。 - * 只有「启动时已连普通蓝牙耳机」分支才用到,懒初始化避免 SPP/无 BT 场景白白多一个常驻线程。 - */ - private val routingExecutorLazy = lazy { - Executors.newSingleThreadExecutor { r -> Thread(r, "SimulRoutingExec") } - } - private val routingExecutor by routingExecutorLazy - - private var asrTrTtsHelper: AsrTrTtsHelper? = null - - override var callback: ((Map) -> Unit)? = null - - @Volatile - private var isRecognitionStopped = true - - private var microphoneRecorder: MicrophoneRecorder? = null - - // 同声传译录音:mono。写入的是送入 ASR 的解码后 16k/16bit PCM。 - @Volatile - private var recordFile: RecordFile? = null - - private var sourceLanguageCode: String = "" - private var targetLanguageCode: String = "" - private var previousAudioMode: Int = AudioManager.MODE_NORMAL - - @Volatile - private var forcedSpeaker: Boolean = false - - @Volatile - private var forcedBtSco: Boolean = false - - /** 本次会话是否走蓝牙 BLE 指令路径(连上耳机时启用,从耳机主麦经 mSBC 拉 PCM)。 */ - @Volatile - private var usingBtPath: Boolean = false - - private var audioDeviceCallback: AudioDeviceCallback? = null - - init { - asrTrTtsHelper = AsrTrTtsHelper(context, autoPlay = true, autoDetectLanguage = true) - microphoneRecorder = MicrophoneRecorder(context) { data -> - asrTrTtsHelper?.pushStreamWrite(data) - // 录音 tap:本地麦克风 PCMReader 输出,确认为 16k/16bit mono PCM - recordFile?.saveAudioDataToWav(data) - } - asrTrTtsHelper?.callback = { data -> - val language = data["language"] as? String ?: "" - val side = if (language.equals(sourceLanguageCode, ignoreCase = true)) "left" else "right" - publishData(data + ("side" to side)) - } - // BT 路径事件分发:0xD9/0xDA 触发 ASR 生命周期,mainMicData 喂 ASR(ffMicData 是辅助麦,丢弃) - BluetoothManager.translationCallback = { data -> - if (usingBtPath) { - when (data["type"]) { - "startMicRecording" -> startRecognition() - "stopMicRecording" -> stopRecognition() - "mainMicData" -> (data["data"] as? ByteArray)?.let { - asrTrTtsHelper?.pushStreamWrite(it) - // 录音 tap:BluetoothManager.mainMicData 已在 msbcDecodeData 解码为 16k/16bit mono PCM - recordFile?.saveAudioDataToWav(it) - } - } - } - } - } - - override fun start(sourceLanguage: String, targetLanguage: String) { - sourceLanguageCode = sourceLanguage - targetLanguageCode = targetLanguage - usingBtPath = BluetoothManager.isConnected - - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - - if (!usingBtPath) { - // 非 BT 路径:本地麦克风采集,需要 MODE_IN_COMMUNICATION 让平台 AEC 生效 - previousAudioMode = audioManager.mode - audioManager.mode = AudioManager.MODE_IN_COMMUNICATION - - // 没有连接蓝牙音频设备时,强制走扬声器(否则 MODE_IN_COMMUNICATION 默认走听筒,声音很小); - // 系统配对了普通蓝牙耳机(A2DP/HFP)时,强制走 SCO,让录音也走耳机麦(代价:播放从 - // A2DP 高音质降为 SCO 8/16kHz 电话音质,这是 Android 协议层面无法兼得的) - if (!hasBluetoothAudioDevice(audioManager)) { - applySpeakerRoute(audioManager) - } else { - applyBtScoRoute(audioManager) - // SCO 中途断开后,AcousticEchoCanceler 不会自动迁移到新路由,必须监听并重建 - registerAudioDeviceCallback(audioManager) - } - } - // BT 路径:录音走 BLE 指令通道,不需要改 audio mode;TTS 默认会走耳机 A2DP - - asrTrTtsHelper?.switchProvider( - languages = listOf(targetLanguage, sourceLanguage) - ) - - if (usingBtPath) { - // 复用会议助手「现场录音」指令:通知耳机启动主麦 mSBC 数据流 - // ASR 开启 + startCall 等设备回 0xD9 后在 startRecognition 里发起 - BluetoothManager.writeBytes(byteArrayOf(0xaa.toByte(), 0x59.toByte())) - } else { - microphoneRecorder?.start() - startRecognition() - } - } - - override fun stop() { - // 缓存一次:避免在 stop() 执行过程中被 translationCallback 的 stopRecognition 改写后 - // 出现「前半段当 BT 路径处理、后半段当非 BT 处理」的不一致 - val bt = usingBtPath - if (bt) { - // ASR 关闭 + stopCall 等设备回 0xDA 后在 stopRecognition 里发起 - BluetoothManager.writeBytes(byteArrayOf(0xaa.toByte(), 0x5a.toByte())) - } else { - microphoneRecorder?.stop() - } - - // 音频会话同步回滚(仅非 BT 路径才需要,BT 路径下没改 audio mode) - if (!bt) { - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - unregisterAudioDeviceCallback(audioManager) - releaseSpeakerRoute(audioManager) - releaseBtScoRoute(audioManager) - audioManager.mode = previousAudioMode - } - - // 非 BT 路径在所有同步清理结束后再触发 stopRecognition + publishStopCall, - // 让 UI 看到 stopCall 时资源已全部释放,与 BT 路径下 0xDA 触发的时机一致 - if (!bt) { - stopRecognition() - } - } - - private fun startRecognition() { - if (!isRecognitionStopped) return - isRecognitionStopped = false - asrTrTtsHelper?.startRecognition() - publishData(mapOf("type" to "startCall")) - } - - private fun stopRecognition() { - if (isRecognitionStopped) return - isRecognitionStopped = true - asrTrTtsHelper?.stopRecognition() - publishData(mapOf("type" to "stopCall")) - usingBtPath = false - } - - // MARK: - 音频路由切换辅助 - - private fun applySpeakerRoute(audioManager: AudioManager) { - forcedSpeaker = true - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val speaker = audioManager.availableCommunicationDevices - .firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } - if (speaker != null) { - audioManager.setCommunicationDevice(speaker) - } - } else { - @Suppress("DEPRECATION") - audioManager.isSpeakerphoneOn = true - } - } - - private fun releaseSpeakerRoute(audioManager: AudioManager) { - if (!forcedSpeaker) return - forcedSpeaker = false - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - audioManager.clearCommunicationDevice() - } else { - @Suppress("DEPRECATION") - audioManager.isSpeakerphoneOn = false - } - } - - private fun applyBtScoRoute(audioManager: AudioManager) { - forcedBtSco = true - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val btSco = audioManager.availableCommunicationDevices.firstOrNull { - it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO || - it.type == AudioDeviceInfo.TYPE_BLE_HEADSET - } - if (btSco != null) { - audioManager.setCommunicationDevice(btSco) - } - } else { - @Suppress("DEPRECATION") - audioManager.startBluetoothSco() - @Suppress("DEPRECATION") - audioManager.isBluetoothScoOn = true - } - } - - private fun releaseBtScoRoute(audioManager: AudioManager) { - if (!forcedBtSco) return - forcedBtSco = false - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - audioManager.clearCommunicationDevice() - } else { - @Suppress("DEPRECATION") - audioManager.isBluetoothScoOn = false - @Suppress("DEPRECATION") - audioManager.stopBluetoothSco() - } - } - - // MARK: - 中途断开蓝牙耳机的热切换 - - private fun registerAudioDeviceCallback(audioManager: AudioManager) { - if (audioDeviceCallback != null) return - val cb = object : AudioDeviceCallback() { - override fun onAudioDevicesRemoved(removedDevices: Array) { - val btRemoved = removedDevices.any { - it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO || - it.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP || - it.type == AudioDeviceInfo.TYPE_BLE_HEADSET - } - if (btRemoved && forcedBtSco) { - // 在后台线程做路由切换 + 录音重启,避免 main 被 recordThread.join 阻塞 - routingExecutor.execute { handleBtAudioLost() } - } - } - } - audioDeviceCallback = cb - audioManager.registerAudioDeviceCallback(cb, mainHandler) - } - - private fun unregisterAudioDeviceCallback(audioManager: AudioManager) { - audioDeviceCallback?.let { - audioManager.unregisterAudioDeviceCallback(it) - } - audioDeviceCallback = null - } - - private fun handleBtAudioLost() { - // 重入守门:会话已停止或不再处于 SCO 状态时直接退出 - if (isRecognitionStopped || !forcedBtSco) return - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - - // 1. 释放 SCO 状态 + 切到内置扬声器(不切回的话播放会落到听筒,声音很小); - // AudioManager 罕见情况会抛异常,单独包住保证下面 mic 重启仍会执行 - try { - releaseBtScoRoute(audioManager) - applySpeakerRoute(audioManager) - } catch (e: Exception) { - android.util.Log.e("SimulHelper", "route switch failed", e) - } - - // 2. 重启 MicrophoneRecorder 让 AudioRecord 在新路由上重新挂载 AEC effect - // dispose 的 shutdownNow 会让 recordThread.join 抛 InterruptedException, - // 此时不要 restart(让 dispose 接管收尾),并恢复中断标志 - try { - microphoneRecorder?.stop() - } catch (_: InterruptedException) { - Thread.currentThread().interrupt() - return - } - if (!isRecognitionStopped) { - microphoneRecorder?.start() - } - } - - private fun hasBluetoothAudioDevice(audioManager: AudioManager): Boolean { - return audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS).any { - it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO || - it.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP - } - } - - private fun publishData(data: Map) { - val cb = callback ?: return - mainHandler.post { cb.invoke(data) } - } - - // 同声传译为单声道录音:写入送入 ASR 的解码后 PCM - override fun enableRecord(filePath: String) { - // 防御:未 stopRecord 就重复 enableRecord 时先关旧的,避免泄漏挂起写入与文件句柄 - recordFile?.closeFile(false) - val file = RecordFile() - file.setAudioConfig(16000, 1) - file.creatingFiles(filePath) - recordFile = file - } - - override fun stopRecord(isSave: Boolean) { - recordFile?.closeFile(isSave) - recordFile = null - } - - override fun dispose() { - stop() - // 若 BT 路径下设备没回 0xDA,stopRecognition 还没跑过;这里兜底强制收尾 - stopRecognition() - // shutdownNow 中断在飞任务、awaitTermination 等其退出,避免 handleBtAudioLost - // 与下面的 microphoneRecorder.dispose() 竞争同一份资源; - // 懒初始化的 executor 没用过就不必关闭 - if (routingExecutorLazy.isInitialized()) { - routingExecutor.shutdownNow() - try { - routingExecutor.awaitTermination(500, TimeUnit.MILLISECONDS) - } catch (_: InterruptedException) { - Thread.currentThread().interrupt() - } - } - microphoneRecorder?.dispose() - microphoneRecorder = null - // 兜底:若上层没显式 stopRecord,dispose 时保存已录内容 - recordFile?.closeFile(true) - recordFile = null - asrTrTtsHelper?.dispose() - asrTrTtsHelper = null - callback = null - BluetoothManager.translationCallback = null - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/TranslationHelperInterface.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/TranslationHelperInterface.kt deleted file mode 100644 index 354b3295..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/TranslationHelperInterface.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.asr_tr_tts_manager.translation_option - -enum class TranslationHelperProvider { - SIMULTANEOUS, - CALL, - FACETOFACE, -} - -interface TranslationHelperInterface { - var callback: ((Map) -> Unit)? - - fun start(sourceLanguage: String = "zh-CN", targetLanguage: String = "en-US") - fun stop() - fun dispose() - - /** 开始把正在送入 ASR 的解码后 PCM 同步写入 WAV 文件(mono 或 stereo 由实现决定)。 */ - fun enableRecord(filePath: String) - - /** 结束录音;isSave=false 时丢弃文件。 */ - fun stopRecord(isSave: Boolean) -} \ No newline at end of file diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/TranslationHelperManager.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/TranslationHelperManager.kt deleted file mode 100644 index 1ec2ec76..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/translation_option/TranslationHelperManager.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.asr_tr_tts_manager.translation_option - -import android.content.Context - -class TranslationHelperManager( - private val context: Context, - provider: TranslationHelperProvider = TranslationHelperProvider.CALL, -) : TranslationHelperInterface { - private var currentProvider: TranslationHelperProvider = provider - private var delegate: TranslationHelperInterface? = null - - override var callback: ((Map) -> Unit)? = null - set(value) { - field = value - delegate?.callback = value - } - - init { - switchProvider() - } - - fun switchProvider(provider: TranslationHelperProvider = currentProvider) { - try { - delegate?.dispose() - } catch (_: Exception) { - } - - currentProvider = provider - - delegate = - when (provider) { - TranslationHelperProvider.SIMULTANEOUS -> SimultaneousTranslationHelper(context) - TranslationHelperProvider.CALL -> CallTranslationHelper(context) - TranslationHelperProvider.FACETOFACE -> FaceToFaceTranslationHelper(context) - }.also { - it.callback = callback - } - } - - fun getProvider(): TranslationHelperProvider = currentProvider - - override fun start(sourceLanguage: String, targetLanguage: String) { - delegate?.start(sourceLanguage, targetLanguage) - } - - override fun stop() { - delegate?.stop() - } - - override fun enableRecord(filePath: String) { - delegate?.enableRecord(filePath) - } - - override fun stopRecord(isSave: Boolean) { - delegate?.stopRecord(isSave) - } - - override fun dispose() { - callback = null - val d = delegate - delegate = null - d?.dispose() - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/AzureTts.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/AzureTts.kt deleted file mode 100644 index 4ee5680e..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/AzureTts.kt +++ /dev/null @@ -1,214 +0,0 @@ -package com.example.asr_tr_tts_manager.tts - -import android.content.Context -import android.media.AudioAttributes -import android.media.AudioDeviceInfo -import android.media.AudioFormat -import android.media.AudioManager -import android.media.AudioTrack -import android.os.Build -import com.example.bluetooth_manager.G722Codec -import com.microsoft.cognitiveservices.speech.SpeechConfig -import com.microsoft.cognitiveservices.speech.SpeechSynthesisOutputFormat -import com.microsoft.cognitiveservices.speech.SpeechSynthesizer -import com.microsoft.cognitiveservices.speech.audio.AudioConfig -import com.microsoft.cognitiveservices.speech.audio.AudioOutputStream -import com.microsoft.cognitiveservices.speech.audio.PushAudioOutputStreamCallback - -class AzureTts(private val context: Context) : TtsInterface { - private val SUBSCRIPTION_KEY: String = - "rLPdyGgD1v7z7khzpdrd4YNWUinjw6GzAbA1pPXBRmUjitfjDxdsJQQJ99BGAC3pKaRXJ3w3AAAYACOGAHZZ" - private val REGION: String = "eastasia" - - private var g722Handle: Long = 0L - override var onSynthesisEncoded: ((ByteArray) -> Unit)? = null - override var autoPlay: Boolean = false - override var forceBuiltInSpeaker: Boolean = false - set(value) { - if (field == value) return - field = value - // AudioAttributes 在 build() 后不可变,usage 随 forceBuiltInSpeaker 变化必须重建 AudioTrack - rebuildAudioTrack() - } - - private var synthesizer: SpeechSynthesizer? = null - private var audioTrack: AudioTrack? = null - - private inner class MemoryPushAudioOutputStreamCallback : PushAudioOutputStreamCallback() { - private var stashBuffer: ByteArray = ByteArray(8192) - private var stashSize: Int = 0 - private val frameBytes: Int = 320 * 2 - - override fun write(data: ByteArray): Int { - if (data.isEmpty()) return 0 - - if (autoPlay) { - audioTrack?.write(data, 0, data.size) - return data.size - } - - val required = stashSize + data.size - if (required > stashBuffer.size) { - var newCap = stashBuffer.size - while (newCap < required) newCap = newCap shl 1 - val newBuf = ByteArray(newCap) - System.arraycopy(stashBuffer, 0, newBuf, 0, stashSize) - stashBuffer = newBuf - } - - System.arraycopy(data, 0, stashBuffer, stashSize, data.size) - stashSize += data.size - - var cursor = 0 - if (g722Handle == 0L) { - g722Handle = G722Codec.init(16000) - } - - while (stashSize - cursor >= frameBytes) { - val frame = ByteArray(frameBytes) - System.arraycopy(stashBuffer, cursor, frame, 0, frameBytes) - val encoded = try { - G722Codec.encodeSync(g722Handle, frame) - } catch (_: Exception) { - null - } - if (encoded != null) { - onSynthesisEncoded?.invoke(encoded) - } - cursor += frameBytes - } - - if (cursor > 0) { - val remaining = stashSize - cursor - if (remaining > 0) { - System.arraycopy(stashBuffer, cursor, stashBuffer, 0, remaining) - } - stashSize = remaining - } - - return data.size - } - - override fun close() { - stashSize = 0 - } - - fun reset() { - stashSize = 0 - } - } - - private val synthesisOutputCallback = MemoryPushAudioOutputStreamCallback() - - init { - val speechConfig = SpeechConfig.fromSubscription(SUBSCRIPTION_KEY, REGION) - speechConfig.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Raw16Khz16BitMonoPcm) - val outputStream = AudioOutputStream.createPushStream(synthesisOutputCallback) - val audioOutputConfig = AudioConfig.fromStreamOutput(outputStream) - synthesizer = SpeechSynthesizer(speechConfig, audioOutputConfig) - - buildAudioTrack() - } - - private fun buildAudioTrack() { - val bufSize = AudioTrack.getMinBufferSize( - 16000, - AudioFormat.CHANNEL_OUT_MONO, - AudioFormat.ENCODING_PCM_16BIT - ) - // forceBuiltInSpeaker(面对面翻译外放)走通信通路:只有 USAGE_VOICE_COMMUNICATION 才会被 - // MODE_IN_COMMUNICATION + setCommunicationDevice(内置扬声器) 正确路由到扬声器; - // USAGE_MEDIA 在小米/HyperOS 通话模式下会被压制,导致外放无声而接耳机正常。 - val usage = if (forceBuiltInSpeaker) { - AudioAttributes.USAGE_VOICE_COMMUNICATION - } else { - AudioAttributes.USAGE_MEDIA - } - audioTrack = AudioTrack.Builder() - .setAudioAttributes( - AudioAttributes.Builder() - .setUsage(usage) - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build() - ) - .setAudioFormat( - AudioFormat.Builder() - .setSampleRate(16000) - .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) - .setEncoding(AudioFormat.ENCODING_PCM_16BIT) - .build() - ) - .setBufferSizeInBytes(bufSize) - .setTransferMode(AudioTrack.MODE_STREAM) - .build() - applyPreferredDevice() - } - - private fun rebuildAudioTrack() { - try { - audioTrack?.release() - } catch (_: Exception) { - } - audioTrack = null - buildAudioTrack() - } - - override fun speakText(text: String, targetLanguage: String, voiceName: String) { - speakSsml(buildSsml(text = text, targetLanguage = targetLanguage, voiceName = voiceName)) - } - - fun speakSsml(ssml: String) { - synthesisOutputCallback.reset() - if (autoPlay) { - audioTrack?.play() - } - synthesizer?.SpeakSsmlAsync(ssml)?.get() - if (autoPlay) { - audioTrack?.stop() - } - } - - private fun applyPreferredDevice() { - val track = audioTrack ?: return - if (forceBuiltInSpeaker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - val speaker = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) - .firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } - track.preferredDevice = speaker - } else { - track.preferredDevice = null - } - } - - override fun dispose() { - try { - synthesizer?.close() - } catch (_: Exception) { - } - synthesizer = null - try { - audioTrack?.release() - } catch (_: Exception) { - } - audioTrack = null - try { - if (g722Handle != 0L) { - G722Codec.release(g722Handle) - g722Handle = 0L - } - } catch (_: Exception) { - } - } - - private fun buildSsml(text: String, targetLanguage: String, voiceName: String): String { - return """ - - - - $text - - - - """.trimIndent() - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/TtsInterface.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/TtsInterface.kt deleted file mode 100644 index b333f514..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/TtsInterface.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.asr_tr_tts_manager.tts - -enum class TtsProvider { - AZURE, - VOLCENGINE, -} - -interface TtsInterface { - var onSynthesisEncoded: ((ByteArray) -> Unit)? - var autoPlay: Boolean - var forceBuiltInSpeaker: Boolean - - fun speakText(text: String, targetLanguage: String, voiceName: String) - fun dispose() -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/TtsManager.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/TtsManager.kt deleted file mode 100644 index d4ca48da..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/TtsManager.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.example.asr_tr_tts_manager.tts - -import android.content.Context - -class TtsManager( - private val context: Context, - provider: TtsProvider = TtsProvider.AZURE, - autoPlay: Boolean = false, - forceBuiltInSpeaker: Boolean = false, -) : TtsInterface { - - private var currentProvider: TtsProvider = provider - private var delegate: TtsInterface? = null - - override var onSynthesisEncoded: ((ByteArray) -> Unit)? = null - set(value) { - field = value - delegate?.onSynthesisEncoded = value - } - - override var autoPlay: Boolean = autoPlay - set(value) { - field = value - delegate?.autoPlay = value - } - - override var forceBuiltInSpeaker: Boolean = forceBuiltInSpeaker - set(value) { - field = value - delegate?.forceBuiltInSpeaker = value - } - - init { - switchProvider() - } - - fun switchProvider(provider: TtsProvider = currentProvider) { - try { - delegate?.dispose() - } catch (_: Exception) { - } - - currentProvider = provider - - delegate = - when (provider) { - TtsProvider.AZURE -> AzureTts(context) - TtsProvider.VOLCENGINE -> VolcengineTts(context) - }.also { - it.onSynthesisEncoded = onSynthesisEncoded - it.autoPlay = autoPlay - it.forceBuiltInSpeaker = forceBuiltInSpeaker - } - } - - fun getProvider(): TtsProvider = currentProvider - - override fun speakText(text: String, targetLanguage: String, voiceName: String) { - delegate?.speakText(text, targetLanguage, voiceName) - } - - override fun dispose() { - onSynthesisEncoded = null - - val d = delegate - delegate = null - d?.dispose() - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/VolcengineTts.kt b/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/VolcengineTts.kt deleted file mode 100644 index 60f28312..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/android/src/main/kotlin/com/example/asr_tr_tts_manager/tts/VolcengineTts.kt +++ /dev/null @@ -1,214 +0,0 @@ -package com.example.asr_tr_tts_manager.tts - -import android.content.Context -import android.media.AudioAttributes -import android.media.AudioDeviceInfo -import android.media.AudioFormat -import android.media.AudioManager -import android.media.AudioTrack -import android.os.Build -import com.example.bluetooth_manager.G722Codec -import com.microsoft.cognitiveservices.speech.SpeechConfig -import com.microsoft.cognitiveservices.speech.SpeechSynthesisOutputFormat -import com.microsoft.cognitiveservices.speech.SpeechSynthesizer -import com.microsoft.cognitiveservices.speech.audio.AudioConfig -import com.microsoft.cognitiveservices.speech.audio.AudioOutputStream -import com.microsoft.cognitiveservices.speech.audio.PushAudioOutputStreamCallback - -class VolcengineTts(private val context: Context) : TtsInterface { - private val SUBSCRIPTION_KEY: String = - "rLPdyGgD1v7z7khzpdrd4YNWUinjw6GzAbA1pPXBRmUjitfjDxdsJQQJ99BGAC3pKaRXJ3w3AAAYACOGAHZZ" - private val REGION: String = "eastasia" - - private var g722Handle: Long = 0L - override var onSynthesisEncoded: ((ByteArray) -> Unit)? = null - override var autoPlay: Boolean = false - override var forceBuiltInSpeaker: Boolean = false - set(value) { - if (field == value) return - field = value - // AudioAttributes 在 build() 后不可变,usage 随 forceBuiltInSpeaker 变化必须重建 AudioTrack - rebuildAudioTrack() - } - - private var synthesizer: SpeechSynthesizer? = null - private var audioTrack: AudioTrack? = null - - private inner class MemoryPushAudioOutputStreamCallback : PushAudioOutputStreamCallback() { - private var stashBuffer: ByteArray = ByteArray(8192) - private var stashSize: Int = 0 - private val frameBytes: Int = 320 * 2 - - override fun write(data: ByteArray): Int { - if (data.isEmpty()) return 0 - - if (autoPlay) { - audioTrack?.write(data, 0, data.size) - return data.size - } - - val required = stashSize + data.size - if (required > stashBuffer.size) { - var newCap = stashBuffer.size - while (newCap < required) newCap = newCap shl 1 - val newBuf = ByteArray(newCap) - System.arraycopy(stashBuffer, 0, newBuf, 0, stashSize) - stashBuffer = newBuf - } - - System.arraycopy(data, 0, stashBuffer, stashSize, data.size) - stashSize += data.size - - var cursor = 0 - if (g722Handle == 0L) { - g722Handle = G722Codec.init(16000) - } - - while (stashSize - cursor >= frameBytes) { - val frame = ByteArray(frameBytes) - System.arraycopy(stashBuffer, cursor, frame, 0, frameBytes) - val encoded = try { - G722Codec.encodeSync(g722Handle, frame) - } catch (_: Exception) { - null - } - if (encoded != null) { - onSynthesisEncoded?.invoke(encoded) - } - cursor += frameBytes - } - - if (cursor > 0) { - val remaining = stashSize - cursor - if (remaining > 0) { - System.arraycopy(stashBuffer, cursor, stashBuffer, 0, remaining) - } - stashSize = remaining - } - - return data.size - } - - override fun close() { - stashSize = 0 - } - - fun reset() { - stashSize = 0 - } - } - - private val synthesisOutputCallback = MemoryPushAudioOutputStreamCallback() - - init { - val speechConfig = SpeechConfig.fromSubscription(SUBSCRIPTION_KEY, REGION) - speechConfig.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Raw16Khz16BitMonoPcm) - val outputStream = AudioOutputStream.createPushStream(synthesisOutputCallback) - val audioOutputConfig = AudioConfig.fromStreamOutput(outputStream) - synthesizer = SpeechSynthesizer(speechConfig, audioOutputConfig) - - buildAudioTrack() - } - - private fun buildAudioTrack() { - val bufSize = AudioTrack.getMinBufferSize( - 16000, - AudioFormat.CHANNEL_OUT_MONO, - AudioFormat.ENCODING_PCM_16BIT - ) - // forceBuiltInSpeaker(面对面翻译外放)走通信通路:只有 USAGE_VOICE_COMMUNICATION 才会被 - // MODE_IN_COMMUNICATION + setCommunicationDevice(内置扬声器) 正确路由到扬声器; - // USAGE_MEDIA 在小米/HyperOS 通话模式下会被压制,导致外放无声而接耳机正常。 - val usage = if (forceBuiltInSpeaker) { - AudioAttributes.USAGE_VOICE_COMMUNICATION - } else { - AudioAttributes.USAGE_MEDIA - } - audioTrack = AudioTrack.Builder() - .setAudioAttributes( - AudioAttributes.Builder() - .setUsage(usage) - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build() - ) - .setAudioFormat( - AudioFormat.Builder() - .setSampleRate(16000) - .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) - .setEncoding(AudioFormat.ENCODING_PCM_16BIT) - .build() - ) - .setBufferSizeInBytes(bufSize) - .setTransferMode(AudioTrack.MODE_STREAM) - .build() - applyPreferredDevice() - } - - private fun rebuildAudioTrack() { - try { - audioTrack?.release() - } catch (_: Exception) { - } - audioTrack = null - buildAudioTrack() - } - - override fun speakText(text: String, targetLanguage: String, voiceName: String) { - speakSsml(buildSsml(text = text, targetLanguage = targetLanguage, voiceName = voiceName)) - } - - fun speakSsml(ssml: String) { - synthesisOutputCallback.reset() - if (autoPlay) { - audioTrack?.play() - } - synthesizer?.SpeakSsmlAsync(ssml)?.get() - if (autoPlay) { - audioTrack?.stop() - } - } - - private fun applyPreferredDevice() { - val track = audioTrack ?: return - if (forceBuiltInSpeaker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - val speaker = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) - .firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } - track.preferredDevice = speaker - } else { - track.preferredDevice = null - } - } - - override fun dispose() { - try { - synthesizer?.close() - } catch (_: Exception) { - } - synthesizer = null - try { - audioTrack?.release() - } catch (_: Exception) { - } - audioTrack = null - try { - if (g722Handle != 0L) { - G722Codec.release(g722Handle) - g722Handle = 0L - } - } catch (_: Exception) { - } - } - - private fun buildSsml(text: String, targetLanguage: String, voiceName: String): String { - return """ - - - - $text - - - - """.trimIndent() - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/.gitignore b/apps/client/local_plugins/asr_tr_tts_manager/ios/.gitignore deleted file mode 100644 index 034771fc..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/.gitignore +++ /dev/null @@ -1,38 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/ephemeral/ -/Flutter/flutter_export_environment.sh diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager.podspec b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager.podspec deleted file mode 100644 index 7a81424b..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager.podspec +++ /dev/null @@ -1,41 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint asr_tr_tts_manager.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'asr_tr_tts_manager' - s.version = '0.0.1' - s.summary = 'ASR, Translation and TTS manager Flutter plugin.' - s.description = <<-DESC -Flutter plugin providing Azure-based ASR, Volcengine translation, and Azure TTS for call translation. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - s.source = { :path => '.' } - - s.source_files = 'asr_tr_tts_manager/Sources/asr_tr_tts_manager/**/*.swift' - - s.dependency 'Flutter' - # bluetooth_manager 提供 BluetoothManager.translationCallback / writeBytes / G722Codec - s.dependency 'bluetooth_manager' - # audio_recorder 提供 PCMReader 用于麦克风录音 - s.dependency 'audio_recorder' - # Azure 认知语音服务 SDK(ASR + TTS) - s.dependency 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.50.0' - - s.platform = :ios, '15.0' - - s.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES', - 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', - } - - s.swift_version = '5.0' - - # If your plugin requires a privacy manifest, for example if it uses any - # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your - # plugin's privacy impact, and then uncomment this line. For more information, - # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files - # s.resource_bundles = {'asr_tr_tts_manager_privacy' => ['asr_tr_tts_manager/Sources/asr_tr_tts_manager/PrivacyInfo.xcprivacy']} -end diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/AsrTrTtsHelper.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/AsrTrTtsHelper.swift deleted file mode 100644 index bb57c991..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/AsrTrTtsHelper.swift +++ /dev/null @@ -1,141 +0,0 @@ -import Foundation - -class AsrTrTtsHelper { - private let asrManager: AsrManager - private let translationManager: TranslationManager - private let ttsManager: TtsManager - - private var currentLanguages: [String] - private var sessionId = UUID().uuidString - private var lastRecognizingResult = AsrResult(text: "", language: "") - - private let workQueue = DispatchQueue(label: "asrtrtts.work", qos: .userInitiated) - - var autoDetectLanguage: Bool - - var callback: ((Dictionary) -> Void)? - var onSynthesisEncoded: ((Data) -> Void)? { - didSet { ttsManager.onSynthesisEncoded = onSynthesisEncoded } - } - - var autoPlay: Bool = false { - didSet { ttsManager.autoPlay = autoPlay } - } - - var forceBuiltInSpeaker: Bool = false { - didSet { ttsManager.forceBuiltInSpeaker = forceBuiltInSpeaker } - } - - init(asrProvider: AsrProvider = .azure, - translationProvider: TranslationProvider = .azure, - ttsProvider: TtsProvider = .azure, - languages: [String] = ["zh-CN", "en-US"], - autoPlay: Bool = false, - autoDetectLanguage: Bool = false, - forceBuiltInSpeaker: Bool = false) { - self.autoDetectLanguage = autoDetectLanguage - currentLanguages = languages - asrManager = AsrManager(provider: asrProvider, languages: languages, autoDetectLanguage: autoDetectLanguage) - translationManager = TranslationManager(provider: translationProvider) - ttsManager = TtsManager(provider: ttsProvider, autoPlay: autoPlay, forceBuiltInSpeaker: forceBuiltInSpeaker) - self.autoPlay = autoPlay - self.forceBuiltInSpeaker = forceBuiltInSpeaker - setupCallbacks() - } - - private func setupCallbacks() { - asrManager.onRecognizing = { [weak self] result in - guard let self = self else { return } - self.lastRecognizingResult = result - let sid = self.sessionId - self.callback?(["sessionId": sid, "type": "recognizing", "recognizing": result.text, "language": result.language]) - } - - asrManager.onRecognized = { [weak self] result in - guard let self = self else { return } - let sid = self.sessionId - let finalResult = result.text.isEmpty ? self.lastRecognizingResult : result - self.lastRecognizingResult = AsrResult(text: "", language: "") - self.sessionId = UUID().uuidString - - guard !finalResult.text.isEmpty else { return } - - let languagesSnapshot = self.currentLanguages - let sourceLanguage: LanguageInfo? - let targetLanguage: LanguageInfo? - - if self.autoDetectLanguage { - let detectedLanguage = LanguageManager.findByCode(finalResult.language) - if detectedLanguage != nil { - sourceLanguage = detectedLanguage - let other = languagesSnapshot.first { $0.caseInsensitiveCompare(finalResult.language) != .orderedSame } - targetLanguage = LanguageManager.findByCode(other) - } else { - sourceLanguage = LanguageManager.findByCode(languagesSnapshot.first) - targetLanguage = LanguageManager.findByCode(languagesSnapshot.last) - } - } else { - sourceLanguage = LanguageManager.findByCode(languagesSnapshot.first) - targetLanguage = LanguageManager.findByCode(languagesSnapshot.last) - } - - self.workQueue.async { - let translationResult = self.translationManager.translateText( - text: finalResult.text, - sourceLanguage: sourceLanguage?.language ?? "", - targetLanguage: targetLanguage?.language ?? "" - ) - let translated = translationResult.translatedText ?? "" - self.callback?(["sessionId": sid, "type": "recognized", "recognized": finalResult.text, "translated": translated, "language": finalResult.language]) - - if !translated.isEmpty, let target = targetLanguage { - self.ttsManager.speakText(text: translated, - targetLanguage: target.code, - voiceName: target.voiceName) - } - } - } - } - - func switchProvider(asrProvider: AsrProvider? = nil, - translationProvider: TranslationProvider? = nil, - ttsProvider: TtsProvider? = nil, - languages: [String]? = nil) { - if let languages = languages { - currentLanguages = languages - } - if asrProvider != nil || languages != nil { - asrManager.switchProvider(provider: asrProvider, languages: currentLanguages) - } - if let tp = translationProvider { - translationManager.switchProvider(provider: tp) - } - if let tp = ttsProvider { - ttsManager.switchProvider(provider: tp) - } - } - - /// 预热 TTS 自动播放引擎(须在麦克风 VPIO 启用前调用),让第一句翻译即保持最大音量。 - func prepareForPlayback() { - ttsManager.prepareForPlayback() - } - - func startRecognition() { - asrManager.startRecognition() - } - - func stopRecognition() { - asrManager.stopRecognition() - } - - func pushStreamWrite(_ bytes: Data) { - asrManager.pushStreamWrite(bytes) - } - - func dispose() { - callback = nil - onSynthesisEncoded = nil - ttsManager.dispose() - asrManager.dispose() - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/AsrTrTtsManagerPlugin.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/AsrTrTtsManagerPlugin.swift deleted file mode 100644 index 0b08f51a..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/AsrTrTtsManagerPlugin.swift +++ /dev/null @@ -1,122 +0,0 @@ -import Flutter -import UIKit - -// MARK: - Event Channel Handler - -private class TranslationStreamHandler: NSObject, FlutterStreamHandler { - weak var plugin: AsrTrTtsManagerPlugin? - - init(plugin: AsrTrTtsManagerPlugin) { self.plugin = plugin } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - plugin?.translationSink = events - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - plugin?.translationSink = nil - return nil - } -} - -// MARK: - Plugin - -public class AsrTrTtsManagerPlugin: NSObject, FlutterPlugin { - private var translationHelper: TranslationHelperManager? - var translationSink: FlutterEventSink? - - private var translationStreamHandler: TranslationStreamHandler? - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "asr_tr_tts_manager", - binaryMessenger: registrar.messenger() - ) - let eventChannel = FlutterEventChannel( - name: "asr_tr_tts_manager/translation", - binaryMessenger: registrar.messenger() - ) - - let instance = AsrTrTtsManagerPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - - instance.translationStreamHandler = TranslationStreamHandler(plugin: instance) - eventChannel.setStreamHandler(instance.translationStreamHandler) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "translationInit": - translationInit(call: call, result: result) - case "translationStart": - translationStart(call: call, result: result) - case "translationStop": - translationStop(result: result) - case "translationDispose": - translationDispose(result: result) - case "enableRecord": - enableRecord(call: call, result: result) - case "stopRecord": - stopRecord(call: call, result: result) - default: - result(FlutterMethodNotImplemented) - } - } - - // MARK: - Method Implementations - - private func translationInit(call: FlutterMethodCall, result: @escaping FlutterResult) { - let args = call.arguments as? [String: Any] - let providerStr = (args?["provider"] as? String ?? "CALL").uppercased() - let provider: TranslationHelperProvider - switch providerStr { - case "SIMULTANEOUS": provider = .simultaneous - case "FACETOFACE": provider = .faceToFace - default: provider = .call - } - translationHelper?.dispose() - translationHelper = TranslationHelperManager(provider: provider) - translationHelper?.callback = { [weak self] data in - self?.translationSink?(data) - } - result(true) - } - - private func translationStart(call: FlutterMethodCall, result: @escaping FlutterResult) { - let args = call.arguments as? [String: Any] - let sourceLanguage = args?["sourceLanguage"] as? String ?? "zh-CN" - let targetLanguage = args?["targetLanguage"] as? String ?? "en-US" - translationHelper?.start(sourceLanguage: sourceLanguage, targetLanguage: targetLanguage) - result(true) - } - - private func translationStop(result: @escaping FlutterResult) { - translationHelper?.stop() - result(true) - } - - private func translationDispose(result: @escaping FlutterResult) { - translationHelper?.dispose() - translationHelper = nil - result(true) - } - - // 开启录音:把当前激活的翻译模式正在处理的解码后 PCM 写入 WAV 文件 - private func enableRecord(call: FlutterMethodCall, result: @escaping FlutterResult) { - let args = call.arguments as? [String: Any] - guard let filePath = args?["filePath"] as? String else { - result(FlutterError(code: "INVALID_ARGS", message: "filePath is required", details: nil)) - return - } - translationHelper?.enableRecord(filePath: filePath) - result(true) - } - - // 停止录音,isSave=false 时丢弃文件 - private func stopRecord(call: FlutterMethodCall, result: @escaping FlutterResult) { - let args = call.arguments as? [String: Any] - let isSave = args?["isSave"] as? Bool ?? true - translationHelper?.stopRecord(isSave: isSave) - result(true) - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/LanguageManager.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/LanguageManager.swift deleted file mode 100644 index 5570b11d..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/LanguageManager.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Foundation - -public struct LanguageInfo { - public let code: String - public let language: String - public let voiceName: String -} - -public enum LanguageManager { - public static let all: [LanguageInfo] = [ - LanguageInfo(code: "zh-CN", language: "zh", voiceName: "zh-CN-XiaoxiaoNeural"), - LanguageInfo(code: "zh-TW", language: "zh-Hant", voiceName: "zh-TW-HsiaoChenNeural"), - LanguageInfo(code: "zh-HK", language: "zh-Hant-hk",voiceName: "zh-HK-HiuMaanNeural"), - LanguageInfo(code: "en-US", language: "en", voiceName: "en-US-AriaNeural"), - LanguageInfo(code: "ja-JP", language: "ja", voiceName: "ja-JP-NanamiNeural"), - LanguageInfo(code: "ko-KR", language: "ko", voiceName: "ko-KR-SunHiNeural"), - LanguageInfo(code: "fr-FR", language: "fr", voiceName: "fr-FR-DeniseNeural"), - LanguageInfo(code: "es-ES", language: "es", voiceName: "es-ES-ElviraNeural"), - LanguageInfo(code: "pt-BR", language: "pt", voiceName: "pt-BR-FranciscaNeural"), - LanguageInfo(code: "it-IT", language: "it", voiceName: "it-IT-ElsaNeural"), - LanguageInfo(code: "de-DE", language: "de", voiceName: "de-DE-KatjaNeural"), - LanguageInfo(code: "ru-RU", language: "ru", voiceName: "ru-RU-SvetlanaNeural"), - LanguageInfo(code: "pl-PL", language: "pl", voiceName: "pl-PL-AgnieszkaNeural"), - LanguageInfo(code: "nl-NL", language: "nl", voiceName: "nl-NL-ColetteNeural"), - LanguageInfo(code: "sv-SE", language: "sv", voiceName: "sv-SE-SofieNeural"), - LanguageInfo(code: "cs-CZ", language: "cs", voiceName: "cs-CZ-VlastaNeural"), - LanguageInfo(code: "el-GR", language: "el", voiceName: "el-GR-AthinaNeural"), - LanguageInfo(code: "ro-RO", language: "ro", voiceName: "ro-RO-AlinaNeural"), - LanguageInfo(code: "hu-HU", language: "hu", voiceName: "hu-HU-NoemiNeural"), - LanguageInfo(code: "uk-UA", language: "uk", voiceName: "uk-UA-PolinaNeural"), - LanguageInfo(code: "da-DK", language: "da", voiceName: "da-DK-ChristelNeural"), - LanguageInfo(code: "fi-FI", language: "fi", voiceName: "fi-FI-NooraNeural"), - LanguageInfo(code: "nb-NO", language: "nb", voiceName: "nb-NO-IselinNeural"), - LanguageInfo(code: "hr-HR", language: "hr", voiceName: "hr-HR-GabrijelaNeural"), - LanguageInfo(code: "ca-ES", language: "ca", voiceName: "ca-ES-JoanaNeural"), - LanguageInfo(code: "ar-EG", language: "ar", voiceName: "ar-EG-SalmaNeural"), - LanguageInfo(code: "th-TH", language: "th", voiceName: "th-TH-PremwadeeNeural"), - LanguageInfo(code: "vi-VN", language: "vi", voiceName: "vi-VN-HoaiMyNeural"), - LanguageInfo(code: "id-ID", language: "id", voiceName: "id-ID-GadisNeural"), - LanguageInfo(code: "hi-IN", language: "hi", voiceName: "hi-IN-SwaraNeural"), - LanguageInfo(code: "tr-TR", language: "tr", voiceName: "tr-TR-EmelNeural"), - LanguageInfo(code: "ms-MY", language: "ms", voiceName: "ms-MY-YasminNeural"), - LanguageInfo(code: "bn-IN", language: "bn", voiceName: "bn-IN-TanishaaNeural"), - LanguageInfo(code: "ta-IN", language: "ta", voiceName: "ta-IN-PallaviNeural"), - LanguageInfo(code: "te-IN", language: "te", voiceName: "te-IN-ShrutiNeural"), - LanguageInfo(code: "mr-IN", language: "mr", voiceName: "mr-IN-AarohiNeural"), - LanguageInfo(code: "ur-IN", language: "ur", voiceName: "ur-IN-GulNeural"), - LanguageInfo(code: "fa-IR", language: "fa", voiceName: "fa-IR-DilaraNeural"), - LanguageInfo(code: "sw-KE", language: "sw", voiceName: "sw-KE-ZuriNeural"), - ] - - private static let byCode: [String: LanguageInfo] = Dictionary(uniqueKeysWithValues: all.map { (normalize($0.code), $0) }) - - public static func findByCode(_ code: String?) -> LanguageInfo? { - guard let code = code, !code.isEmpty else { return nil } - return byCode[normalize(code)] - } - - private static func normalize(_ value: String) -> String { - value.trimmingCharacters(in: .whitespaces).replacingOccurrences(of: "_", with: "-").lowercased() - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/PrivacyInfo.xcprivacy b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/PrivacyInfo.xcprivacy deleted file mode 100644 index a34b7e2e..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/PrivacyInfo.xcprivacy +++ /dev/null @@ -1,14 +0,0 @@ - - - - - NSPrivacyTrackingDomains - - NSPrivacyAccessedAPITypes - - NSPrivacyCollectedDataTypes - - NSPrivacyTracking - - - diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/VolcengineHelper.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/VolcengineHelper.swift deleted file mode 100644 index e810a8f1..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/VolcengineHelper.swift +++ /dev/null @@ -1,782 +0,0 @@ -import Foundation -import bluetooth_manager - -final class VolcengineHelper: NSObject { - // MARK: - 常量 - - private static let TAG = "VolcengineHelper" - private static let WS_URL = "wss://openspeech.bytedance.com/api/v4/ast/v2/translate" - - private static let APP_KEY = "8595121680" - private static let ACCESS_KEY = "ERAUYbJQBOv6M0ooFnUwo-tTWs08HWZn" - private static let RESOURCE_ID = "volc.service_type.10053" - - private static let EVENT_START_SESSION = 100 - private static let EVENT_FINISH_SESSION = 102 - private static let EVENT_SESSION_STARTED = 150 - private static let EVENT_SESSION_CANCELED = 151 - private static let EVENT_SESSION_FINISHED = 152 - private static let EVENT_SESSION_FAILED = 153 - private static let EVENT_TASK_REQUEST = 200 - private static let EVENT_SOURCE_SUBTITLE_START = 650 - private static let EVENT_SOURCE_SUBTITLE_RESPONSE = 651 - private static let EVENT_SOURCE_SUBTITLE_END = 652 - private static let EVENT_TRANSLATION_SUBTITLE_START = 653 - private static let EVENT_TRANSLATION_SUBTITLE_RESPONSE = 654 - private static let EVENT_TRANSLATION_SUBTITLE_END = 655 - private static let EVENT_TTS_SENTENCE_START = 350 - private static let EVENT_TTS_RESPONSE = 352 - private static let EVENT_TTS_SENTENCE_END = 351 - - private static let FRAME_PCM_BYTES = 640 // 16kHz × 20ms × 2 bytes - - private static let MAX_RECONNECT_ATTEMPTS = 3 - private static let RECONNECT_BASE_DELAY_MS = 1000 // 1s / 2s / 4s 指数退避 - - private static let FINISH_SESSION_TIMEOUT_MS = 5000 - - private static let PING_INTERVAL_SECONDS: TimeInterval = 15 - - // 预构建 TaskRequest header(event=200),所有实例共享:3 字节 [0x10, 0xC8, 0x01] - private static let taskRequestHeader: Data = { - let pw = ProtoWriter() - pw.writeVarint(field: 2, value: Int64(EVENT_TASK_REQUEST)) - return pw.toData() - }() - - // MARK: - 回调 - - var callback: ((Dictionary) -> Void)? - var onSynthesisEncoded: ((Data) -> Void)? - - // MARK: - 状态(workQueue 串行化) - - private var currentLanguages: [String] - private var session: URLSession? - private var webSocketTask: URLSessionWebSocketTask? - private var isRecognizing = false - private var isConnected = false - private var sessionId = UUID().uuidString - private var connectionId = UUID().uuidString - private var segmentId = UUID().uuidString - private var currentSourceText = "" - - private var reconnectAttempts = 0 - private var reconnectWork: DispatchWorkItem? - private var finishTimeoutWork: DispatchWorkItem? - private var pingTimer: DispatchSourceTimer? - - private let workQueue = DispatchQueue(label: "volcengine.helper.work", qos: .userInitiated) - - // MARK: - 音频 G722 编码(所有访问都在 workQueue 上,无需额外加锁) - - private var g722: G722Codec? - private var stashBuffer = Data(count: 8192) - private var stashSize = 0 - - // MARK: - 初始化 - - init(languages: [String] = ["zh-CN", "en-US"]) { - self.currentLanguages = languages - super.init() - } - - // MARK: - 对外 API - - func switchProvider(languages: [String]?) { - workQueue.async { [weak self] in - guard let self = self else { return } - if let langs = languages { - self.currentLanguages = langs - } - } - } - - func startRecognition() { - workQueue.async { [weak self] in - guard let self = self else { return } - if self.isRecognizing { return } - self.isRecognizing = true - self.isConnected = false - self.reconnectAttempts = 0 - self.cancelReconnect() - self.cancelFinishTimeout() - self.sessionId = UUID().uuidString - self.connectionId = UUID().uuidString - self.segmentId = UUID().uuidString - self.currentSourceText = "" - - self.releaseG722() - self.stashSize = 0 - - self.connectWebSocket() - } - } - - func stopRecognition() { - workQueue.async { [weak self] in - guard let self = self else { return } - if !self.isRecognizing { return } - self.isRecognizing = false - self.cancelReconnect() - self.sendFinishSession() - - // 等待服务端返回 SESSION_FINISHED/SESSION_CANCELED 后关闭,超时兜底 - let work = DispatchWorkItem { [weak self] in - guard let self = self else { return } - if self.webSocketTask != nil { - print("[\(VolcengineHelper.TAG)] FinishSession timeout, force closing WebSocket") - self.closeWebSocket() - } - } - self.finishTimeoutWork = work - self.workQueue.asyncAfter( - deadline: .now() + .milliseconds(VolcengineHelper.FINISH_SESSION_TIMEOUT_MS), - execute: work - ) - } - } - - func pushStreamWrite(_ bytes: Data) { - // 热路径,不入队列;依赖 URLSessionWebSocketTask.send 自身线程安全 - if !isRecognizing || !isConnected { return } - sendTaskRequest(bytes) - } - - func dispose() { - workQueue.async { [weak self] in - guard let self = self else { return } - self.isRecognizing = false - self.isConnected = false - self.callback = nil - self.onSynthesisEncoded = nil - self.cancelReconnect() - self.cancelFinishTimeout() - self.closeWebSocket() - - self.releaseG722() - self.stashSize = 0 - } - } - - // MARK: - WebSocket 连接管理 - - private func connectWebSocket() { - closeWebSocket() - - guard let url = URL(string: VolcengineHelper.WS_URL) else { - print("[\(VolcengineHelper.TAG)] Invalid WS_URL") - return - } - - var request = URLRequest(url: url) - request.setValue(VolcengineHelper.APP_KEY, forHTTPHeaderField: "X-Api-App-Key") - request.setValue(VolcengineHelper.ACCESS_KEY, forHTTPHeaderField: "X-Api-Access-Key") - request.setValue(VolcengineHelper.RESOURCE_ID, forHTTPHeaderField: "X-Api-Resource-Id") - - let config = URLSessionConfiguration.default - config.timeoutIntervalForRequest = 10 - let newSession = URLSession(configuration: config, delegate: self, delegateQueue: nil) - let task = newSession.webSocketTask(with: request) - self.session = newSession - self.webSocketTask = task - - receiveLoop(task: task) - task.resume() - startPingTimer() - } - - private func closeWebSocket() { - stopPingTimer() - let task = self.webSocketTask - self.webSocketTask = nil - self.isConnected = false - task?.cancel(with: .normalClosure, reason: "normal close".data(using: .utf8)) - - // 释放 session(打破 URLSession → delegate 强引用) - let s = self.session - self.session = nil - s?.finishTasksAndInvalidate() - } - - private func receiveLoop(task: URLSessionWebSocketTask) { - task.receive { [weak self, weak task] result in - guard let self = self, let task = task else { return } - switch result { - case .success(let message): - switch message { - case .data(let data): - self.workQueue.async { self.parseResponse(data) } - case .string(let text): - print("[\(VolcengineHelper.TAG)] Received text: \(text)") - @unknown default: - break - } - // 只要当前仍是同一个 task,继续递归监听 - if task.state == .running { - self.receiveLoop(task: task) - } - case .failure(let error): - self.workQueue.async { - self.handleConnectionFailure(error: error) - } - } - } - } - - private func handleConnectionFailure(error: Error) { - isConnected = false - // 主动停止时的取消失败是预期行为,静默丢弃,不抛 error 事件 - if !isRecognizing { - print("[\(VolcengineHelper.TAG)] WebSocket closed while stopping: \(error.localizedDescription)") - return - } - print("[\(VolcengineHelper.TAG)] WebSocket failure: \(error.localizedDescription)") - if reconnectAttempts < VolcengineHelper.MAX_RECONNECT_ATTEMPTS { - let delay = VolcengineHelper.RECONNECT_BASE_DELAY_MS << reconnectAttempts - reconnectAttempts += 1 - print("[\(VolcengineHelper.TAG)] Reconnect attempt \(reconnectAttempts) after \(delay)ms...") - let work = DispatchWorkItem { [weak self] in - guard let self = self else { return } - if self.isRecognizing { self.connectWebSocket() } - } - reconnectWork = work - workQueue.asyncAfter(deadline: .now() + .milliseconds(delay), execute: work) - } else { - // 重连用尽:清理 task/session/pingTimer,打破 URLSession → delegate 强引用。 - // 保留 isRecognizing=true 与 Android 一致(要求用户主动 stop 确认错误后再 start) - closeWebSocket() - callback?([ - "type": "error", - "message": error.localizedDescription, - ]) - } - } - - private func cancelReconnect() { - reconnectWork?.cancel() - reconnectWork = nil - } - - private func cancelFinishTimeout() { - finishTimeoutWork?.cancel() - finishTimeoutWork = nil - } - - private func startPingTimer() { - stopPingTimer() - let timer = DispatchSource.makeTimerSource(queue: workQueue) - timer.schedule( - deadline: .now() + VolcengineHelper.PING_INTERVAL_SECONDS, - repeating: VolcengineHelper.PING_INTERVAL_SECONDS - ) - timer.setEventHandler { [weak self] in - guard let self = self, let task = self.webSocketTask else { return } - task.sendPing { err in - if let err = err { - print("[\(VolcengineHelper.TAG)] Ping failed: \(err.localizedDescription)") - } - } - } - timer.resume() - pingTimer = timer - } - - private func stopPingTimer() { - pingTimer?.cancel() - pingTimer = nil - } - - // MARK: - 发送 Protobuf 消息 - - private func sendStartSession() { - // handshake 完成前若已 stop,直接关闭、不开 session,避免服务端回的 - // SESSION_STARTED 把 isConnected 置回 true 产生幻觉状态 - if !isRecognizing { - closeWebSocket() - return - } - let langs = currentLanguages - if langs.count < 2 { - print("[\(VolcengineHelper.TAG)] Need at least 2 languages, got: \(langs)") - isRecognizing = false - isConnected = false - callback?([ - "type": "error", - "message": "Need source and target language", - ]) - closeWebSocket() - return - } - let sourceCode = langs[0] - let targetCode = langs[1] - let sourceLang = LanguageManager.findByCode(sourceCode)?.language ?? "zh" - let targetLang = LanguageManager.findByCode(targetCode)?.language ?? "en" - - let pw = ProtoWriter() - - // field 1: request_meta (RequestMeta) - let metaPw = ProtoWriter() - metaPw.writeString(field: 2, value: VolcengineHelper.APP_KEY) - metaPw.writeString(field: 4, value: VolcengineHelper.RESOURCE_ID) - metaPw.writeString(field: 5, value: connectionId) - metaPw.writeString(field: 6, value: sessionId) - pw.writeBytes(field: 1, value: metaPw.toData()) - - // field 2: event (enum Type = StartSession = 100) - pw.writeVarint(field: 2, value: Int64(VolcengineHelper.EVENT_START_SESSION)) - - // field 3: user (User) - let userPw = ProtoWriter() - userPw.writeString(field: 1, value: "deapvoice_ios") - userPw.writeString(field: 3, value: "ios") - pw.writeBytes(field: 3, value: userPw.toData()) - - // field 4: source_audio (Audio) - let srcAudioPw = ProtoWriter() - srcAudioPw.writeString(field: 4, value: "pcm") - srcAudioPw.writeVarint(field: 7, value: 16000) - srcAudioPw.writeVarint(field: 8, value: 16) - srcAudioPw.writeVarint(field: 9, value: 1) - pw.writeBytes(field: 4, value: srcAudioPw.toData()) - - // field 5: target_audio (Audio) - let tgtAudioPw = ProtoWriter() - tgtAudioPw.writeString(field: 4, value: "pcm") - tgtAudioPw.writeVarint(field: 7, value: 16000) - tgtAudioPw.writeVarint(field: 8, value: 16) - tgtAudioPw.writeVarint(field: 9, value: 1) - pw.writeBytes(field: 5, value: tgtAudioPw.toData()) - - // field 6: request (ast.ReqParams) - let reqPw = ProtoWriter() - reqPw.writeString(field: 1, value: "s2s") - reqPw.writeString(field: 2, value: sourceLang) - reqPw.writeString(field: 3, value: targetLang) - pw.writeBytes(field: 6, value: reqPw.toData()) - - let data = pw.toData() - webSocketTask?.send(.data(data)) { err in - if let err = err { - print("[\(VolcengineHelper.TAG)] Send StartSession failed: \(err.localizedDescription)") - } - } - print("[\(VolcengineHelper.TAG)] Sent StartSession (\(data.count) bytes), session=\(sessionId), \(sourceLang)→\(targetLang)") - } - - private func sendTaskRequest(_ audioData: Data) { - let audioLen = audioData.count - // 外层嵌套结构:[header] field4(length=innerSize) { field14(length=audioLen) = audioData } - let innerSize = 1 + varintByteCount(audioLen) + audioLen - let outerSize = 1 + varintByteCount(innerSize) + innerSize - - var buf = Data() - buf.reserveCapacity(VolcengineHelper.taskRequestHeader.count + outerSize) - buf.append(VolcengineHelper.taskRequestHeader) - - buf.append(UInt8((4 << 3) | 2)) // field 4, wire type 2 - appendVarint(&buf, value: innerSize) - buf.append(UInt8((14 << 3) | 2)) // field 14, wire type 2 - appendVarint(&buf, value: audioLen) - buf.append(audioData) - - webSocketTask?.send(.data(buf)) { [weak self] err in - if let err = err { - print("[\(VolcengineHelper.TAG)] sendTaskRequest failed: \(err.localizedDescription)") - self?.workQueue.async { self?.isConnected = false } - } - } - } - - private func varintByteCount(_ value: Int) -> Int { - var v = UInt(bitPattern: value) - var size = 1 - while (v & ~0x7F) != 0 { - size += 1 - v >>= 7 - } - return size - } - - private func appendVarint(_ buf: inout Data, value: Int) { - var v = UInt(bitPattern: value) - while (v & ~0x7F) != 0 { - buf.append(UInt8((v & 0x7F) | 0x80)) - v >>= 7 - } - buf.append(UInt8(v & 0x7F)) - } - - private func sendFinishSession() { - let pw = ProtoWriter() - pw.writeVarint(field: 2, value: Int64(VolcengineHelper.EVENT_FINISH_SESSION)) - let data = pw.toData() - webSocketTask?.send(.data(data)) { err in - if let err = err { - print("[\(VolcengineHelper.TAG)] Send FinishSession failed: \(err.localizedDescription)") - } - } - print("[\(VolcengineHelper.TAG)] Sent FinishSession") - } - - // MARK: - 解析 Protobuf 响应 - - private func parseResponse(_ data: Data) { - do { - var pr = ProtoReader(data: data) - var event = 0 - var responseData: Data? - var text = "" - var statusCode = 0 - var statusMessage = "" - - while pr.hasMore { - let tag = try pr.readTag() - let fieldNumber = tag >> 3 - let wireType = tag & 0x7 - - switch fieldNumber { - case 1: // response_meta - let metaBytes = try pr.readLengthDelimited() - var metaPr = ProtoReader(data: metaBytes) - while metaPr.hasMore { - let metaTag = try metaPr.readTag() - let metaField = metaTag >> 3 - let metaWire = metaTag & 0x7 - switch metaField { - case 3: - statusCode = Int(try metaPr.readVarint()) - case 4: - statusMessage = try metaPr.readStringField(wireType: metaWire) - default: - try metaPr.skip(wireType: metaWire) - } - } - case 2: - event = Int(try pr.readVarint()) - case 3: - responseData = try pr.readLengthDelimited() - case 4: - text = try pr.readStringField(wireType: wireType) - default: - try pr.skip(wireType: wireType) - } - } - - handleEvent( - event: event, - text: text, - audioData: responseData, - statusCode: statusCode, - statusMessage: statusMessage - ) - } catch { - print("[\(VolcengineHelper.TAG)] Parse response failed: \(error.localizedDescription)") - } - } - - private func handleEvent( - event: Int, - text: String, - audioData: Data?, - statusCode: Int, - statusMessage: String - ) { - switch event { - case VolcengineHelper.EVENT_SESSION_STARTED: - print("[\(VolcengineHelper.TAG)] Session started") - isConnected = true - reconnectAttempts = 0 - - case VolcengineHelper.EVENT_SESSION_FAILED: - print("[\(VolcengineHelper.TAG)] Session failed: \(statusCode) \(statusMessage)") - isConnected = false - cancelReconnect() - closeWebSocket() - callback?([ - "type": "error", - "message": "Session failed: \(statusCode) \(statusMessage)", - ]) - - case VolcengineHelper.EVENT_SESSION_FINISHED: - print("[\(VolcengineHelper.TAG)] Session finished") - isConnected = false - cancelReconnect() - closeWebSocket() - - case VolcengineHelper.EVENT_SESSION_CANCELED: - print("[\(VolcengineHelper.TAG)] Session canceled") - isConnected = false - cancelReconnect() - closeWebSocket() - - case VolcengineHelper.EVENT_SOURCE_SUBTITLE_START: - currentSourceText = "" - - case VolcengineHelper.EVENT_SOURCE_SUBTITLE_RESPONSE: - currentSourceText = text - if !text.isEmpty { - let sourceCode = currentLanguages.first ?? "zh-CN" - callback?([ - "sessionId": segmentId, - "type": "recognizing", - "recognizing": text, - "language": sourceCode, - ]) - } - - case VolcengineHelper.EVENT_SOURCE_SUBTITLE_END: - if !text.isEmpty { - currentSourceText = text - } - - case VolcengineHelper.EVENT_TRANSLATION_SUBTITLE_RESPONSE: - if let audio = audioData, !audio.isEmpty { - handleAudioData(audio) - } - - case VolcengineHelper.EVENT_TRANSLATION_SUBTITLE_END: - if let audio = audioData, !audio.isEmpty { - handleAudioData(audio) - } - flushAudioStash() - - let sourceCode = currentLanguages.first ?? "zh-CN" - let sourceText = currentSourceText - let translatedText = text - if !sourceText.isEmpty || !translatedText.isEmpty { - callback?([ - "sessionId": segmentId, - "type": "recognized", - "recognized": sourceText, - "translated": translatedText, - "language": sourceCode, - ]) - segmentId = UUID().uuidString - currentSourceText = "" - } - - case VolcengineHelper.EVENT_TTS_RESPONSE: - if let audio = audioData, !audio.isEmpty { - handleAudioData(audio) - } - - case VolcengineHelper.EVENT_TTS_SENTENCE_END: - flushAudioStash() - - default: - if statusCode != 0 && !statusMessage.isEmpty { - print("[\(VolcengineHelper.TAG)] Event \(event) with status \(statusCode): \(statusMessage)") - } - } - } - - // MARK: - 音频 G722 编码 - - private func handleAudioData(_ pcmData: Data) { - guard let encodedCb = onSynthesisEncoded, !pcmData.isEmpty else { return } - - if g722 == nil { - g722 = G722Codec(sampleRate: 16000) - } - - let required = stashSize + pcmData.count - if required > stashBuffer.count { - var newCap = stashBuffer.count - while newCap < required { newCap <<= 1 } - var newBuf = Data(count: newCap) - newBuf.replaceSubrange(0..= frameBytes { - // slice 下标共享底层存储,比 subdata(in:) 少一次 640 字节拷贝 - let frame = stashBuffer[cursor.. 0 { - let remaining = stashSize - cursor - if remaining > 0 { - // 用 subdata 避免源/目标 slice 共享存储引发的 aliasing(保守但可读) - let tail = stashBuffer.subdata(in: cursor..>= 7 - } - buffer.append(UInt8(v & 0x7F)) - } - - func toData() -> Data { buffer } - } - - // MARK: - Protobuf 解码工具 - - private struct ProtoReader { - let data: Data - private var pos: Int = 0 - - init(data: Data) { self.data = data } - - var hasMore: Bool { pos < data.count } - - mutating func readTag() throws -> Int { Int(try readRawVarint()) } - - mutating func readVarint() throws -> Int64 { try readRawVarint() } - - mutating func readLengthDelimited() throws -> Data { - let length = Int(try readRawVarint()) - if length < 0 || pos + length > data.count { - throw VolcengineProtoError.invalidLength(length: length, pos: pos, size: data.count) - } - let result = data.subdata(in: pos.. String { - if wireType == 2 { - let bytes = try readLengthDelimited() - return String(data: bytes, encoding: .utf8) ?? "" - } else { - _ = try readRawVarint() - return "" - } - } - - mutating func skip(wireType: Int) throws { - switch wireType { - case 0: - _ = try readRawVarint() - case 1: - pos += 8 - case 2: - let len = Int(try readRawVarint()) - pos += len - case 5: - pos += 4 - default: - throw VolcengineProtoError.unknownWireType(wireType) - } - } - - private mutating func readRawVarint() throws -> Int64 { - var result: UInt64 = 0 - var shift = 0 - while pos < data.count { - let b = UInt64(data[pos]) - pos += 1 - result |= (b & 0x7F) << shift - if (b & 0x80) == 0 { - return Int64(bitPattern: result) - } - shift += 7 - if shift >= 64 { - throw VolcengineProtoError.varintTooLong - } - } - return Int64(bitPattern: result) - } - } - - private enum VolcengineProtoError: Error { - case invalidLength(length: Int, pos: Int, size: Int) - case unknownWireType(Int) - case varintTooLong - } -} - -// MARK: - URLSessionWebSocketDelegate - -extension VolcengineHelper: URLSessionWebSocketDelegate { - func urlSession( - _ session: URLSession, - webSocketTask: URLSessionWebSocketTask, - didOpenWithProtocol protocolName: String? - ) { - workQueue.async { [weak self] in - guard let self = self else { return } - // 避免旧 task 的迟到回调 - guard self.webSocketTask === webSocketTask else { return } - print("[\(VolcengineHelper.TAG)] WebSocket connected") - self.sendStartSession() - } - } - - func urlSession( - _ session: URLSession, - webSocketTask: URLSessionWebSocketTask, - didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, - reason: Data? - ) { - workQueue.async { [weak self] in - guard let self = self else { return } - guard self.webSocketTask === webSocketTask else { return } - let reasonStr = reason.flatMap { String(data: $0, encoding: .utf8) } ?? "" - print("[\(VolcengineHelper.TAG)] WebSocket closed: \(closeCode.rawValue) \(reasonStr)") - self.isConnected = false - } - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/AsrManager.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/AsrManager.swift deleted file mode 100644 index 53b0cde4..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/AsrManager.swift +++ /dev/null @@ -1,56 +0,0 @@ -import Foundation - -class AsrManager: AsrProtocol { - private var currentProvider: AsrProvider - private var currentLanguages: [String] - private var autoDetectLanguage: Bool - private var delegate: AsrProtocol? - - var onRecognizing: ((AsrResult) -> Void)? { - didSet { delegate?.onRecognizing = onRecognizing } - } - var onRecognized: ((AsrResult) -> Void)? { - didSet { delegate?.onRecognized = onRecognized } - } - - init(provider: AsrProvider = .azure, languages: [String] = ["zh-CN", "en-US"], autoDetectLanguage: Bool = false) { - self.currentProvider = provider - self.currentLanguages = languages - self.autoDetectLanguage = autoDetectLanguage - createDelegate() - } - - func switchProvider(provider: AsrProvider? = nil, languages: [String]? = nil, autoDetectLanguage: Bool? = nil) { - if let p = provider { currentProvider = p } - if let l = languages { currentLanguages = l } - if let a = autoDetectLanguage { self.autoDetectLanguage = a } - delegate?.dispose() - createDelegate() - } - - private func createDelegate() { - switch currentProvider { - case .azure: - delegate = AzureAsr(languages: currentLanguages, autoDetectLanguage: autoDetectLanguage) - case .volcengine: - delegate = VolcengineAsr(languages: currentLanguages, autoDetectLanguage: autoDetectLanguage) - } - delegate?.onRecognizing = onRecognizing - delegate?.onRecognized = onRecognized - } - - func getProvider() -> AsrProvider { currentProvider } - - func startRecognition() { delegate?.startRecognition() } - func stopRecognition() { delegate?.stopRecognition() } - func pushStreamWrite(_ bytes: Data) { delegate?.pushStreamWrite(bytes) } - func pushStreamClose() { delegate?.pushStreamClose() } - - func dispose() { - onRecognizing = nil - onRecognized = nil - let d = delegate - delegate = nil - d?.dispose() - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/AsrProtocol.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/AsrProtocol.swift deleted file mode 100644 index b12aab56..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/AsrProtocol.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation - -enum AsrProvider { - case azure - case volcengine -} - -struct AsrResult { - let text: String - let language: String -} - -protocol AsrProtocol: AnyObject { - var onRecognizing: ((AsrResult) -> Void)? { get set } - var onRecognized: ((AsrResult) -> Void)? { get set } - - func startRecognition() - func stopRecognition() - func pushStreamWrite(_ bytes: Data) - func pushStreamClose() - func dispose() -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/AzureAsr.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/AzureAsr.swift deleted file mode 100644 index 12ee230b..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/AzureAsr.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Foundation -import MicrosoftCognitiveServicesSpeech - -class AzureAsr: AsrProtocol { - var onRecognizing: ((AsrResult) -> Void)? - var onRecognized: ((AsrResult) -> Void)? - - private var pushStream: SPXPushAudioInputStream? - private var recognizer: SPXSpeechRecognizer? - - private let subscriptionKey = "rLPdyGgD1v7z7khzpdrd4YNWUinjw6GzAbA1pPXBRmUjitfjDxdsJQQJ99BGAC3pKaRXJ3w3AAAYACOGAHZZ" - private let region = "eastasia" - - private let workQueue = DispatchQueue(label: "asr.work", qos: .userInitiated) - private let autoDetectLanguage: Bool - private let languages: [String] - - init(languages: [String] = ["zh-CN", "en-US"], autoDetectLanguage: Bool = false) { - self.autoDetectLanguage = autoDetectLanguage - self.languages = languages - setup(languages: languages) - } - - private func setup(languages: [String]) { - do { - let speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) - guard let audioFormat = SPXAudioStreamFormat(usingPCMWithSampleRate: 16000, bitsPerSample: 16, channels: 1) else { return } - guard let stream = try SPXPushAudioInputStream(audioFormat: audioFormat) else { return } - guard let audioConfig = try SPXAudioConfiguration(streamInput: stream) else { return } - - let rec: SPXSpeechRecognizer - if autoDetectLanguage { - try speechConfig.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode) - let autoDetect = try SPXAutoDetectSourceLanguageConfiguration(languages) - rec = try SPXSpeechRecognizer( - speechConfiguration: speechConfig, - autoDetectSourceLanguageConfiguration: autoDetect, - audioConfiguration: audioConfig - ) - } else { - speechConfig.speechRecognitionLanguage = languages.first ?? "zh-CN" - rec = try SPXSpeechRecognizer( - speechConfiguration: speechConfig, - audioConfiguration: audioConfig - ) - } - - rec.addRecognizingEventHandler { [weak self] (_: SPXSpeechRecognizer, event: SPXSpeechRecognitionEventArgs) in - guard let self = self else { return } - let text = event.result.text?.trimmingCharacters(in: CharacterSet.whitespaces) ?? "" - guard !text.isEmpty else { return } - let language: String - if self.autoDetectLanguage { - language = SPXAutoDetectSourceLanguageResult(event.result).language ?? "" - } else { - language = self.languages.first ?? "" - } - self.onRecognizing?(AsrResult(text: text, language: language)) - } - - rec.addRecognizedEventHandler { [weak self] (_: SPXSpeechRecognizer, event: SPXSpeechRecognitionEventArgs) in - guard let self = self else { return } - guard event.result.reason == SPXResultReason.recognizedSpeech else { return } - let text = event.result.text?.trimmingCharacters(in: CharacterSet.whitespaces) ?? "" - let language: String - if self.autoDetectLanguage { - language = SPXAutoDetectSourceLanguageResult(event.result).language ?? "" - } else { - language = self.languages.first ?? "" - } - self.onRecognized?(AsrResult(text: text, language: language)) - } - - self.pushStream = stream - self.recognizer = rec - } catch { - } - } - - func startRecognition() { - workQueue.async { [weak self] in - try? self?.recognizer?.startContinuousRecognition() - } - } - - func stopRecognition() { - workQueue.async { [weak self] in - try? self?.recognizer?.stopContinuousRecognition() - } - } - - func pushStreamWrite(_ bytes: Data) { - pushStream?.write(bytes) - } - - func pushStreamClose() { - pushStream?.close() - } - - func dispose() { - workQueue.async { [weak self] in - try? self?.recognizer?.stopContinuousRecognition() - self?.pushStream?.close() - self?.recognizer = nil - self?.pushStream = nil - } - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/VolcengineAsr.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/VolcengineAsr.swift deleted file mode 100644 index fc162547..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/asr/VolcengineAsr.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Foundation -import MicrosoftCognitiveServicesSpeech - -class VolcengineAsr: AsrProtocol { - var onRecognizing: ((AsrResult) -> Void)? - var onRecognized: ((AsrResult) -> Void)? - - private var pushStream: SPXPushAudioInputStream? - private var recognizer: SPXSpeechRecognizer? - - private let subscriptionKey = "rLPdyGgD1v7z7khzpdrd4YNWUinjw6GzAbA1pPXBRmUjitfjDxdsJQQJ99BGAC3pKaRXJ3w3AAAYACOGAHZZ" - private let region = "eastasia" - - private let workQueue = DispatchQueue(label: "asr.volcengine.work", qos: .userInitiated) - private let autoDetectLanguage: Bool - private let languages: [String] - - init(languages: [String] = ["zh-CN", "en-US"], autoDetectLanguage: Bool = false) { - self.autoDetectLanguage = autoDetectLanguage - self.languages = languages - setup(languages: languages) - } - - private func setup(languages: [String]) { - do { - let speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) - guard let audioFormat = SPXAudioStreamFormat(usingPCMWithSampleRate: 16000, bitsPerSample: 16, channels: 1) else { return } - guard let stream = try SPXPushAudioInputStream(audioFormat: audioFormat) else { return } - guard let audioConfig = try SPXAudioConfiguration(streamInput: stream) else { return } - - let rec: SPXSpeechRecognizer - if autoDetectLanguage { - try speechConfig.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode) - let autoDetect = try SPXAutoDetectSourceLanguageConfiguration(languages) - rec = try SPXSpeechRecognizer( - speechConfiguration: speechConfig, - autoDetectSourceLanguageConfiguration: autoDetect, - audioConfiguration: audioConfig - ) - } else { - speechConfig.speechRecognitionLanguage = languages.first ?? "zh-CN" - rec = try SPXSpeechRecognizer( - speechConfiguration: speechConfig, - audioConfiguration: audioConfig - ) - } - - rec.addRecognizingEventHandler { [weak self] (_: SPXSpeechRecognizer, event: SPXSpeechRecognitionEventArgs) in - guard let self = self else { return } - let text = event.result.text?.trimmingCharacters(in: CharacterSet.whitespaces) ?? "" - guard !text.isEmpty else { return } - let language: String - if self.autoDetectLanguage { - language = SPXAutoDetectSourceLanguageResult(event.result).language ?? "" - } else { - language = self.languages.first ?? "" - } - self.onRecognizing?(AsrResult(text: text, language: language)) - } - - rec.addRecognizedEventHandler { [weak self] (_: SPXSpeechRecognizer, event: SPXSpeechRecognitionEventArgs) in - guard let self = self else { return } - guard event.result.reason == SPXResultReason.recognizedSpeech else { return } - let text = event.result.text?.trimmingCharacters(in: CharacterSet.whitespaces) ?? "" - let language: String - if self.autoDetectLanguage { - language = SPXAutoDetectSourceLanguageResult(event.result).language ?? "" - } else { - language = self.languages.first ?? "" - } - self.onRecognized?(AsrResult(text: text, language: language)) - } - - self.pushStream = stream - self.recognizer = rec - } catch { - } - } - - func startRecognition() { - workQueue.async { [weak self] in - try? self?.recognizer?.startContinuousRecognition() - } - } - - func stopRecognition() { - workQueue.async { [weak self] in - try? self?.recognizer?.stopContinuousRecognition() - } - } - - func pushStreamWrite(_ bytes: Data) { - pushStream?.write(bytes) - } - - func pushStreamClose() { - pushStream?.close() - } - - func dispose() { - workQueue.async { [weak self] in - try? self?.recognizer?.stopContinuousRecognition() - self?.pushStream?.close() - self?.recognizer = nil - self?.pushStream = nil - } - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/recorder/AstStereoRecorder.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/recorder/AstStereoRecorder.swift deleted file mode 100644 index 08975b88..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/recorder/AstStereoRecorder.swift +++ /dev/null @@ -1,171 +0,0 @@ -import Foundation - -/// 通话翻译模式立体声录音器 -/// -/// 由本端麦克风 PCM(来自 BLE 输入的左声道)作为时钟,按 16k/16bit 的节奏 -/// 持续写入录音文件;对方端解码出的 PCM(远端/对方声音)异步排入右声道 FIFO, -/// 与左声道按相同样本数交错合成立体声后写出。 -/// -/// 输出格式:16 kHz / 16-bit PCM / 2 channels WAV。 -/// 从 azure_speech 插件移植,未依赖任何外部模块。 -public final class AstStereoRecorder { - /// 通话翻译录音中麦克风原音偏小、对方 TTS 偏响,对两路分别做线性增益再 tanh 软削波。 - /// 左(麦克风) +12dB ≈ 3.98x,右(TTS) -4dB ≈ 0.63x,与 Android 端 AstStereoRecorder 保持一致。 - private static let leftGain: Float = 3.98 - private static let rightGain: Float = 0.63 - /// 右声道 FIFO 上限(约 5 秒 @16kHz/16bit 单声道);左声道(时钟)长时间缺席时防止无限增长 - private static let maxRightFifoBytes = 16000 * 2 * 5 - - /// 受写入队列保护的状态 - private let stateQueue = DispatchQueue(label: "ast.stereo.recorder.state") - private var recordFile: RecordFile? - private var rightFifo = Data() - private var isRunningFlag = false - private var isPausedFlag = false - - /// 调用点用的轻量级开关,避免每帧都进 stateQueue.sync。 - /// 在 start/pause/resume/stop 内同步更新,读时直接走原子 Bool(接受 start/stop 瞬间的一两帧延迟)。 - private var _active: Bool = false - public var isActive: Bool { _active } - - public init() {} - - /// 开始录音 - /// - Parameter filePath: 输出文件路径(.wav) - @discardableResult - public func start(filePath: String) -> Bool { - return stateQueue.sync { - // 已在录音,先停掉旧的 - if recordFile != nil { - _ = recordFile?.closeFile(isSave: false) - recordFile = nil - } - rightFifo.removeAll(keepingCapacity: false) - isPausedFlag = false - - let file = RecordFile() - file.setAudioConfig(sampleRate: 16000, channels: 2) - file.creatingFiles(atPath: filePath) - recordFile = file - isRunningFlag = true - _active = true - return true - } - } - - @discardableResult - public func pause() -> Bool { - return stateQueue.sync { - isPausedFlag = true - _active = false - return true - } - } - - @discardableResult - public func resume() -> Bool { - return stateQueue.sync { - isPausedFlag = false - _active = isRunningFlag - return true - } - } - - @discardableResult - public func stop(isSave: Bool) -> Bool { - return stateQueue.sync { - guard let file = recordFile else { - _active = false - return false - } - isRunningFlag = false - isPausedFlag = false - _active = false - let ok = file.closeFile(isSave: isSave) - recordFile = nil - rightFifo.removeAll(keepingCapacity: false) - return ok - } - } - - /// 排入右声道(对方端解码出的远端 PCM) - /// - Parameter pcm: 16 kHz / 16-bit PCM mono - public func enqueueRightChannel(_ pcm: Data) { - guard !pcm.isEmpty else { return } - stateQueue.sync { - guard isRunningFlag, !isPausedFlag else { return } - rightFifo.append(pcm) - // 超出上限则丢弃最旧字节,避免 OOM - if rightFifo.count > AstStereoRecorder.maxRightFifoBytes { - rightFifo.removeFirst(rightFifo.count - AstStereoRecorder.maxRightFifoBytes) - } - } - } - - /// 写入一帧左声道(本端麦克风 PCM)并消费等量右声道字节,交错后落盘 - /// - Parameter mono: 16 kHz / 16-bit PCM mono(来自 BLE 输入解出的 leftBuffer) - public func writeLeftChannel(_ mono: Data) { - guard !mono.isEmpty else { return } - // 左声道字节数必须是偶数(16-bit 对齐) - let leftBytes = mono.count - (mono.count % 2) - if leftBytes == 0 { return } - - var stereo = Data() - var fileToWrite: RecordFile? - stateQueue.sync { - guard isRunningFlag, !isPausedFlag, let file = recordFile else { return } - - // 从右声道 FIFO 中取出与左声道等长的字节,不足部分补零 - let popCount = min(rightFifo.count, leftBytes) - var right = Data(capacity: leftBytes) - if popCount > 0 { - right.append(rightFifo.prefix(popCount)) - rightFifo.removeFirst(popCount) - } - if popCount < leftBytes { - right.append(Data(count: leftBytes - popCount)) - } - - stereo = Data(capacity: leftBytes * 2) - let leftPrefix = mono.prefix(leftBytes) - leftPrefix.withUnsafeBytes { (lPtr: UnsafeRawBufferPointer) in - right.withUnsafeBytes { (rPtr: UnsafeRawBufferPointer) in - let lBase = lPtr.bindMemory(to: UInt8.self).baseAddress! - let rBase = rPtr.bindMemory(to: UInt8.self).baseAddress! - let sampleCount = leftBytes / 2 - for i in 0.. (UInt8, UInt8) { - if gain == 1.0 { return (lo, hi) } - let unsigned = UInt16(lo) | (UInt16(hi) << 8) - let s = Int16(bitPattern: unsigned) - let x = (Float(s) / 32767.0) * gain - let y = tanh(x) - var v = y * 32767.0 - if v > 32767.0 { v = 32767.0 } - if v < -32768.0 { v = -32768.0 } - let outU = UInt16(bitPattern: Int16(v)) - return (UInt8(outU & 0xFF), UInt8((outU >> 8) & 0xFF)) - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/recorder/RecordFile.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/recorder/RecordFile.swift deleted file mode 100644 index ed0782ab..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/recorder/RecordFile.swift +++ /dev/null @@ -1,185 +0,0 @@ -import Foundation -import os.log - -/// WAV 录音写入器(16-bit PCM,单/双声道可配)。 -/// -/// creatingFiles() 先写占位 WAV 头;saveAudioDataToWav() 在后台串行队列异步追加 -/// PCM;closeFile() 等写入完成后回填真实 RIFF/data 长度,得到合法 WAV。 -/// 若 isSave==false 或无数据则删除文件。 -/// -/// 单声道:同声/设备辅助翻译直接使用。双声道:通话翻译由 AstStereoRecorder -/// 先把左右声道交织成立体声帧后再调本类写入。 -public class RecordFile { - private let log = OSLog(subsystem: "com.asrtrtts.manager", category: "RecordFile") - private var currentAudioFile: URL? - private var fileHandle: FileHandle? - private let writeQueue = DispatchQueue(label: "audio.write.queue") - private let writeGroup = DispatchGroup() - private var isWriting = false - private var dataBuffer = Data() - private var totalBytesWritten = 0 - - private var sampleRate: UInt32 = 16000 - private var channels: UInt32 = 1 // 1=单声道, 2=立体声 - - public init() {} - - public func setAudioConfig(sampleRate: UInt32, channels: UInt32) { - self.sampleRate = sampleRate - self.channels = channels - } - - // 创建文件并写入占位 WAV 头 - public func creatingFiles(atPath path: String) { - guard fileHandle == nil else { return } - - let fileURL = URL(fileURLWithPath: path) - let directoryURL = fileURL.deletingLastPathComponent() - do { - try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) - } catch { - os_log("创建目录失败: %{public}@", log: log, type: .error, error.localizedDescription) - return - } - - FileManager.default.createFile(atPath: path, contents: nil) - do { - fileHandle = try FileHandle(forWritingTo: fileURL) - currentAudioFile = fileURL - let header = generateWavHeader(dataLength: 0) - try fileHandle?.write(contentsOf: header) - totalBytesWritten = 0 - } catch { - os_log("文件初始化失败: %{public}@", log: log, type: .error, error.localizedDescription) - fileHandle = nil - currentAudioFile = nil - } - } - - // 异步追加 PCM(避免在音频实时回调线程上做文件 I/O) - public func saveAudioDataToWav(_ data: Data) { - guard fileHandle != nil, currentAudioFile != nil else { return } - - writeQueue.async { [weak self] in - guard let self = self else { return } - self.dataBuffer.append(data) - self.totalBytesWritten += data.count - - if !self.isWriting { - self.isWriting = true - self.writeGroup.enter() - defer { - self.isWriting = false - self.writeGroup.leave() - } - - while !self.dataBuffer.isEmpty { - let chunk: Data - if self.dataBuffer.count > 8192 { - chunk = self.dataBuffer.prefix(8192) - self.dataBuffer.removeFirst(8192) - } else { - chunk = self.dataBuffer - self.dataBuffer.removeAll() - } - do { - try self.fileHandle?.seekToEnd() - self.fileHandle?.write(chunk) - } catch { - os_log("写入数据失败: %{public}@", log: log, type: .error, error.localizedDescription) - } - } - } - } - } - - // 关闭并回填 WAV 头;isSave=false 或无数据则删除文件。 - // 整个过程在串行写队列上执行:排在所有已入队的异步写入之后、且与写入互斥, - // 彻底避免"关闭与写入并发"(写已关闭句柄)。 - public func closeFile(isSave: Bool) -> Bool { - // 全过程在串行写队列上执行:与所有异步写入互斥并排在其后;关闭后再来的写入块 - // 会因 fileHandle 已在队列内被置 nil 而自动跳过,无需额外的跨线程标志 - return writeQueue.sync { - defer { - fileHandle = nil - currentAudioFile = nil - dataBuffer.removeAll() - totalBytesWritten = 0 - } - - guard let fileURL = currentAudioFile, let handle = fileHandle else { - return false - } - - do { - try handle.close() - - if !isSave || totalBytesWritten == 0 { - try FileManager.default.removeItem(at: fileURL) - return false - } - - let header = generateWavHeader(dataLength: totalBytesWritten) - let updateHandle = try FileHandle(forUpdating: fileURL) - try updateHandle.seek(toOffset: 0) - updateHandle.write(header) - try updateHandle.close() - return true - } catch { - os_log("关闭文件失败: %{public}@", log: log, type: .error, error.localizedDescription) - return false - } - } - } - - // 标准 16-bit PCM WAV 头(44 字节,little-endian) - private func generateWavHeader(dataLength: Int) -> Data { - let totalLength = 36 + dataLength - let bytesPerSample: UInt32 = 2 - let byteRate = sampleRate * channels * bytesPerSample - let blockAlign = channels * bytesPerSample - - var header = Data() - header.append("RIFF".data(using: .ascii)!) - header.append(Data(bytes: [ - UInt8(totalLength & 0xFF), - UInt8((totalLength >> 8) & 0xFF), - UInt8((totalLength >> 16) & 0xFF), - UInt8((totalLength >> 24) & 0xFF) - ])) - header.append("WAVE".data(using: .ascii)!) - header.append("fmt ".data(using: .ascii)!) - header.append(Data(bytes: [16, 0, 0, 0])) // PCM 头长度 - header.append(Data(bytes: [1, 0])) // PCM 格式 - header.append(Data(bytes: [ - UInt8(channels & 0xFF), - UInt8((channels >> 8) & 0xFF) - ])) // 声道数 - header.append(Data(bytes: [ - UInt8(sampleRate & 0xFF), - UInt8((sampleRate >> 8) & 0xFF), - UInt8((sampleRate >> 16) & 0xFF), - UInt8((sampleRate >> 24) & 0xFF) - ])) // 采样率 - header.append(Data(bytes: [ - UInt8(byteRate & 0xFF), - UInt8((byteRate >> 8) & 0xFF), - UInt8((byteRate >> 16) & 0xFF), - UInt8((byteRate >> 24) & 0xFF) - ])) // 字节率 - header.append(Data(bytes: [ - UInt8(blockAlign & 0xFF), - UInt8((blockAlign >> 8) & 0xFF) - ])) // 块对齐 - header.append(Data(bytes: [16, 0])) // 样本位数 - header.append("data".data(using: .ascii)!) - header.append(Data(bytes: [ - UInt8(dataLength & 0xFF), - UInt8((dataLength >> 8) & 0xFF), - UInt8((dataLength >> 16) & 0xFF), - UInt8((dataLength >> 24) & 0xFF) - ])) - - return header - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/AzureTranslation.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/AzureTranslation.swift deleted file mode 100644 index c40107bb..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/AzureTranslation.swift +++ /dev/null @@ -1,184 +0,0 @@ -import Foundation -import CommonCrypto - -class AzureTranslation: TranslationProtocol { - private let baseURL = "https://translate.volcengineapi.com" - private let endpoint = "/" - private let service = "translate" - private let version = "2020-06-01" - private let action = "TranslateText" - private let algorithm = "HMAC-SHA256" - - private let accessKey = "AKLTZjJmODdkODQwN2RjNDcyMWI4NWM2ZWRmNDkxY2RiYzI" - private let secretKey = "T0Roa09EZGlNalZpTjJObE5HVmlZMkV3WVRkbVpUTTBORGMzTW1FNFlqYw==" - private let region = "cn-north-1" - private let maxRetry = 3 - - func translateText(text: String, sourceLanguage: String, targetLanguage: String) -> TranslationResult { - var lastError = "未知错误" - for attempt in 0.. TranslationResult { - let body: [String: Any] = [ - "SourceLanguage": sourceLanguage, - "TargetLanguage": targetLanguage, - "TextList": [text], - ] - guard let bodyData = try? JSONSerialization.data(withJSONObject: body), - let bodyString = String(data: bodyData, encoding: .utf8) else { - return TranslationResult(success: false, error: "请求序列化失败") - } - - let queryParams: [(String, String)] = [ - ("Action", action), - ("Region", region), - ("Service", service), - ("Version", version), - ] - - let headers: [String: String] - do { - headers = try generateSignature(method: "POST", bodyString: bodyString, queryParams: queryParams) - } catch { - return TranslationResult(success: false, error: "签名生成失败: \(error)") - } - - var components = URLComponents(string: baseURL)! - components.queryItems = queryParams.map { URLQueryItem(name: $0.0, value: $0.1) } - guard let url = components.url else { - return TranslationResult(success: false, error: "URL 构建失败") - } - - var request = URLRequest(url: url, timeoutInterval: 10) - request.httpMethod = "POST" - request.httpBody = bodyData - headers.forEach { request.setValue($1, forHTTPHeaderField: $0) } - - let semaphore = DispatchSemaphore(value: 0) - var responseData: Data? - var responseError: Error? - var statusCode = 0 - - URLSession.shared.dataTask(with: request) { data, response, error in - responseData = data - responseError = error - statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0 - semaphore.signal() - }.resume() - semaphore.wait() - - if let error = responseError { - return TranslationResult(success: false, error: "请求异常: \(error.localizedDescription)") - } - guard (200..<300).contains(statusCode), let data = responseData else { - return TranslationResult(success: false, error: "HTTP 错误: \(statusCode)") - } - - guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - return TranslationResult(success: false, error: "响应解析失败") - } - - if let translation = extractTranslation(json) { - return TranslationResult(success: true, translatedText: translation, confidence: 0.9) - } - let err = extractError(json) ?? "翻译结果为空" - return TranslationResult(success: false, error: err) - } - - private func extractTranslation(_ json: [String: Any]) -> String? { - if let list = json["TranslationList"] as? [[String: Any]], - let first = list.first, - let t = first["Translation"] as? String { return t } - if let t = json["Translation"] as? String { return t } - if let result = json["Result"] as? [String: Any], - let t = result["Translation"] as? String { return t } - if let data = json["Data"] as? [String: Any] { - if let t = data["Translation"] as? String { return t } - if let list = data["TranslationList"] as? [[String: Any]], - let first = list.first, - let t = first["Translation"] as? String { return t } - } - return nil - } - - private func extractError(_ json: [String: Any]) -> String? { - if let meta = json["ResponseMetadata"] as? [String: Any], - let err = meta["Error"] as? [String: Any] { - return err["Message"] as? String ?? err["Code"] as? String - } - if let err = json["Error"] as? [String: Any] { - return err["Message"] as? String ?? err["Code"] as? String - } - return nil - } - - private func generateSignature(method: String, bodyString: String, - queryParams: [(String, String)]) throws -> [String: String] { - let now = Date() - let dateFmt = DateFormatter(); dateFmt.dateFormat = "yyyyMMdd"; dateFmt.timeZone = TimeZone(identifier: "UTC") - let tsFmt = DateFormatter(); tsFmt.dateFormat = "yyyyMMdd'T'HHmmss'Z'"; tsFmt.timeZone = TimeZone(identifier: "UTC") - let date = dateFmt.string(from: now) - let timestamp = tsFmt.string(from: now) - - let sortedParams = queryParams.sorted { $0.0 < $1.0 } - let canonicalQuery = sortedParams.map { "\(urlEncode($0.0))=\(urlEncode($0.1))" }.joined(separator: "&") - - let host = "translate.volcengineapi.com" - let payloadHash = sha256Hex(bodyString) - let canonicalHeaders = "host:\(host)\nx-date:\(timestamp)\n" - let signedHeaders = "host;x-date" - let canonicalRequest = "\(method)\n\(endpoint)\n\(canonicalQuery)\n\(canonicalHeaders)\n\(signedHeaders)\n\(payloadHash)" - - let credentialScope = "\(date)/\(region)/\(service)/request" - let stringToSign = "\(algorithm)\n\(timestamp)\n\(credentialScope)\n\(sha256Hex(canonicalRequest))" - - let kSecret = secretKey.data(using: .utf8)! - let kDate = hmacSha256(key: kSecret, data: date) - let kRegion = hmacSha256(key: kDate, data: region) - let kService = hmacSha256(key: kRegion, data: service) - let kSigning = hmacSha256(key: kService, data: "request") - let signature = hmacSha256(key: kSigning, data: stringToSign).map { String(format: "%02x", $0) }.joined() - - let authorization = "\(algorithm) Credential=\(accessKey)/\(credentialScope), SignedHeaders=\(signedHeaders), Signature=\(signature)" - return [ - "Content-Type": "application/json", - "X-Date": timestamp, - "Authorization": authorization, - "Host": host, - ] - } - - private func sha256Hex(_ input: String) -> String { - let data = Data(input.utf8) - var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) - data.withUnsafeBytes { CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) } - return hash.map { String(format: "%02x", $0) }.joined() - } - - private func hmacSha256(key: Data, data: String) -> Data { - let dataBytes = Data(data.utf8) - var mac = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) - key.withUnsafeBytes { keyPtr in - dataBytes.withUnsafeBytes { dataPtr in - CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), - keyPtr.baseAddress, key.count, - dataPtr.baseAddress, dataBytes.count, - &mac) - } - } - return Data(mac) - } - - private func urlEncode(_ s: String) -> String { - s.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? s - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/TranslationManager.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/TranslationManager.swift deleted file mode 100644 index 72a84a45..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/TranslationManager.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation - -class TranslationManager: TranslationProtocol { - private var currentProvider: TranslationProvider - private var delegate: TranslationProtocol? - - init(provider: TranslationProvider = .azure) { - self.currentProvider = provider - createDelegate() - } - - func switchProvider(provider: TranslationProvider? = nil) { - if let p = provider { currentProvider = p } - createDelegate() - } - - private func createDelegate() { - switch currentProvider { - case .azure: - delegate = AzureTranslation() - case .volcengine: - delegate = VolcengineTranslation() - } - } - - func getProvider() -> TranslationProvider { currentProvider } - - func translateText(text: String, sourceLanguage: String, targetLanguage: String) -> TranslationResult { - return delegate?.translateText(text: text, sourceLanguage: sourceLanguage, targetLanguage: targetLanguage) - ?? TranslationResult(success: false, error: "TranslationManager not initialized") - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/TranslationProtocol.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/TranslationProtocol.swift deleted file mode 100644 index 128a16ae..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/TranslationProtocol.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -enum TranslationProvider { - case azure - case volcengine -} - -struct TranslationResult { - let success: Bool - let translatedText: String? - let error: String? - let confidence: Float - - init(success: Bool, translatedText: String? = nil, error: String? = nil, confidence: Float = 0) { - self.success = success - self.translatedText = translatedText - self.error = error - self.confidence = confidence - } -} - -protocol TranslationProtocol { - func translateText(text: String, sourceLanguage: String, targetLanguage: String) -> TranslationResult -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/VolcengineTranslation.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/VolcengineTranslation.swift deleted file mode 100644 index 7bf6f993..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation/VolcengineTranslation.swift +++ /dev/null @@ -1,184 +0,0 @@ -import Foundation -import CommonCrypto - -class VolcengineTranslation: TranslationProtocol { - private let baseURL = "https://translate.volcengineapi.com" - private let endpoint = "/" - private let service = "translate" - private let version = "2020-06-01" - private let action = "TranslateText" - private let algorithm = "HMAC-SHA256" - - private let accessKey = "AKLTZjJmODdkODQwN2RjNDcyMWI4NWM2ZWRmNDkxY2RiYzI" - private let secretKey = "T0Roa09EZGlNalZpTjJObE5HVmlZMkV3WVRkbVpUTTBORGMzTW1FNFlqYw==" - private let region = "cn-north-1" - private let maxRetry = 3 - - func translateText(text: String, sourceLanguage: String, targetLanguage: String) -> TranslationResult { - var lastError = "未知错误" - for attempt in 0.. TranslationResult { - let body: [String: Any] = [ - "SourceLanguage": sourceLanguage, - "TargetLanguage": targetLanguage, - "TextList": [text], - ] - guard let bodyData = try? JSONSerialization.data(withJSONObject: body), - let bodyString = String(data: bodyData, encoding: .utf8) else { - return TranslationResult(success: false, error: "请求序列化失败") - } - - let queryParams: [(String, String)] = [ - ("Action", action), - ("Region", region), - ("Service", service), - ("Version", version), - ] - - let headers: [String: String] - do { - headers = try generateSignature(method: "POST", bodyString: bodyString, queryParams: queryParams) - } catch { - return TranslationResult(success: false, error: "签名生成失败: \(error)") - } - - var components = URLComponents(string: baseURL)! - components.queryItems = queryParams.map { URLQueryItem(name: $0.0, value: $0.1) } - guard let url = components.url else { - return TranslationResult(success: false, error: "URL 构建失败") - } - - var request = URLRequest(url: url, timeoutInterval: 10) - request.httpMethod = "POST" - request.httpBody = bodyData - headers.forEach { request.setValue($1, forHTTPHeaderField: $0) } - - let semaphore = DispatchSemaphore(value: 0) - var responseData: Data? - var responseError: Error? - var statusCode = 0 - - URLSession.shared.dataTask(with: request) { data, response, error in - responseData = data - responseError = error - statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0 - semaphore.signal() - }.resume() - semaphore.wait() - - if let error = responseError { - return TranslationResult(success: false, error: "请求异常: \(error.localizedDescription)") - } - guard (200..<300).contains(statusCode), let data = responseData else { - return TranslationResult(success: false, error: "HTTP 错误: \(statusCode)") - } - - guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - return TranslationResult(success: false, error: "响应解析失败") - } - - if let translation = extractTranslation(json) { - return TranslationResult(success: true, translatedText: translation, confidence: 0.9) - } - let err = extractError(json) ?? "翻译结果为空" - return TranslationResult(success: false, error: err) - } - - private func extractTranslation(_ json: [String: Any]) -> String? { - if let list = json["TranslationList"] as? [[String: Any]], - let first = list.first, - let t = first["Translation"] as? String { return t } - if let t = json["Translation"] as? String { return t } - if let result = json["Result"] as? [String: Any], - let t = result["Translation"] as? String { return t } - if let data = json["Data"] as? [String: Any] { - if let t = data["Translation"] as? String { return t } - if let list = data["TranslationList"] as? [[String: Any]], - let first = list.first, - let t = first["Translation"] as? String { return t } - } - return nil - } - - private func extractError(_ json: [String: Any]) -> String? { - if let meta = json["ResponseMetadata"] as? [String: Any], - let err = meta["Error"] as? [String: Any] { - return err["Message"] as? String ?? err["Code"] as? String - } - if let err = json["Error"] as? [String: Any] { - return err["Message"] as? String ?? err["Code"] as? String - } - return nil - } - - private func generateSignature(method: String, bodyString: String, - queryParams: [(String, String)]) throws -> [String: String] { - let now = Date() - let dateFmt = DateFormatter(); dateFmt.dateFormat = "yyyyMMdd"; dateFmt.timeZone = TimeZone(identifier: "UTC") - let tsFmt = DateFormatter(); tsFmt.dateFormat = "yyyyMMdd'T'HHmmss'Z'"; tsFmt.timeZone = TimeZone(identifier: "UTC") - let date = dateFmt.string(from: now) - let timestamp = tsFmt.string(from: now) - - let sortedParams = queryParams.sorted { $0.0 < $1.0 } - let canonicalQuery = sortedParams.map { "\(urlEncode($0.0))=\(urlEncode($0.1))" }.joined(separator: "&") - - let host = "translate.volcengineapi.com" - let payloadHash = sha256Hex(bodyString) - let canonicalHeaders = "host:\(host)\nx-date:\(timestamp)\n" - let signedHeaders = "host;x-date" - let canonicalRequest = "\(method)\n\(endpoint)\n\(canonicalQuery)\n\(canonicalHeaders)\n\(signedHeaders)\n\(payloadHash)" - - let credentialScope = "\(date)/\(region)/\(service)/request" - let stringToSign = "\(algorithm)\n\(timestamp)\n\(credentialScope)\n\(sha256Hex(canonicalRequest))" - - let kSecret = secretKey.data(using: .utf8)! - let kDate = hmacSha256(key: kSecret, data: date) - let kRegion = hmacSha256(key: kDate, data: region) - let kService = hmacSha256(key: kRegion, data: service) - let kSigning = hmacSha256(key: kService, data: "request") - let signature = hmacSha256(key: kSigning, data: stringToSign).map { String(format: "%02x", $0) }.joined() - - let authorization = "\(algorithm) Credential=\(accessKey)/\(credentialScope), SignedHeaders=\(signedHeaders), Signature=\(signature)" - return [ - "Content-Type": "application/json", - "X-Date": timestamp, - "Authorization": authorization, - "Host": host, - ] - } - - private func sha256Hex(_ input: String) -> String { - let data = Data(input.utf8) - var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) - data.withUnsafeBytes { CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) } - return hash.map { String(format: "%02x", $0) }.joined() - } - - private func hmacSha256(key: Data, data: String) -> Data { - let dataBytes = Data(data.utf8) - var mac = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) - key.withUnsafeBytes { keyPtr in - dataBytes.withUnsafeBytes { dataPtr in - CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), - keyPtr.baseAddress, key.count, - dataPtr.baseAddress, dataBytes.count, - &mac) - } - } - return Data(mac) - } - - private func urlEncode(_ s: String) -> String { - s.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? s - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/BoundedQueue.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/BoundedQueue.swift deleted file mode 100644 index c2cd11d9..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/BoundedQueue.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation - -class BoundedQueue { - private var buffer: [T?] - private let capacity: Int - private let lock = NSLock() - private var head = 0 - private var tail = 0 - private var count = 0 - - init(capacity: Int) { - self.capacity = capacity - self.buffer = [T?](repeating: nil, count: capacity) - } - - func offer(_ item: T) { - lock.lock(); defer { lock.unlock() } - if count >= capacity { - // 丢弃最旧的元素 - head = (head + 1) % capacity - count -= 1 - } - buffer[tail] = item - tail = (tail + 1) % capacity - count += 1 - } - - func poll() -> T? { - lock.lock(); defer { lock.unlock() } - guard count > 0 else { return nil } - let item = buffer[head] - buffer[head] = nil - head = (head + 1) % capacity - count -= 1 - return item - } - - func clear() { - lock.lock(); defer { lock.unlock() } - buffer = [T?](repeating: nil, count: capacity) - head = 0 - tail = 0 - count = 0 - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/CallTranslationHelper.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/CallTranslationHelper.swift deleted file mode 100644 index c31f5026..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/CallTranslationHelper.swift +++ /dev/null @@ -1,258 +0,0 @@ -import Foundation -import AVFoundation -import bluetooth_manager - -class CallTranslationHelper: TranslationHelperProtocol { - private let asrTrTtsHelperSpk = VolcengineHelper() - private let asrTrTtsHelperMic = VolcengineHelper() - - var callback: ((Dictionary) -> Void)? - - // 音频发送队列(容量上限 4096,超限丢弃最旧) - private let spkQueue = BoundedQueue(capacity: 4096) - private let micQueue = BoundedQueue(capacity: 4096) - - private var previousCategory: AVAudioSession.Category? - private var previousMode: AVAudioSession.Mode? - private var previousOptions: AVAudioSession.CategoryOptions? - private var interruptionObserver: NSObjectProtocol? - - private var isSending = false - private var sendInterval: TimeInterval = 0.060 // 60ms - private var sendTimer: DispatchSourceTimer? - private let timerQueue = DispatchQueue(label: "call.sendtimer", qos: .userInteractive) - private var isRecognitionStopped = true - - /// 通话翻译立体声录音器(左=本端麦克风,右=对方/远端)。 - /// 两路均为 BluetoothManager 已 G722 解码的 16k/16bit 单声道 PCM。 - private var stereoRecorder: AstStereoRecorder? - - /// 静音填充数据:与 Android 一致的全零 PCM G722 编码帧 - private let silenceData = Data([ - 0x0A, 0x8E, 0x30, 0xBD, 0x3F, 0x83, 0xFF, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF - ]) - - init() { - // 注册蓝牙电话翻译回调 - BluetoothManager.translationCallback = { [weak self] data in - guard let self = self else { return } - switch data["type"] as? String { - case "startCall": self.startRecognition() - case "stopCall": self.stopRecognition() - case "spkData": if let d = data["data"] as? Data { self.spkData(d) } - case "micData": if let d = data["data"] as? Data { self.micData(d) } - case "speedAdjustment": if let d = data["data"] as? Data { self.handleSpeedAdjustment(d) } - default: break - } - } - - asrTrTtsHelperSpk.callback = { [weak self] data in - self?.publishData(data.merging(["side": "right"]) { $1 }) - } - asrTrTtsHelperMic.callback = { [weak self] data in - self?.publishData(data.merging(["side": "left"]) { $1 }) - } - asrTrTtsHelperSpk.onSynthesisEncoded = { [weak self] bytes in - self?.spkQueue.offer(bytes) - } - asrTrTtsHelperMic.onSynthesisEncoded = { [weak self] bytes in - self?.micQueue.offer(bytes) - } - } - - // MARK: - 公共接口 - - func start(sourceLanguage: String, targetLanguage: String) { - // 设置音频会话,确保后台保活 - let session = AVAudioSession.sharedInstance() - previousCategory = session.category - previousMode = session.mode - previousOptions = session.categoryOptions - do { - try session.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth]) - try session.setActive(true) - } catch {} - - // 注册音频中断处理 - interruptionObserver = NotificationCenter.default.addObserver( - forName: AVAudioSession.interruptionNotification, - object: nil, queue: nil - ) { notification in - guard let info = notification.userInfo, - let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt, - let type = AVAudioSession.InterruptionType(rawValue: typeValue), - type == .ended else { return } - try? AVAudioSession.sharedInstance().setActive(true) - } - - asrTrTtsHelperSpk.switchProvider(languages: [targetLanguage, sourceLanguage]) - asrTrTtsHelperMic.switchProvider(languages: [sourceLanguage, targetLanguage]) - BluetoothManager.writeBytes(Data([0xaa, 0x51])) - } - - func stop() { - BluetoothManager.writeBytes(Data([0xaa, 0x52])) - - // 移除中断监听 - if let observer = interruptionObserver { - NotificationCenter.default.removeObserver(observer) - interruptionObserver = nil - } - - // 恢复音频会话 - if let cat = previousCategory, let mode = previousMode, let opts = previousOptions { - try? AVAudioSession.sharedInstance().setCategory(cat, mode: mode, options: opts) - } - } - - func dispose() { - stop() - stopRecognition() - asrTrTtsHelperSpk.dispose() - asrTrTtsHelperMic.dispose() - // 兜底关闭录音文件(dispose 时保存已写入内容) - _ = stereoRecorder?.stop(isSave: true) - stereoRecorder = nil - callback = nil - BluetoothManager.translationCallback = nil - stopSendTimer() - } - - // MARK: - 录音(立体声:左=本端麦克风,右=对方/远端) - - func enableRecord(filePath: String) { - // 防御:未 stopRecord 就重复 enableRecord 时先停旧的 - _ = stereoRecorder?.stop(isSave: false) - let recorder = AstStereoRecorder() - recorder.start(filePath: filePath) - stereoRecorder = recorder - } - - func stopRecord(isSave: Bool) { - _ = stereoRecorder?.stop(isSave: isSave) - stereoRecorder = nil - } - - // MARK: - 私有:识别控制 - - private func startRecognition() { - guard isRecognitionStopped else { return } - isRecognitionStopped = false - BluetoothManager.writeBytes(Data([0xaa, 0x57])) - asrTrTtsHelperSpk.startRecognition() - asrTrTtsHelperMic.startRecognition() - spkQueue.clear() - micQueue.clear() - isSending = true - startSendTimer() - publishData(["type": "startCall"]) - } - - private func stopRecognition() { - guard !isRecognitionStopped else { return } - isRecognitionStopped = true - BluetoothManager.writeBytes(Data([0xaa, 0x58])) - asrTrTtsHelperSpk.stopRecognition() - asrTrTtsHelperMic.stopRecognition() - isSending = false - stopSendTimer() - publishData(["type": "stopCall"]) - } - - private func handleSpeedAdjustment(_ data: Data) { - guard data.count > 6 else { return } - let newInterval: TimeInterval - switch data[6] { - case 0x01: newInterval = 0.055 - case 0x02: newInterval = 0.060 - case 0x03: newInterval = 0.065 - default: newInterval = 0.060 - } - if sendInterval != newInterval { - sendInterval = newInterval - if isSending { - stopSendTimer() - startSendTimer() - } - } - } - - // MARK: - 私有:音频数据路由 - - private func spkData(_ data: Data) { - asrTrTtsHelperSpk.pushStreamWrite(data) - // 录音点(右声道):data 为 BluetoothManager 已 G722 解码的对方/远端 16k/16bit 单声道 PCM - stereoRecorder?.enqueueRightChannel(data) - } - private func micData(_ data: Data) { - asrTrTtsHelperMic.pushStreamWrite(data) - // 录音点(左声道时钟):data 为 BluetoothManager 已 G722 解码的本端麦克风 16k/16bit 单声道 PCM - stereoRecorder?.writeLeftChannel(data) - } - - // MARK: - 私有:定时发送 - - private func startSendTimer() { - let timer = DispatchSource.makeTimerSource(queue: timerQueue) - let interval = sendInterval - timer.schedule(deadline: .now(), repeating: interval, leeway: .milliseconds(2)) - timer.setEventHandler { [weak self] in self?.sendAudioData() } - timer.resume() - sendTimer = timer - } - - private func stopSendTimer() { - sendTimer?.cancel() - sendTimer = nil - } - - private func sendAudioData() { - guard isSending else { return } - let count = 3 - let payloadSize = 80 * count // 每包 SPK(40) + MIC(40) - let totalSize = 4 + payloadSize - var packet = Data(repeating: 0, count: totalSize) - - // Header - packet[0] = count > 4 ? 0xDD : 0xAA - packet[1] = 0x56 - packet[2] = UInt8(clamping: totalSize) - packet[3] = 0x03 - - var hasData = false - for i in 0..) { - let cb = callback - DispatchQueue.main.async { cb?(data) } - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/FaceToFaceTranslationHelper.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/FaceToFaceTranslationHelper.swift deleted file mode 100644 index ddafdc4b..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/FaceToFaceTranslationHelper.swift +++ /dev/null @@ -1,248 +0,0 @@ -import Foundation -import AVFoundation -import bluetooth_manager - -class FaceToFaceTranslationHelper: TranslationHelperProtocol { - private var asrTrTtsHelperSpk: AsrTrTtsHelper? - private var asrTrTtsHelperMic: AsrTrTtsHelper? - private var microphoneRecorder: MicrophoneRecorder? - - /// 面对面翻译录音器(单声道)。录的是本端手机麦克风的 16k/16bit 单声道 PCM。 - private var stereoRecorder: AstStereoRecorder? - - var callback: ((Dictionary) -> Void)? - - private var isSending = false - private let micQueue = BoundedQueue(capacity: 4096) - - private var sendTimer: DispatchSourceTimer? - private let timerQueue = DispatchQueue(label: "f2f.sendtimer", qos: .userInteractive) - private var sendInterval: TimeInterval = 0.060 - private var isRecognitionStopped = true - private var previousCategory: AVAudioSession.Category? - private var previousMode: AVAudioSession.Mode? - private var previousOptions: AVAudioSession.CategoryOptions? - private var interruptionObserver: NSObjectProtocol? - - private let silenceData = Data([ - 0x0A, 0x8E, 0x30, 0xBD, 0x3F, 0x83, 0xFF, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF - ]) - - init() { - asrTrTtsHelperSpk = AsrTrTtsHelper() - asrTrTtsHelperMic = AsrTrTtsHelper(autoPlay: true, forceBuiltInSpeaker: true) - - // 注册蓝牙面对面翻译回调 - BluetoothManager.translationCallback = { [weak self] data in - guard let self = self else { return } - switch data["type"] as? String { - case "startFaceToFace": self.startRecognition() - case "stopFaceToFace": self.stopRecognition() - case "spkData": if let d = data["data"] as? Data { self.micData(d) } - case "speedAdjustment": if let d = data["data"] as? Data { self.handleSpeedAdjustment(d) } - default: break - } - } - - microphoneRecorder = MicrophoneRecorder { [weak self] data in - self?.spkData(data) - } - - asrTrTtsHelperSpk?.callback = { [weak self] data in - var newData = data - newData["side"] = "right" - self?.publishData(newData) - } - asrTrTtsHelperMic?.callback = { [weak self] data in - var newData = data - newData["side"] = "left" - self?.publishData(newData) - } - asrTrTtsHelperSpk?.onSynthesisEncoded = { [weak self] bytes in - self?.micQueue.offer(bytes) - } - } - - func start(sourceLanguage: String, targetLanguage: String) { - // 设置通信模式,让平台级AEC生效 - let session = AVAudioSession.sharedInstance() - previousCategory = session.category - previousMode = session.mode - previousOptions = session.categoryOptions - - do { - try session.setCategory(.playAndRecord, mode: .voiceChat, options: [.defaultToSpeaker]) - try session.setActive(true) - } catch {} - - // 注册音频中断处理,中断结束后重新激活音频会话 - interruptionObserver = NotificationCenter.default.addObserver( - forName: AVAudioSession.interruptionNotification, - object: nil, queue: nil - ) { notification in - guard let info = notification.userInfo, - let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt, - let type = AVAudioSession.InterruptionType(rawValue: typeValue), - type == .ended else { return } - try? AVAudioSession.sharedInstance().setActive(true) - } - - asrTrTtsHelperSpk?.switchProvider(languages: [targetLanguage, sourceLanguage]) - asrTrTtsHelperMic?.switchProvider(languages: [sourceLanguage, targetLanguage]) - BluetoothManager.writeBytes(Data([0xaa, 0x66])) - // 在麦克风 VPIO 启用前预热手机扬声器 TTS 播放引擎(让其尽早跑起来、整段复用), - // 消除冷启动首句偏小、停止重启后才变大的问题。 - asrTrTtsHelperMic?.prepareForPlayback() - microphoneRecorder?.start() - } - - func stop() { - BluetoothManager.writeBytes(Data([0xaa, 0x67])) - microphoneRecorder?.stop() - - // 移除中断监听 - if let observer = interruptionObserver { - NotificationCenter.default.removeObserver(observer) - interruptionObserver = nil - } - - // 恢复音频设置 - if let cat = previousCategory, let mode = previousMode, let opts = previousOptions { - try? AVAudioSession.sharedInstance().setCategory(cat, mode: mode, options: opts) - } - } - - private func startRecognition() { - guard isRecognitionStopped else { return } - isRecognitionStopped = false - asrTrTtsHelperSpk?.startRecognition() - asrTrTtsHelperMic?.startRecognition() - micQueue.clear() - isSending = true - - let timer = DispatchSource.makeTimerSource(queue: timerQueue) - timer.schedule(deadline: .now(), repeating: sendInterval, leeway: .milliseconds(2)) - timer.setEventHandler { [weak self] in self?.sendAudioData() } - timer.resume() - sendTimer = timer - - publishData(["type": "startCall"]) - } - - private func stopRecognition() { - guard !isRecognitionStopped else { return } - isRecognitionStopped = true - asrTrTtsHelperSpk?.stopRecognition() - asrTrTtsHelperMic?.stopRecognition() - isSending = false - sendTimer?.cancel() - sendTimer = nil - publishData(["type": "stopCall"]) - } - - private func sendAudioData() { - guard isSending else { return } - let count = 3 - let payloadSize = 40 * count // 每包 MIC(40) - let totalSize = 4 + payloadSize - var packet = Data(repeating: 0, count: totalSize) - - // Header - packet[0] = 0xAA - packet[1] = 0x69 - packet[2] = UInt8(clamping: totalSize) - packet[3] = 0x01 - - var hasData = false - for i in 0.. 6 else { return } - let newInterval: TimeInterval - switch data[6] { - case 0x01: newInterval = 0.055 - case 0x02: newInterval = 0.060 - case 0x03: newInterval = 0.065 - default: newInterval = 0.060 - } - - guard sendInterval != newInterval else { return } - sendInterval = newInterval - if isSending { - sendTimer?.cancel() - let timer = DispatchSource.makeTimerSource(queue: timerQueue) - timer.schedule(deadline: .now(), repeating: sendInterval, leeway: .milliseconds(2)) - timer.setEventHandler { [weak self] in self?.sendAudioData() } - timer.resume() - sendTimer = timer - } - } - - private func spkData(_ data: Data) { - asrTrTtsHelperSpk?.pushStreamWrite(data) - // 录音点(左声道时钟):data 来自 MicrophoneRecorder(PCMReader), - // 是本端手机麦克风的 16k/16bit 单声道 PCM - stereoRecorder?.writeLeftChannel(data) - } - - private func micData(_ data: Data) { - asrTrTtsHelperMic?.pushStreamWrite(data) - // 录音点(右声道):data 为 BluetoothManager 已解码的对方(BLE 设备) - // 16k/16bit 单声道 PCM - stereoRecorder?.enqueueRightChannel(data) - } - - private func publishData(_ data: Dictionary) { - let cb = callback - DispatchQueue.main.async { cb?(data) } - } - - // MARK: - 录音 - - func enableRecord(filePath: String) { - // 防御:未 stopRecord 就重复 enableRecord 时先停旧的 - _ = stereoRecorder?.stop(isSave: false) - // 立体声录音:左=本端手机麦克风,右=对方(BLE 设备) - let recorder = AstStereoRecorder() - recorder.start(filePath: filePath) - stereoRecorder = recorder - } - - func stopRecord(isSave: Bool) { - _ = stereoRecorder?.stop(isSave: isSave) - stereoRecorder = nil - } - - func dispose() { - stop() - stopRecognition() - microphoneRecorder?.dispose() - microphoneRecorder = nil - asrTrTtsHelperSpk?.dispose() - asrTrTtsHelperMic?.dispose() - // 兜底关闭录音文件(dispose 时保存已写入内容) - _ = stereoRecorder?.stop(isSave: true) - stereoRecorder = nil - callback = nil - BluetoothManager.translationCallback = nil - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/MicrophoneRecorder.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/MicrophoneRecorder.swift deleted file mode 100644 index 03252673..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/MicrophoneRecorder.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Foundation -import audio_recorder - -class MicrophoneRecorder { - private var pcmReader: PCMReader? - private var isRecording = false - private let sampleRate: Int - private let onAudioData: (Data) -> Void - - /// 是否启用回声消除,每次 start() 时生效 - var echoCancel: Bool = true - - init(sampleRate: Int = 16000, onAudioData: @escaping (Data) -> Void) { - self.sampleRate = sampleRate - self.onAudioData = onAudioData - } - - func start() { - guard !isRecording else { return } - - let reader = PCMReader( - sampleRate: sampleRate, - numChannels: 1, - echoCancel: echoCancel, - autoGain: echoCancel, - bufferSize: 1024, - onData: onAudioData - ) - - do { - try reader.start() - pcmReader = reader - isRecording = true - } catch { - print("[MicrophoneRecorder] start failed: \(error)") - } - } - - func stop() { - guard isRecording else { return } - isRecording = false - pcmReader?.stop() - pcmReader?.release() - pcmReader = nil - } - - func dispose() { - stop() - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/SimultaneousTranslationHelper.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/SimultaneousTranslationHelper.swift deleted file mode 100644 index 588528c9..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/SimultaneousTranslationHelper.swift +++ /dev/null @@ -1,205 +0,0 @@ -import Foundation -import AVFoundation -import bluetooth_manager - -class SimultaneousTranslationHelper: NSObject, TranslationHelperProtocol { - private var asrTrTtsHelper: AsrTrTtsHelper? - private var microphoneRecorder: MicrophoneRecorder? - - /// 同声传译录音器(单声道)。录的就是喂给 ASR 的解码后 16k/16bit 单声道 PCM。 - private var recordFile: RecordFile? - - var callback: ((Dictionary) -> Void)? - - private var isRecognitionStopped = true - private var sourceLanguageCode: String = "" - private var targetLanguageCode: String = "" - private var previousCategory: AVAudioSession.Category? - private var previousMode: AVAudioSession.Mode? - private var previousOptions: AVAudioSession.CategoryOptions? - private var interruptionObserver: NSObjectProtocol? - /// 本次会话是否走蓝牙 BLE 指令路径(连上耳机时启用,从耳机主麦经 mSBC 拉 PCM)。 - /// 由 start/stop(主线程)写入、translationCallback(BluetoothManager 处理队列)读取, - /// 用 NSLock 串行化避免数据竞争。 - private let stateLock = NSLock() - private var _usingBtPath = false - private var usingBtPath: Bool { - get { - stateLock.lock() - defer { stateLock.unlock() } - return _usingBtPath - } - set { - stateLock.lock() - defer { stateLock.unlock() } - _usingBtPath = newValue - } - } - - override init() { - super.init() - asrTrTtsHelper = AsrTrTtsHelper(autoPlay: true, autoDetectLanguage: true) - microphoneRecorder = MicrophoneRecorder { [weak self] data in - self?.asrTrTtsHelper?.pushStreamWrite(data) - // 录音点(非 BT 路径):data 为 PCMReader 输出的 16k/16bit 单声道 PCM - self?.recordFile?.saveAudioDataToWav(data) - } - asrTrTtsHelper?.callback = { [weak self] data in - guard let self = self else { return } - let language = data["language"] as? String ?? "" - let side = language.caseInsensitiveCompare(self.sourceLanguageCode) == .orderedSame ? "left" : "right" - var newData = data - newData["side"] = side - self.publishData(newData) - } - // BT 路径事件分发:0xD9/0xDA 触发 ASR 生命周期,mainMicData 喂 ASR(ffMicData 是辅助麦,丢弃) - BluetoothManager.translationCallback = { [weak self] data in - guard let self = self, self.usingBtPath else { return } - switch data["type"] as? String { - case "startMicRecording": - self.startRecognition() - case "stopMicRecording": - self.stopRecognition() - case "mainMicData": - if let pcm = data["data"] as? Data { - self.asrTrTtsHelper?.pushStreamWrite(pcm) - // 录音点(BT 路径):pcm 为 BluetoothManager 已 mSBC 解码的 16k/16bit 单声道 PCM - self.recordFile?.saveAudioDataToWav(pcm) - } - default: - break - } - } - } - - func start(sourceLanguage: String, targetLanguage: String) { - sourceLanguageCode = sourceLanguage - targetLanguageCode = targetLanguage - usingBtPath = BluetoothManager.sharedInstance?.isConnected == true - - let session = AVAudioSession.sharedInstance() - previousCategory = session.category - previousMode = session.mode - previousOptions = session.categoryOptions - - do { - if usingBtPath { - // BT 路径:录音走 BLE 指令通道,iOS 端只播 TTS → .playback + A2DP 走耳机高音质 - try session.setCategory(.playback, mode: .default, options: [.allowBluetoothA2DP]) - } else { - // .allowBluetooth: 有蓝牙耳机时 iOS 自动走 HFP(麦克风+扬声器都走蓝牙) - // .defaultToSpeaker: 没有蓝牙耳机时回退到手机扬声器(不走听筒) - // iOS 会自动处理蓝牙连接/断开的路由切换 - try session.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .defaultToSpeaker]) - } - try session.setActive(true) - } catch {} - - // 注册音频中断处理,中断结束后重新激活音频会话 - interruptionObserver = NotificationCenter.default.addObserver( - forName: AVAudioSession.interruptionNotification, - object: nil, queue: nil - ) { notification in - guard let info = notification.userInfo, - let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt, - let type = AVAudioSession.InterruptionType(rawValue: typeValue), - type == .ended else { return } - try? AVAudioSession.sharedInstance().setActive(true) - } - - asrTrTtsHelper?.switchProvider(languages: [targetLanguage, sourceLanguage]) - - if usingBtPath { - // 复用会议助手「现场录音」指令:通知耳机启动主麦 mSBC 数据流 - // ASR 开启 + startCall 等设备回 0xD9 后在 startRecognition 里发起 - BluetoothManager.writeBytes(Data([0xaa, 0x59])) - } else { - // 在麦克风 VPIO 启用前预热 TTS 播放引擎(让其尽早跑起来、整段复用), - // 消除冷启动首句偏小、停止重启后才变大的问题。 - asrTrTtsHelper?.prepareForPlayback() - // MicrophoneRecorder.echoCancel 默认开启(VoiceProcessing 防 TTS 回灌), - // PCMReader 内部通过 AVAudioEngineConfigurationChange 处理蓝牙切换 - microphoneRecorder?.start() - startRecognition() - } - } - - func stop() { - // 缓存一次:避免在 stop() 执行过程中被 translationCallback 的 stopRecognition 改写后 - // 出现「前半段当 BT 路径处理、后半段当非 BT 处理」的不一致 - let bt = usingBtPath - if bt { - // ASR 关闭 + stopCall 等设备回 0xDA 后在 stopRecognition 里发起 - BluetoothManager.writeBytes(Data([0xaa, 0x5a])) - } else { - microphoneRecorder?.stop() - } - - // 音频会话同步回滚(与 FaceToFaceTranslationHelper 一致) - if let observer = interruptionObserver { - NotificationCenter.default.removeObserver(observer) - interruptionObserver = nil - } - if let cat = previousCategory, let mode = previousMode, let opts = previousOptions { - try? AVAudioSession.sharedInstance().setCategory(cat, mode: mode, options: opts) - } - - // 非 BT 路径在所有同步清理结束后再触发 stopRecognition + publishStopCall, - // 让 UI 看到 stopCall 时资源已全部释放,与 BT 路径下 0xDA 触发的时机一致 - if !bt { - stopRecognition() - } - } - - private func startRecognition() { - guard isRecognitionStopped else { return } - isRecognitionStopped = false - asrTrTtsHelper?.startRecognition() - publishData(["type": "startCall"]) - } - - private func stopRecognition() { - guard !isRecognitionStopped else { return } - isRecognitionStopped = true - asrTrTtsHelper?.stopRecognition() - publishData(["type": "stopCall"]) - usingBtPath = false - } - - private func publishData(_ data: Dictionary) { - let cb = callback - DispatchQueue.main.async { cb?(data) } - } - - // MARK: - 录音 - - func enableRecord(filePath: String) { - // 防御:未 stopRecord 就重复 enableRecord 时先关旧的,避免泄漏挂起写入与文件句柄 - _ = recordFile?.closeFile(isSave: false) - // 单声道录音:16k/16bit/1ch - let file = RecordFile() - file.setAudioConfig(sampleRate: 16000, channels: 1) - file.creatingFiles(atPath: filePath) - recordFile = file - } - - func stopRecord(isSave: Bool) { - _ = recordFile?.closeFile(isSave: isSave) - recordFile = nil - } - - func dispose() { - stop() - // 若 BT 路径下设备没回 0xDA,stopRecognition 还没跑过;这里兜底强制收尾 - stopRecognition() - microphoneRecorder?.dispose() - microphoneRecorder = nil - asrTrTtsHelper?.dispose() - asrTrTtsHelper = nil - // 兜底关闭录音文件(dispose 时保存已写入内容) - _ = recordFile?.closeFile(isSave: true) - recordFile = nil - callback = nil - BluetoothManager.translationCallback = nil - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/TranslationHelperManager.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/TranslationHelperManager.swift deleted file mode 100644 index aa095d3c..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/TranslationHelperManager.swift +++ /dev/null @@ -1,55 +0,0 @@ -import Foundation - -class TranslationHelperManager: TranslationHelperProtocol { - private var currentProvider: TranslationHelperProvider - private var delegate: TranslationHelperProtocol? - - var callback: ((Dictionary) -> Void)? { - didSet { delegate?.callback = callback } - } - - init(provider: TranslationHelperProvider = .call) { - self.currentProvider = provider - switchProvider(provider: provider) - } - - func switchProvider(provider: TranslationHelperProvider) { - try? delegate?.dispose() - currentProvider = provider - - switch provider { - case .simultaneous: - delegate = SimultaneousTranslationHelper() - case .call: - delegate = CallTranslationHelper() - case .faceToFace: - delegate = FaceToFaceTranslationHelper() - } - delegate?.callback = callback - } - - func getProvider() -> TranslationHelperProvider { currentProvider } - - func start(sourceLanguage: String, targetLanguage: String) { - delegate?.start(sourceLanguage: sourceLanguage, targetLanguage: targetLanguage) - } - - func stop() { - delegate?.stop() - } - - func enableRecord(filePath: String) { - delegate?.enableRecord(filePath: filePath) - } - - func stopRecord(isSave: Bool) { - delegate?.stopRecord(isSave: isSave) - } - - func dispose() { - callback = nil - let d = delegate - delegate = nil - d?.dispose() - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/TranslationHelperProtocol.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/TranslationHelperProtocol.swift deleted file mode 100644 index 911ccad4..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/translation_option/TranslationHelperProtocol.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -enum TranslationHelperProvider { - case simultaneous - case call - case faceToFace -} - -protocol TranslationHelperProtocol: AnyObject { - var callback: ((Dictionary) -> Void)? { get set } - - func start(sourceLanguage: String, targetLanguage: String) - func stop() - func dispose() - - /// 开启录音:将 ASR 正在处理的解码后 PCM 写入 WAV 文件。 - /// 同声传译/面对面为单声道;通话翻译为立体声(左=本端麦克风,右=对方)。 - func enableRecord(filePath: String) - /// 停止录音。isSave=false 时丢弃文件。 - func stopRecord(isSave: Bool) -} - -extension TranslationHelperProtocol { - func start() { - start(sourceLanguage: "zh-CN", targetLanguage: "en-US") - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/AzureTts.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/AzureTts.swift deleted file mode 100644 index 70814990..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/AzureTts.swift +++ /dev/null @@ -1,250 +0,0 @@ -import Foundation -import AVFoundation -import MicrosoftCognitiveServicesSpeech -import bluetooth_manager - -class AzureTts: TtsProtocol { - var onSynthesisEncoded: ((Data) -> Void)? - var autoPlay: Bool = false - var forceBuiltInSpeaker: Bool = false - - private var synthesizer: SPXSpeechSynthesizer? - private var g722Codec: G722Codec? - - private let subscriptionKey = "rLPdyGgD1v7z7khzpdrd4YNWUinjw6GzAbA1pPXBRmUjitfjDxdsJQQJ99BGAC3pKaRXJ3w3AAAYACOGAHZZ" - private let region = "eastasia" - - /// 每帧 PCM 字节数:16kHz × 20ms × 2 bytes = 640 bytes(与 Android 一致) - private let framePcmBytes = 640 - - /// 流式缓冲区(与 Android MemoryPushAudioOutputStreamCallback 对齐) - private var stashBuffer = Data(capacity: 8192) - private var stashSize: Int = 0 - - /// 自动播放用的 AVAudioEngine(整个会话期间复用,避免每句重建引擎触发崩溃) - private var audioEngine: AVAudioEngine? - private var playerNode: AVAudioPlayerNode? - /// 引擎图使用标准 Float32 单声道格式(mixer 的规范处理格式),比 Int16 交错格式更兼容,降低 connect 抛异常概率 - private let playbackFormat = AVAudioFormat(standardFormatWithSampleRate: 16000, channels: 1)! - /// 保护 audioEngine / playerNode 的并发访问(合成回调线程与工作队列可能并发) - private let engineLock = NSLock() - /// 建引擎时的输出硬件格式,用于检测音频路由变化后重建引擎(适配新路由) - private var builtOutputFormat: AVAudioFormat? - - init() { - setup() - } - - private func setup() { - do { - let speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) - try speechConfig.setSpeechSynthesisOutputFormat(.raw16Khz16BitMonoPcm) - synthesizer = try SPXSpeechSynthesizer(speechConfiguration: speechConfig, audioConfiguration: nil) - g722Codec = G722Codec(sampleRate: 16000) - - synthesizer?.addSynthesizingEventHandler { [weak self] (_: SPXSpeechSynthesizer, event: SPXSpeechSynthesisEventArgs) in - guard let self = self, let data = event.result.audioData, !data.isEmpty else { return } - self.processStreamingAudio(data) - } - } catch {} - } - - func speakText(text: String, targetLanguage: String, voiceName: String) { - speakSsml(buildSsml(text: text, targetLanguage: targetLanguage, voiceName: voiceName)) - } - - /// 在麦克风 VPIO 启用之前预先构建并启动自动播放引擎(长生命周期,整段会话复用)。 - /// 冷启动若等到第一句才懒构建,扬声器 TTS 会偏小,停止→重启后才变大; - /// 会话起始就把引擎跑起来可消除该差异(真机验证),让第一句即保持正常音量。 - /// 注:响度与 VPIO 把会话切到 .voiceChat(通话音量域,比 .default 媒体音量域更响)有关, - /// 这里只负责让引擎尽早就绪,不改音频路由。非自动播放时为空操作。 - func prepareForPlayback() { - guard autoPlay else { return } - if forceBuiltInSpeaker { - try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) - } - _ = ensurePlaybackEngineReady() - } - - func speakSsml(_ ssml: String) { - guard let synthesizer = synthesizer else { return } - stashSize = 0 - - if autoPlay { - if forceBuiltInSpeaker { - try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) - } - // 准备(必要时创建)可复用的播放引擎;失败时本句不播放,但不会崩溃 - _ = ensurePlaybackEngineReady() - } - - // 阻塞直到合成完成 - try? synthesizer.speakSsml(ssml) - - if autoPlay { - drainPlayback() - } - } - - /// 准备可复用的播放引擎,返回是否就绪。 - /// 关键:连接前确保音频会话已激活且输出节点硬件格式有效, - /// 否则 AVAudioEngine.connect 会抛出未捕获的 ObjC 异常(SetFormat 失败)导致 SIGABRT 崩溃。 - private func ensurePlaybackEngineReady() -> Bool { - engineLock.lock() - defer { engineLock.unlock() } - - // 确保会话已激活(面对面模式下通常已由外层激活,这里再确认一次) - try? AVAudioSession.sharedInstance().setActive(true) - - // 复用已有引擎:仅当仍在运行且音频路由(输出硬件格式)未变化时 - if let engine = audioEngine, let player = playerNode, let built = builtOutputFormat { - let live = engine.outputNode.outputFormat(forBus: 0) - let routeUnchanged = live.sampleRate == built.sampleRate && live.channelCount == built.channelCount - // 路由(输出格式)未变时优先“原地重启”同一引擎:若系统在 VPIO 启停时停掉了播放引擎, - // 重启同一实例即可,避免走下面的销毁+重建路径(历史上重建易触发 SIGABRT)。 - if routeUnchanged { - if !engine.isRunning { try? engine.start() } - if engine.isRunning { - if !player.isPlaying { player.play() } - return true - } - } - // 引擎被系统停止或音频路由已变化:销毁后按当前路由重建 - player.stop() - engine.stop() - audioEngine = nil - playerNode = nil - builtOutputFormat = nil - } - - // 重建引擎 - let engine = AVAudioEngine() - // 校验输出硬件格式有效(采样率/声道数 > 0),无效说明会话未就绪,放弃本次播放以避免 connect 崩溃 - let hwFormat = engine.outputNode.outputFormat(forBus: 0) - guard hwFormat.sampleRate > 0, hwFormat.channelCount > 0 else { return false } - - let player = AVAudioPlayerNode() - engine.attach(player) - engine.connect(player, to: engine.mainMixerNode, format: playbackFormat) - do { - try engine.start() - player.play() - audioEngine = engine - playerNode = player - builtOutputFormat = hwFormat - return true - } catch { - audioEngine = nil - playerNode = nil - builtOutputFormat = nil - return false - } - } - - /// 等待当前已排入的音频播放完毕(带超时保护),但保留引擎以便复用 - private func drainPlayback() { - engineLock.lock() - guard let engine = audioEngine, engine.isRunning, let player = playerNode, - let silence = AVAudioPCMBuffer(pcmFormat: playbackFormat, frameCapacity: 1) else { - engineLock.unlock() - return - } - silence.frameLength = 1 - silence.floatChannelData?[0][0] = 0 - let semaphore = DispatchSemaphore(value: 0) - player.scheduleBuffer(silence) { semaphore.signal() } - engineLock.unlock() - _ = semaphore.wait(timeout: .now() + 30) - } - - /// 处理流式 PCM 数据 - private func processStreamingAudio(_ data: Data) { - if autoPlay { - engineLock.lock() - defer { engineLock.unlock() } - guard let engine = audioEngine, engine.isRunning, - let player = playerNode, let buffer = pcmBuffer(from: data) else { return } - player.scheduleBuffer(buffer) - return - } - - guard let codec = g722Codec else { return } - - // 扩容并追加数据 - let required = stashSize + data.count - if required > stashBuffer.count { - var newCap = max(stashBuffer.count, 8192) - while newCap < required { newCap <<= 1 } - var newBuf = Data(capacity: newCap) - newBuf.append(stashBuffer.prefix(stashSize)) - newBuf.append(data) - stashBuffer = newBuf - } else { - if stashSize < stashBuffer.count { - let end = min(stashSize + data.count, stashBuffer.count) - stashBuffer.replaceSubrange(stashSize..= framePcmBytes { - let frame = Data(stashBuffer[cursor.. 0 { - let remaining = stashSize - cursor - if remaining > 0 { - let leftover = Data(stashBuffer[cursor.. AVAudioPCMBuffer? { - // Azure 输出为小端 Int16 单声道(每帧 2 字节),转换为引擎所用的 Float32 - let frameCount = AVAudioFrameCount(data.count / 2) - guard frameCount > 0, - let buffer = AVAudioPCMBuffer(pcmFormat: playbackFormat, frameCapacity: frameCount), - let dst = buffer.floatChannelData else { return nil } - buffer.frameLength = frameCount - data.withUnsafeBytes { raw in - guard let src = raw.bindMemory(to: Int16.self).baseAddress else { return } - for i in 0.. String { - return """ - - - - \(text) - - - - """ - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/TtsManager.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/TtsManager.swift deleted file mode 100644 index 61d393fb..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/TtsManager.swift +++ /dev/null @@ -1,60 +0,0 @@ -import Foundation - -class TtsManager: TtsProtocol { - private var currentProvider: TtsProvider - private var delegate: TtsProtocol? - - var onSynthesisEncoded: ((Data) -> Void)? { - didSet { delegate?.onSynthesisEncoded = onSynthesisEncoded } - } - - var autoPlay: Bool = false { - didSet { delegate?.autoPlay = autoPlay } - } - - var forceBuiltInSpeaker: Bool = false { - didSet { delegate?.forceBuiltInSpeaker = forceBuiltInSpeaker } - } - - init(provider: TtsProvider = .azure, autoPlay: Bool = false, forceBuiltInSpeaker: Bool = false) { - self.currentProvider = provider - self.autoPlay = autoPlay - self.forceBuiltInSpeaker = forceBuiltInSpeaker - createDelegate() - } - - func switchProvider(provider: TtsProvider? = nil) { - if let p = provider { currentProvider = p } - delegate?.dispose() - createDelegate() - } - - private func createDelegate() { - switch currentProvider { - case .azure: - delegate = AzureTts() - case .volcengine: - delegate = VolcengineTts() - } - delegate?.onSynthesisEncoded = onSynthesisEncoded - delegate?.autoPlay = autoPlay - delegate?.forceBuiltInSpeaker = forceBuiltInSpeaker - } - - func getProvider() -> TtsProvider { currentProvider } - - func speakText(text: String, targetLanguage: String, voiceName: String) { - delegate?.speakText(text: text, targetLanguage: targetLanguage, voiceName: voiceName) - } - - func prepareForPlayback() { - delegate?.prepareForPlayback() - } - - func dispose() { - onSynthesisEncoded = nil - let d = delegate - delegate = nil - d?.dispose() - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/TtsProtocol.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/TtsProtocol.swift deleted file mode 100644 index 5a4a0502..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/TtsProtocol.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -enum TtsProvider { - case azure - case volcengine -} - -protocol TtsProtocol: AnyObject { - var onSynthesisEncoded: ((Data) -> Void)? { get set } - var autoPlay: Bool { get set } - var forceBuiltInSpeaker: Bool { get set } - - func speakText(text: String, targetLanguage: String, voiceName: String) - /// 预热自动播放引擎:在麦克风 VPIO 启用之前、会话刚激活时调用, - /// 让长生命周期的播放引擎尽早跑起来并整段复用,消除冷启动首句偏小、停止重启后才变大的问题。 - /// 非自动播放时为空操作。 - func prepareForPlayback() - func dispose() -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/VolcengineTts.swift b/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/VolcengineTts.swift deleted file mode 100644 index ee6473b8..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/ios/asr_tr_tts_manager/Sources/asr_tr_tts_manager/tts/VolcengineTts.swift +++ /dev/null @@ -1,249 +0,0 @@ -import Foundation -import AVFoundation -import MicrosoftCognitiveServicesSpeech -import bluetooth_manager - -class VolcengineTts: TtsProtocol { - var onSynthesisEncoded: ((Data) -> Void)? - var autoPlay: Bool = false - var forceBuiltInSpeaker: Bool = false - - private var synthesizer: SPXSpeechSynthesizer? - private var g722Codec: G722Codec? - - private let subscriptionKey = "rLPdyGgD1v7z7khzpdrd4YNWUinjw6GzAbA1pPXBRmUjitfjDxdsJQQJ99BGAC3pKaRXJ3w3AAAYACOGAHZZ" - private let region = "eastasia" - - /// 每帧 PCM 字节数:16kHz × 20ms × 2 bytes = 640 bytes(与 Android 一致) - private let framePcmBytes = 640 - - /// 流式缓冲区(与 Android MemoryPushAudioOutputStreamCallback 对齐) - private var stashBuffer = Data(capacity: 8192) - private var stashSize: Int = 0 - - /// 自动播放用的 AVAudioEngine(整个会话期间复用,避免每句重建引擎触发崩溃) - private var audioEngine: AVAudioEngine? - private var playerNode: AVAudioPlayerNode? - /// 引擎图使用标准 Float32 单声道格式(mixer 的规范处理格式),比 Int16 交错格式更兼容,降低 connect 抛异常概率 - private let playbackFormat = AVAudioFormat(standardFormatWithSampleRate: 16000, channels: 1)! - /// 保护 audioEngine / playerNode 的并发访问(合成回调线程与工作队列可能并发) - private let engineLock = NSLock() - /// 建引擎时的输出硬件格式,用于检测音频路由变化后重建引擎(适配新路由) - private var builtOutputFormat: AVAudioFormat? - - init() { - setup() - } - - private func setup() { - do { - let speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) - try speechConfig.setSpeechSynthesisOutputFormat(.raw16Khz16BitMonoPcm) - synthesizer = try SPXSpeechSynthesizer(speechConfiguration: speechConfig, audioConfiguration: nil) - g722Codec = G722Codec(sampleRate: 16000) - - synthesizer?.addSynthesizingEventHandler { [weak self] (_: SPXSpeechSynthesizer, event: SPXSpeechSynthesisEventArgs) in - guard let self = self, let data = event.result.audioData, !data.isEmpty else { return } - self.processStreamingAudio(data) - } - } catch {} - } - - func speakText(text: String, targetLanguage: String, voiceName: String) { - speakSsml(buildSsml(text: text, targetLanguage: targetLanguage, voiceName: voiceName)) - } - - /// 在麦克风 VPIO 启用之前预先构建并启动自动播放引擎(长生命周期,整段会话复用)。 - /// 冷启动若等到第一句才懒构建,扬声器 TTS 会偏小,停止→重启后才变大; - /// 会话起始就把引擎跑起来可消除该差异(真机验证),让第一句即保持正常音量。 - /// 注:响度与 VPIO 把会话切到 .voiceChat(通话音量域,比 .default 媒体音量域更响)有关, - /// 这里只负责让引擎尽早就绪,不改音频路由。非自动播放时为空操作。 - func prepareForPlayback() { - guard autoPlay else { return } - if forceBuiltInSpeaker { - try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) - } - _ = ensurePlaybackEngineReady() - } - - func speakSsml(_ ssml: String) { - guard let synthesizer = synthesizer else { return } - stashSize = 0 - - if autoPlay { - if forceBuiltInSpeaker { - try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) - } - // 准备(必要时创建)可复用的播放引擎;失败时本句不播放,但不会崩溃 - _ = ensurePlaybackEngineReady() - } - - try? synthesizer.speakSsml(ssml) - - if autoPlay { - drainPlayback() - } - } - - /// 准备可复用的播放引擎,返回是否就绪。 - /// 关键:连接前确保音频会话已激活且输出节点硬件格式有效, - /// 否则 AVAudioEngine.connect 会抛出未捕获的 ObjC 异常(SetFormat 失败)导致 SIGABRT 崩溃。 - private func ensurePlaybackEngineReady() -> Bool { - engineLock.lock() - defer { engineLock.unlock() } - - // 确保会话已激活(面对面模式下通常已由外层激活,这里再确认一次) - try? AVAudioSession.sharedInstance().setActive(true) - - // 复用已有引擎:仅当仍在运行且音频路由(输出硬件格式)未变化时 - if let engine = audioEngine, let player = playerNode, let built = builtOutputFormat { - let live = engine.outputNode.outputFormat(forBus: 0) - let routeUnchanged = live.sampleRate == built.sampleRate && live.channelCount == built.channelCount - // 路由(输出格式)未变时优先“原地重启”同一引擎:若系统在 VPIO 启停时停掉了播放引擎, - // 重启同一实例即可,避免走下面的销毁+重建路径(历史上重建易触发 SIGABRT)。 - if routeUnchanged { - if !engine.isRunning { try? engine.start() } - if engine.isRunning { - if !player.isPlaying { player.play() } - return true - } - } - // 引擎被系统停止或音频路由已变化:销毁后按当前路由重建 - player.stop() - engine.stop() - audioEngine = nil - playerNode = nil - builtOutputFormat = nil - } - - // 重建引擎 - let engine = AVAudioEngine() - // 校验输出硬件格式有效(采样率/声道数 > 0),无效说明会话未就绪,放弃本次播放以避免 connect 崩溃 - let hwFormat = engine.outputNode.outputFormat(forBus: 0) - guard hwFormat.sampleRate > 0, hwFormat.channelCount > 0 else { return false } - - let player = AVAudioPlayerNode() - engine.attach(player) - engine.connect(player, to: engine.mainMixerNode, format: playbackFormat) - do { - try engine.start() - player.play() - audioEngine = engine - playerNode = player - builtOutputFormat = hwFormat - return true - } catch { - audioEngine = nil - playerNode = nil - builtOutputFormat = nil - return false - } - } - - /// 等待当前已排入的音频播放完毕(带超时保护),但保留引擎以便复用 - private func drainPlayback() { - engineLock.lock() - guard let engine = audioEngine, engine.isRunning, let player = playerNode, - let silence = AVAudioPCMBuffer(pcmFormat: playbackFormat, frameCapacity: 1) else { - engineLock.unlock() - return - } - silence.frameLength = 1 - silence.floatChannelData?[0][0] = 0 - let semaphore = DispatchSemaphore(value: 0) - player.scheduleBuffer(silence) { semaphore.signal() } - engineLock.unlock() - _ = semaphore.wait(timeout: .now() + 30) - } - - /// 处理流式 PCM 数据 - private func processStreamingAudio(_ data: Data) { - if autoPlay { - engineLock.lock() - defer { engineLock.unlock() } - guard let engine = audioEngine, engine.isRunning, - let player = playerNode, let buffer = pcmBuffer(from: data) else { return } - player.scheduleBuffer(buffer) - return - } - - guard let codec = g722Codec else { return } - - // 扩容并追加数据 - let required = stashSize + data.count - if required > stashBuffer.count { - var newCap = max(stashBuffer.count, 8192) - while newCap < required { newCap <<= 1 } - var newBuf = Data(capacity: newCap) - newBuf.append(stashBuffer.prefix(stashSize)) - newBuf.append(data) - stashBuffer = newBuf - } else { - if stashSize < stashBuffer.count { - let end = min(stashSize + data.count, stashBuffer.count) - stashBuffer.replaceSubrange(stashSize..= framePcmBytes { - let frame = Data(stashBuffer[cursor.. 0 { - let remaining = stashSize - cursor - if remaining > 0 { - let leftover = Data(stashBuffer[cursor.. AVAudioPCMBuffer? { - // 输出为小端 Int16 单声道(每帧 2 字节),转换为引擎所用的 Float32 - let frameCount = AVAudioFrameCount(data.count / 2) - guard frameCount > 0, - let buffer = AVAudioPCMBuffer(pcmFormat: playbackFormat, frameCapacity: frameCount), - let dst = buffer.floatChannelData else { return nil } - buffer.frameLength = frameCount - data.withUnsafeBytes { raw in - guard let src = raw.bindMemory(to: Int16.self).baseAddress else { return } - for i in 0.. String { - return """ - - - - \(text) - - - - """ - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/lib/asr_tr_tts_manager.dart b/apps/client/local_plugins/asr_tr_tts_manager/lib/asr_tr_tts_manager.dart deleted file mode 100644 index 6f21313a..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/lib/asr_tr_tts_manager.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'asr_tr_tts_manager_platform_interface.dart'; - -class AsrTrTtsManager { - Future translationInit(Map data) { - return AsrTrTtsManagerPlatform.instance.translationInit(data); - } - - Future translationStart(Map data) { - return AsrTrTtsManagerPlatform.instance.translationStart(data); - } - - Future translationStop() { - return AsrTrTtsManagerPlatform.instance.translationStop(); - } - - Future translationDispose() { - return AsrTrTtsManagerPlatform.instance.translationDispose(); - } - - /// 开始录音:将 ASR 正在处理的 PCM 帧写成 WAV。 - /// [data]: {'filePath': String}。声道由原生按当前翻译模式决定 - /// (同声/设备辅助=单声道,通话=双声道)。 - Future enableRecord(Map data) { - return AsrTrTtsManagerPlatform.instance.enableRecord(data); - } - - /// 停止录音。[data]: {'isSave': bool},false 丢弃文件。 - Future stopRecord(Map data) { - return AsrTrTtsManagerPlatform.instance.stopRecord(data); - } - - Stream onTranslationEvent() { - return AsrTrTtsManagerPlatform.instance.onTranslationEvent(); - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/lib/asr_tr_tts_manager_method_channel.dart b/apps/client/local_plugins/asr_tr_tts_manager/lib/asr_tr_tts_manager_method_channel.dart deleted file mode 100644 index 12d61ec6..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/lib/asr_tr_tts_manager_method_channel.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -import 'asr_tr_tts_manager_platform_interface.dart'; - -/// An implementation of [AsrTrTtsManagerPlatform] that uses method channels. -class MethodChannelAsrTrTtsManager extends AsrTrTtsManagerPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - final methodChannel = const MethodChannel('asr_tr_tts_manager'); - final translationChannel = const EventChannel( - "asr_tr_tts_manager/translation", - ); - - Stream? _translationEventStream; - StreamSubscription? _translationEventSubscription; - - final StreamController translationEvent = - StreamController.broadcast(); - - void _onTranslationEvent(Map event) { - translationEvent.add(event); - } - - @override - Future translationInit(Map data) async { - final result = await methodChannel.invokeMethod( - 'translationInit', - data, - ); - return result ?? false; - } - - @override - Future translationStart(Map data) async { - final result = await methodChannel.invokeMethod( - 'translationStart', - data, - ); - return result ?? false; - } - - @override - Future translationStop() async { - final result = await methodChannel.invokeMethod( - 'translationStop', - ); - return result ?? false; - } - - @override - Future translationDispose() async { - await _translationEventSubscription?.cancel(); - _translationEventSubscription = null; - _translationEventStream = null; - - final result = await methodChannel.invokeMethod( - 'translationDispose', - ); - return result ?? false; - } - - @override - Future enableRecord(Map data) async { - final result = await methodChannel.invokeMethod( - 'enableRecord', - data, - ); - return result ?? false; - } - - @override - Future stopRecord(Map data) async { - final result = await methodChannel.invokeMethod( - 'stopRecord', - data, - ); - return result ?? false; - } - - @override - Stream onTranslationEvent() { - if (_translationEventStream == null) { - _translationEventStream = translationChannel.receiveBroadcastStream(); - _translationEventSubscription = _translationEventStream!.listen((event) { - if (event is Map) { - _onTranslationEvent(event); - } - }); - } - - return translationEvent.stream; - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/lib/asr_tr_tts_manager_platform_interface.dart b/apps/client/local_plugins/asr_tr_tts_manager/lib/asr_tr_tts_manager_platform_interface.dart deleted file mode 100644 index 82648d11..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/lib/asr_tr_tts_manager_platform_interface.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'asr_tr_tts_manager_method_channel.dart'; - -abstract class AsrTrTtsManagerPlatform extends PlatformInterface { - /// Constructs a AsrTrTtsManagerPlatform. - AsrTrTtsManagerPlatform() : super(token: _token); - - static final Object _token = Object(); - - static AsrTrTtsManagerPlatform _instance = MethodChannelAsrTrTtsManager(); - - /// The default instance of [AsrTrTtsManagerPlatform] to use. - /// - /// Defaults to [MethodChannelAsrTrTtsManager]. - static AsrTrTtsManagerPlatform get instance => _instance; - - /// Platform-specific implementations should set this with their own - /// platform-specific class that extends [AsrTrTtsManagerPlatform] when - /// they register themselves. - static set instance(AsrTrTtsManagerPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - Future translationInit(Map data) { - throw UnimplementedError('translationInit() has not been implemented.'); - } - - Future translationStart(Map data) { - throw UnimplementedError('translationStart() has not been implemented.'); - } - - Future translationStop() { - throw UnimplementedError('translationStop() has not been implemented.'); - } - - Future translationDispose() { - throw UnimplementedError('translationDispose() has not been implemented.'); - } - - Future enableRecord(Map data) { - throw UnimplementedError('enableRecord() has not been implemented.'); - } - - Future stopRecord(Map data) { - throw UnimplementedError('stopRecord() has not been implemented.'); - } - - Stream onTranslationEvent() { - throw UnimplementedError('onTranslationEvent() has not been implemented.'); - } -} diff --git a/apps/client/local_plugins/asr_tr_tts_manager/pubspec.yaml b/apps/client/local_plugins/asr_tr_tts_manager/pubspec.yaml deleted file mode 100644 index c21a4eb9..00000000 --- a/apps/client/local_plugins/asr_tr_tts_manager/pubspec.yaml +++ /dev/null @@ -1,72 +0,0 @@ -name: asr_tr_tts_manager -description: "A new Flutter project." -version: 0.0.1 -homepage: - -environment: - sdk: ^3.10.4 - flutter: '>=3.3.0' - -dependencies: - flutter: - sdk: flutter - plugin_platform_interface: ^2.0.2 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^6.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) - # which should be registered in the plugin registry. This is required for - # using method channels. - # The Android 'package' specifies package in which the registered class is. - # This is required for using method channels on Android. - # The 'ffiPlugin' specifies that native code should be built and bundled. - # This is required for using `dart:ffi`. - # All these are used by the tooling to maintain consistency when - # adding or updating assets for this project. - plugin: - platforms: - android: - package: com.example.asr_tr_tts_manager - pluginClass: AsrTrTtsManagerPlugin - ios: - pluginClass: AsrTrTtsManagerPlugin - - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/to/asset-from-package - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # To add custom fonts to your plugin package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/to/font-from-package diff --git a/apps/client/local_plugins/jl_opus/README.md b/apps/client/local_plugins/jl_opus/README.md deleted file mode 100644 index 124a1c56..00000000 --- a/apps/client/local_plugins/jl_opus/README.md +++ /dev/null @@ -1,126 +0,0 @@ -# JL OPUS Plugin - -基于杰理音频编解码库的Flutter插件,提供OPUS音频格式的解码功能。 - -## 功能 - -- 支持OPUS文件解码为PCM格式 -- 支持OPUS数据流解码 -- 支持多种采样率和通道配置 -- 支持带协议头/不带协议头的OPUS数据 - -## 引入方式 - -在pubspec.yaml中添加依赖: - -```yaml -dependencies: - jl_opus: - path: ./local_plugins/jl_opus -``` - -## 使用方法 - -### 初始化插件 - -```dart -import 'package:jl_opus/jl_opus.dart'; - -final jlOpus = JlOpus(); -``` - -### 解码OPUS文件 - -```dart -Future decodeOpusFile() async { - final inPath = '/path/to/input.opus'; - final outPath = '/path/to/output.pcm'; - - // 创建解码选项 - final option = OpusOption( - hasHeader: false, // 是否有协议头 - channel: 1, // 通道数:1=单声道,2=立体声 - sampleRate: 16000, // 采样率 - packetSize: 40, // 数据包大小(仅当hasHeader=false时生效) - ); - - try { - // 开始解码文件 - final outFilePath = await jlOpus.decodeOpusFile(inPath, outPath, option); - print('解码成功: $outFilePath'); - } catch (e) { - print('解码失败: $e'); - } -} -``` - -### 解码OPUS数据流 - -```dart -Future decodeOpusStream() async { - // 监听解码后的PCM数据 - jlOpus.decodedDataStream.listen((pcmData) { - print('收到解码后的PCM数据: ${pcmData.length} 字节'); - // 处理解码后的PCM数据 - }); - - // 创建解码选项 - final option = OpusOption( - hasHeader: false, - channel: 1, - sampleRate: 16000, - packetSize: 40, - ); - - // 开始解码数据流 - final success = await jlOpus.startDecodeStream(option); - if (success) { - print('开始解码数据流'); - - // 写入OPUS数据进行解码 - final opusData = Uint8List.fromList([/* OPUS数据 */]); - await jlOpus.writeAudioStream(opusData); - - // 数据处理完成后停止解码 - await jlOpus.stopDecodeStream(); - } -} -``` - -### 监听事件 - -```dart -void listenToEvents() { - jlOpus.eventStream.listen((event) { - switch (event.event) { - case 'onStart': - print('开始${event.type == "file" ? "文件" : "流"}解码'); - break; - case 'onComplete': - print('完成${event.type == "file" ? "文件" : "流"}解码'); - if (event.filePath != null) { - print('输出文件: ${event.filePath}'); - } - break; - case 'onError': - print('解码错误: [${event.errorCode}] ${event.errorMessage}'); - break; - } - }); -} -``` - -### 资源释放 - -```dart -Future disposePlugin() async { - await jlOpus.dispose(); - print('插件资源已释放'); -} -``` - -## 注意事项 - -1. OPUS解码需要正确设置参数,特别是采样率和通道数必须与原始音频匹配 -2. 使用前请确保jl_audio_decode_V2.0.0_20005.aar包已添加到libs目录 -3. 本插件仅支持Android平台 \ No newline at end of file diff --git a/apps/client/local_plugins/jl_opus/android/build.gradle.kts b/apps/client/local_plugins/jl_opus/android/build.gradle.kts deleted file mode 100644 index cb74f462..00000000 --- a/apps/client/local_plugins/jl_opus/android/build.gradle.kts +++ /dev/null @@ -1,62 +0,0 @@ -plugins { - // Android Library 插件 - id("com.android.library") - // Kotlin Android 插件 - id("org.jetbrains.kotlin.android") - id("com.github.johnrengelman.shadow") version "8.1.1" -} - -// 添加mavenLocal()仓库 -repositories { - mavenLocal() - google() - mavenCentral() -} - -android { - // 命名空间,对应你插件的包名(需与代码内包名保持一致) - namespace = "com.yunqiinnovation.jl_opus" - - // 目标 SDK 版本 - compileSdk = 35 - - defaultConfig { - // 最低 SDK 版本 - minSdk = 21 - targetSdk = 35 - } - - // Java 语言级别兼容配置 - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - // Kotlin 语言级别 - kotlinOptions { - jvmTarget = "11" - } - - // 添加packaging配置,排除冲突文件 - packaging { - resources { - excludes.add("META-INF/DEPENDENCIES") - excludes.add("META-INF/LICENSE") - excludes.add("META-INF/LICENSE.txt") - excludes.add("META-INF/license.txt") - excludes.add("META-INF/NOTICE") - excludes.add("META-INF/NOTICE.txt") - excludes.add("META-INF/notice.txt") - excludes.add("META-INF/*.kotlin_module") - } - } -} - -dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10") - implementation("androidx.annotation:annotation:1.5.0") - implementation("androidx.core:core-ktx:1.9.0") - // 使用从本地Maven仓库引用的AAR依赖 - implementation("com.jieli.audio:jl_audio_decode:2.1.0") -} - diff --git a/apps/client/local_plugins/jl_opus/android/settings.gradle.kts b/apps/client/local_plugins/jl_opus/android/settings.gradle.kts deleted file mode 100644 index 453a0b3f..00000000 --- a/apps/client/local_plugins/jl_opus/android/settings.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = "jl_opus" \ No newline at end of file diff --git a/apps/client/local_plugins/jl_opus/android/src/main/AndroidManifest.xml b/apps/client/local_plugins/jl_opus/android/src/main/AndroidManifest.xml deleted file mode 100644 index 331a4778..00000000 --- a/apps/client/local_plugins/jl_opus/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/apps/client/local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt b/apps/client/local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt deleted file mode 100644 index a2c9ad16..00000000 --- a/apps/client/local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt +++ /dev/null @@ -1,285 +0,0 @@ -package com.yunqiinnovation.jl_opus - -import android.content.Context -import android.os.Handler -import android.os.Looper -import android.util.Log -import androidx.annotation.NonNull -import com.jieli.jl_audio_decode.opus.model.OpusOption -import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback -import com.jieli.jl_audio_decode.callback.OnStateCallback -import com.jieli.jl_audio_decode.opus.OpusManager -import com.jieli.jl_audio_decode.exceptions.OpusException -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import java.io.File - -/** JlOpusPlugin */ -class JlOpusPlugin: FlutterPlugin, MethodCallHandler { - private lateinit var channel: MethodChannel - private lateinit var eventChannel: EventChannel - private lateinit var context: Context - private var opusManager: OpusManager? = null - private var streamSink: EventChannel.EventSink? = null - - // 添加主线程Handler用于确保在主线程回调 - private val mainHandler = Handler(Looper.getMainLooper()) - - companion object { - private const val TAG = "JlOpusPlugin" - private const val CHANNEL_NAME = "com.yunqiinnovation.jl_opus" - private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_opus/events" - } - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - context = flutterPluginBinding.applicationContext - channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) - channel.setMethodCallHandler(this) - - eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME) - eventChannel.setStreamHandler(object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - streamSink = events - } - - override fun onCancel(arguments: Any?) { - streamSink = null - } - }) - - - } - - // 在主线程上安全发送事件 - private fun sendEventOnMainThread(event: Map) { - if (streamSink == null) return - - if (Looper.myLooper() == Looper.getMainLooper()) { - // 如果已经在主线程,直接发送 - streamSink?.success(event) - } else { - // 否则切换到主线程 - mainHandler.post { - streamSink?.success(event) - } - } - } - - // 在主线程上安全返回结果 - private fun sendResultOnMainThread(result: Result, value: Any?) { - if (Looper.myLooper() == Looper.getMainLooper()) { - // 如果已经在主线程,直接返回 - result.success(value) - } else { - // 否则切换到主线程 - mainHandler.post { - result.success(value) - } - } - } - - // 在主线程上安全返回错误 - private fun sendErrorOnMainThread(result: Result, errorCode: String, errorMessage: String, errorDetails: Any?) { - if (Looper.myLooper() == Looper.getMainLooper()) { - // 如果已经在主线程,直接返回 - result.error(errorCode, errorMessage, errorDetails) - } else { - // 否则切换到主线程 - mainHandler.post { - result.error(errorCode, errorMessage, errorDetails) - } - } - } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - // if (opusManager == null) { - // result.error("OPUS_MANAGER_NULL", "OpusManager未初始化", null) - // return - // } - - when (call.method) { - "initOpusDecoder" -> { - // 初始化OpusManager - - opusManager = OpusManager() - Log.i("TAG", "OpusManager初始化成功") - result.success(true) - - - } - "decodeOpusFile" -> { - val inPath = call.argument("inPath") - val outPath = call.argument("outPath") - val hasHeader = call.argument("hasHeader") ?: false - val channel = call.argument("channel") ?: 1 - val sampleRate = call.argument("sampleRate") ?: 16000 - val packetSize = call.argument("packetSize") ?: 40 - - if (inPath == null || outPath == null) { - result.error("INVALID_ARGS", "输入或输出路径不能为空", null) - return - } - - // 确保输入文件存在 - val inFile = File(inPath) - if (!inFile.exists()) { - result.error("FILE_NOT_FOUND", "输入文件不存在: $inPath", null) - return - } - - // 创建解码参数 - val option = OpusOption() - .setHasHead(hasHeader) - .setChannel(channel) - .setSampleRate(sampleRate) - .setPacketSize(packetSize) - - Log.d(TAG, "开始解码文件, 参数: $option, 输入: $inPath, 输出: $outPath") - - opusManager?.decodeFile(inPath, outPath, option, object : OnStateCallback { - override fun onStart() { - Log.d(TAG, "文件解码开始") - sendEventOnMainThread(mapOf( - "event" to "onStart", - "type" to "file" - )) - } - - override fun onComplete(outFilePath: String?) { - Log.d(TAG, "文件解码完成: $outFilePath") - sendEventOnMainThread(mapOf( - "event" to "onComplete", - "type" to "file", - "filePath" to outFilePath - )) - sendResultOnMainThread(result, outFilePath) - } - - override fun onError(code: Int, message: String?) { - Log.e(TAG, "文件解码错误: [$code] $message") - sendEventOnMainThread(mapOf( - "event" to "onError", - "type" to "file", - "code" to code, - "message" to message - )) - sendErrorOnMainThread(result, "DECODE_ERROR", "解码出错: [$code] $message", null) - } - }) - } - "startDecodeStream" -> { - val hasHeader = call.argument("hasHeader") ?: false - val channel = call.argument("channel") ?: 1 - val sampleRate = call.argument("sampleRate") ?: 16000 - val packetSize = call.argument("packetSize") ?: 40 - - // 如果已经在解码流,先停止 - if (opusManager?.isDecodeStream == true) { - opusManager?.stopDecodeStream() - } - - // 创建解码参数 - val option = OpusOption() - .setHasHead(hasHeader) - .setChannel(channel) - .setSampleRate(sampleRate) - .setPacketSize(packetSize) - - Log.d(TAG, "开始解码数据流, 参数: $option") - - opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { - override fun onDecodeStream(data: ByteArray?) { - if (data != null) { - sendEventOnMainThread(mapOf( - "event" to "onDecodeStream", - "data" to data - )) - } - } - - override fun onStart() { - Log.d(TAG, "数据流解码开始") - sendEventOnMainThread(mapOf( - "event" to "onStart", - "type" to "stream" - )) - sendResultOnMainThread(result, true) - } - - override fun onComplete(outPath: String?) { - Log.d(TAG, "数据流解码完成: $outPath") - sendEventOnMainThread(mapOf( - "event" to "onComplete", - "type" to "stream" - )) - } - - override fun onError(code: Int, message: String?) { - Log.e(TAG, "数据流解码错误: [$code] $message") - sendEventOnMainThread(mapOf( - "event" to "onError", - "type" to "stream", - "code" to code, - "message" to message - )) - sendErrorOnMainThread(result, "DECODE_STREAM_ERROR", "数据流解码出错: [$code] $message", null) - } - }) - } - "stopDecodeStream" -> { - if (opusManager?.isDecodeStream == true) { - opusManager?.stopDecodeStream() - result.success(true) - } else { - result.success(false) - } - } - "writeAudioStream" -> { - val data = call.argument("data") - if (data == null) { - result.error("INVALID_ARGS", "音频数据不能为空", null) - return - } - - if (opusManager?.isDecodeStream == true) { - opusManager?.writeAudioStream(data) - result.success(true) - } else { - result.error("NOT_DECODING", "当前没有处于解码状态", null) - } - } - "isDecoding" -> { - result.success(opusManager?.isDecodeStream == true) - } - "dispose" -> { - opusManager?.let { - if (it.isDecodeStream) { - it.stopDecodeStream() - } - it.release() - } - opusManager = null - result.success(true) - } - else -> { - result.notImplemented() - } - } - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - eventChannel.setStreamHandler(null) - opusManager?.let { - if (it.isDecodeStream) { - it.stopDecodeStream() - } - it.release() - } - opusManager = null - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/jl_opus/android/src/main/libs/jl_audio_decode_V2.1.0_20012-release.aar b/apps/client/local_plugins/jl_opus/android/src/main/libs/jl_audio_decode_V2.1.0_20012-release.aar deleted file mode 100644 index 1a609561d533cdbbd161459eece838e2038f85ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1445327 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
I(w4Ao{e=G`y!6{mD0rvx?bbJq3DhX09LL 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%0D|GoO?a<6j;mZ^e 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�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 z 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 z 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

  • 63Lcr70CO$Zn%S zT|tiY+jV%RQ3P!l1-W$;+D{B>;rW<#GbEK~rsRZnH1M18(4e*yZHcE#P7j`OXGp|@ zr!Z(mN);PpzI2A=})-ywL!}B=nuO)NEOsTTsG3ehOz^uK3BG8S`w z0K@<(saQivgwyBH-OvNLiVqX+Cc{mtI<+W}79~2j6Hh)0+R16z*3RW5)bW&xMKrz+ zYSa?5cBjry+AeW3=fS5<+sHT2JngP+_IZ~-dqdYiGp}^_^l6e*&!hj>LPuO(^<-g?8r34FQHv`5uZ4GyP;!u-Qb{izGX&{ijbuI50j1-l{eflhMu`NI zkn|VKqIsxB+PbbjNV--_mSYb}`il9|wo|3jsdZ)2m}4c<>5Avz*+bHS(kCUI^&#oc z#<|jM+nx*3z!TDI1-D2qH>OGHhwhhBJ^xGkzM@bPkFAi*>(V9ultxk=a!8vzE@|^R zouoeXq?F;gRni`+lzwZ>l1#^DNq-nzCoS0Kk~SK7NquZ9z-?^lcAI z2dq7EPTgZtujdI#yY4n8D{UB@FR|Mmm9`ciHNc*<6 z1Fq&sQyL$YzBT?@`eW&*Fji_<(bxcA@%8c0_h$ePZwO;vpw0y_#-9g?@u0H@#`3Al zbKlIkN;w9Fazd{&^=Lz9P^bsj z_c^UjhxAEBk@O$HDd@qssgy{+^^`-|nbJ!YGbA-|g-|+E`U_y#t}T}AM(B?=P2x(+ zm2b5N`-Wv_N~VfiU{nauPD+eZ*5OKE4}61sfM>$G_>pIToppF0WyUNiu0jux?*Ob7}3srOv(ae5=Mg6tU5yg zLt4ZaF>$$LcAoBw&oMl!_9xWGKTm;w>VY@rypFQuO1Gvyp`k~f(O%++Yq(#aVmqU& zuLKU!@{Fj~JPKE;c%bkCnh$vMAiO~7rD;+|BjB_Gy+ft|<#YxyKG}u>p!9;Jz)$}o zrLIepHdO$%mzqIV0-tF2D3(VX8`aXs=>1U2N}IPWa~4TW>p1Dtid~qeRPwKTTv7uS z#aSN#X}DIp88BZAn0J7zn(xV%{$<@G(s!k~lD~9~v=3z6e(PhB%Lp3{X$)p`4X_j=1t_q3={EvNf z#p`;XI*sPn0>5TPt^@sW3oDsiRy4Utp5Z9m#JP53@}rt@Cg7|_kB zD8TX@*1>kP^x^$5)A(1VAQVg6jLt;CD2%Qhw9bUI?C@jnj_S{PA;G*;@B$74A~84F znKNWcs$lU$UmpJKir0}ekxpTUm;su`ieKin09*+j8G>50b4_-`**giH$rLa{nt{=$ z8A8}cC*uC~qzNi%>nyuWY`8mQX=4PNfP2H);gW!5Bzq!8X#X%W znU)1A^=PZD?Tt6_f9a@{rqy;UcAb%Q>aAubi(8MRVpelK_p zRNSP&)9ly9cC|mHSe~)`)d;653#^FWXm}elf6DpUn3s1!_W{rd_{qg`;o<#4vAp17 zS>T!Y;=aUH7$<`j<8@Sw=T%}nrxD}T^~llFp9kLKm6)k|xR2S7VGX9o_>O1+LEs+x z`0)9BE_(53B0Tz7YdMe}Xw6a^RI(*oQA+&Dw?v8di}5`Q4=;gMRiamg*61dI^HcE| zif4S!sFujtdtMbew%)GmdP`U+^Q&miVws;`Z~yKBBMfvW3o9`j@Q(Ga_o`?1ERkm{ z;KdE>I|9kz=3sR2SHz9_$AAVm#$!*g7r#gOk&p0FQP0J!ZFtrU^9yi{2XlWT>^!WH zVAY^PzF|RMq>>BaFCB&dD<$jnKlk9-TUlVM>MPmc#pq@E3rdA{&utS>Z^g{+TrNsy zT40vgif2JOr321D3!lS*1W|+3Vq*3>Mw6t%2sB2tPXQ@r?7`f)dffATHt??ETa5W} z^KL2&oMdS}8R8?b{t-_St+<}*M3wrzi?jgV)8Gjs#oJeH^E)5sA8o}Q`0sM&W{15Os*+jq-3`~y_ck?u)yI57r*hS>qHa=kPZL%c z=v=M1Q9I}(K)1~9vcPJV?($YM;s)J*!~F(oh1E&kv~Ko!l2K5zNT74L7w&SrBA(EH z3hU1k@nwNc7-NX#L|%j5yy#E#kQ04Exfy1O6vXB^#yBg=WZmyX9yq~plD&Y7WehDq z`ttjhL2m~8xEI%?0H0WF-Rujx2uEK7TpWsUEVQA28?dJ0U_Mb=U`4$eEdUsx2Fx?g z7r=}HGcmMer7%Cjr~&qzNEKT2azsx_#4rn3M>{Qi`?(lo%KP{(df2TWnWs+rj#c;c z(FKfkN;e~(S@R&scZ^3xQf&skNZD7C^@_J1+8y1GUUXUxZNRJuLCm8L`E0bG=<9|v z-M2o~$d2%B1K#QNcD}9U%9^Jpbky6qHglLbj$8>pmDB;x3f(TeV{?SFv>mZ)w{5M3 z>ma7m7F>g?{9FmKu66QlYl?|EQQ77Fz`$NTPkN?VFZ^cy-3De_j5fyXgxR_MMkOYL z9{QJh)2}Dt+)QspJ#ln)#;7Lfvy}=k@4rZmL0rRvc0#RtQ0vQjw;E$N;Mq6e%9!r6 zRWs9W>;~5&=dPs&{h=RT%_YXiL){>8nCiZ?}3ZdD*8Y#P@2He0!ffm2DF3RF^4>2)?Z zR<*RbKn<&o>uhkWaninmI9RP*v%xtbj9TC{x)QpLczTODI2GSO1x9PklfdR_bm_V- z1BJgGv&TV;xq=aUoYK-TRljJz`g~o?;tp+g6nh+a&cO4wffa8;Z;fBX>~X9k_Be{= z&fUF;*B@MyyKqk28#HM75ggNaF82q-vN2Jy=Ac>q3LZIOj4%mpNrA2~<_6kv{MGlD z-m5m~*X4HpZ8PWx`mMq@@=3NK*&JroYn^ekGQx4{Q4J(D%#!P4lfEvhb!CD3wQ(70 zahn?7?7@2=yZ7m@2){Sbsi$&P-O++)Rn&J}iSdwgadJ1(U97GYsOvtZuEQc_76^&L3Z*VVsY?U( zVBV9%pa)%FQ!VC(&n^ob;Fk$m7_ZBl6+R|0%}UH(6KYDtgiu@baotOW%n=VYv`C05 zu>&fU_$_;bf56DsrBGK2`rz*kzEp6Kpmf1V9_fDk|A&Z0h0mbQY~War*0a;a%D#dxwFCOo_fBp}VY!ya-L#q6!7c+O*dX&fyeYzyHmAez|^p4^x z12d161&ZQrhTA|>iud#4W_o>u?pVAFI*XfrH$c>4ERi$X(G!c!RGocCj90v}m`te? z`$sL`D1JHj3Mm2=kmBfUv}8t&@2@^zFSd*tn#~Z)&x%puEGco_^FiZ{dD_Kr{XsSQ zp*}W(?b{S=?+um~AV${%rX2#}*gT49g7*>2icgSRBT^DY98aftb^k4ncR`PK|6k&` z9dS&q;TW{16q1U$=rIP#dV59}FRmwhgOe~S7iOU{!VHC;p{03}ts-4wuV!Ua_24dc zJgj#}s?MJ)*^N1pUGdbpE~*Xa_C+MejhdP6ztGIG!f%N&p8ov!jx3DR_FV!?O7wpp zhYjOH>Sut?_OG}y`;8KdEoSRoT{yj(ioVBSji!6lz=~%dA>u~me?^u2i^f>iRdxM{2zH}Bfjg!gTYzE|8osucXe$}5JDL58G=oOTMw_fl8HElaA zRw*9518|LXa_Pl==7VKnh9}~7!i%?QzX`s52kSwOcVyhe@(%@r=eSS&Vv&3niVfq7T$)yoL|kFscvGc_Z}X81yM{G-61I#WY+E z?U$%?1)I|v{xPT$5ZX=lhr7|wE)L}C4!ug$sW;nCw%sS_{F_S9=Xb5MT(%H8M7+6z zHT+#r4Wr}H;+Re8#(2%>h5DMfSz`^K3*wU{eP%O!8($N>go+vk;}kWp0xA*~Z5B}@ zht-RgkvP!saxqo}U;$&dG)uR`Tbl`vIIKwV+ln5*wh8FT%2@Pgq2>aOk00X0ctxO% zr7nzbSyO;Ez!+I?6U>bFzL_9p0Tx$Z#p3FMdwOA-=p3om6ThvGo*v~b8%J1e(dxM4ACUGMHEC{;foysar=@-nrjP{CeZ*YbY-!^p< zQ?7t-j=X0bd9MIhZ1|p4sWraa@9>gbB?1QK6xliAjd;aJQCcDY!^qo8(-eA$`K{3& zY8s>PJu$^NUwa+r;~5g>dM2WiT=lL8nypXS0wYR=qtzI}v087qPAViz2JN*NbJyYk zWQF%DaR5eg70hjHScZ8D<__vu()ImM!+HGAWrceC;bznbOyjx{fT)R=tI3qTL4RXe zV8>{TeRY+LlSmKoCbS4N+< zw!@R90^G4`U=%uT0^Nuf%gu$a!wj@(B&z<=l&6I#;OV&D8XgS&2Idc9JR%_nyp;XU z;O5BjRp|K-SR!+3nk9Z4dj8K{g?+l-i_@;vT2xSL%%hWJH$`qj`%D#&&$cz#>y_AF zhg0?j_h`v98c;#Y?I{?MA*C%-pcz-aK^vPQCjGM|G_zPfd3dc8u#NFnK^DIC`7OR; z`SaC_OmjV0lX{34wl!OGl$c;<%rAJlk7<`{PWSQS=3@?@1sI)liF|N1tgsyKRp$t& z`?!N7OrzRnjHUiiOLMguBi=7p4UvAi+8SOLYOY==;IFkculbksP4{3%dUZfWFw37! z$O=0z6w80n^GbZq4n1%y#ZkChC<2^=u4qLsG>qrpDJL5c>r3S31$otsTRXAircRlR z+nV29g>jw_XUAke_K>HGOtU@jRX@oUvzW1e5+M_iF z$qKjGjAST#Odug+voURinYM9>Y}3d8OvlJqB#HVu z-*IdB$AFn`3iv=jEDQflalB-w0*2><#X1gpXG5-#fplbgk!c=Q=0u3Zgd*$kj%K zUaOp49khG<4eRqg)Z8N34p9rr#TIG)vACU}-JjAlyd#mW9@*0Lo}>@p+`Xfk*6|S& zQP(lQSNWM2a0Wi)B4NpUv<5ZLs7HyyAZ{c2x}(+b&5veIjb&8fZM5N7WL$!-4&$`N zzYa7JR@IdY2iwM(Y=o<$Ic(i@|Kl}WJ=@4H-@5z+m20-sF|THYPh1#NuW8UXqK9yf zaYr?BJ2#|@V5H)F$!nZ1C2V8F^~`Lk-3t06)yDR-tnjR06M76pI2HQw;mo<-8CSp5 zwYLMxH@#bpm;tN|z9lUf9Gwdn4Dt=IlH7(7`9X>u!kJtrC1}@3)s3^IrxhFPtgz_< zT5=;4n3aW(MgG!aPM)`%6Pb&fY6~h?3iuZz~ZJ8^?@L_)ehkq+gsuS;O&)9|NhM zFn2T1a{v6OiLxw^8;-kgIrwvLQL8exF`fJTntGgV!e z%o8%C39A~B2kMlVx;)H(ItBA6J&Nfao+e`ufqxm&vkPiogm>XAV zI~C(Id=iN#%peJKjYtRB#!)5S39p5=F5`*l9WF-P$iyOl&j&f7HSSW{rb>)Qgk~dy zeT>~LQtlq|ZR=G7Uis^N-ibH==P@QWH~(0(UHGa`wR$sp+bpEnCZQAfY`2T6J1vj% zVToT?;+T0E*xdmNiNr`TwGi ztrxHBovS0&vCbJRHje$gG6u0S^n_y52;1i zMRDb%Y8Z`Sv9+2GX(Kq7Xhvk{TTc9uy_#k>32a?#C9&IuGNrg{zsuDPy#b1v-*~TZw>$;-?3VM(;k)TQR@|gnglk|e zuV6S?{GeKio2SI=g?9}f0>3<7!;JKStw;EkI40Gjz36boB!j}2&|8#%~x&NPh!22qa9)symff!6Nh_Xf? z()l|u9`K_*FLBC|`%8jss-ma=Mg;|35{Jh_tjJg$D$2<95nw_*x?N=-FA;y8< z;410R)KiPOJFebaZJEzC0yZ;`RX^^8?{v+JW2)c17>l`%adNETb0WsUc^h{Q=engW zBN5z-v@gM1eZC&h+PB#MPfuLEs)5b)R)gO4HPZk!C9;h)`t%{LV8{ngKxWW};~j9n zpRczWF*D7v>O^Oe486}E^1a!sRU(N4HeMUW#%SD@MN#JO;z)DbpKC6bR~M2g-^lWu z{UX(pjL}F6`GxP*6wBp$Z*^Q2=3q$+{CN#%l~bC;PgogNLra%4mM=>%lM1fPHtLgn z23bosV1z4nO!pGFKhYSM1tl^Lh@Z0f-T+|*)U=9acVQf?K~@wN%WGH9T}8wVqaLd8 z-V=o{2(ha*SFaYut}y3|ev=@3w%Lp2!j!T=J1Ooqv;K)ZlXIHI@BH-d-7jHfrBkd?763<<~i0d z%eIFW`iTf$nyK8x~%7f3BpTKoer~9yaM0(s}90{eS{%K zc-lN1nBobwALM4IhfY{hLucj_(axks%WmEk%3l4qQ0S1AawTwmfcvwJ4cfxeHv-!3^im%)NcP`x_bN8Gl?}_P?mxIRRxO5L! zikY_ZClBtp^tBJ;#Nmp*un@*W_Uf^9ZZhumHAOf*LI&ReQ@Nh5!ptiBKucTFH+KBd zYRrOR)5rNq=7P2+xa&%}-9;{)^QuG*dC^`5a|h-eFHdR4IDy$fb9$X;{wkOm6B{Q@ zm&X%~97m==eL7;s?25KRI^{0e;%>Gd1??3fItaQ;u5GvRtyT2Edy*-u)BJQHQ! z-6Y17rH(nP);Q{IUIVr4Z(9T}oOv>m?aydeH_1 z<}3kweo}!PW6gE8xnLDsvif${i#>?t7wu4L&l35G1xUgF+7rv7vZ9cOKB$J+BobkC z7|+o&i5X%iA%((wDLFu;TEXtm{qm<_sK4;}<+nL-3|=ryl9`-k4m!w3w!*x%uMO~A zc$b`-Gr?(wRR*1U?M{{xxb@dC0;};nYC|g%q2z8k)qS!p*Xfi~4K>DG%=@vNOv92c zxbpx~Pzq)%#2zpLLbBODu47I#iA(^D9Ttj=%N$GOM+;xDAL)Lt8d@oTrx*J*+DF;* zQiRjt?tEF;7Edjst5B33xN9BDNU|V>om?z0Kg`3Jqh7hF?Xd8Ma5SP$@N~qBdg3ww z9`u{-p`KiUm`KJL9MuIhW07pO?Fa0S!%@wqvrW#O_OU&8$z~Iob`LyL4Ke+kxId{E zMhxHlbJqfaWR$vKywbnN@k+tgw%E@p?O`9W%};#!o?MI&mVuPGRDYKejc&OfYs7uC zav5{^*!1uXlRVa6>RKRRoeyIKJCoWGj!OoZobb!3P{S0b4a(Bl*o*b{FVBO#1I{Sf zfl>W{7D=7mcIgmEtHRIA0wo%ZEtn-d7|;^~#vIq@gUsUM^-2`>)p{lWO%B3k@_z}n zyO%HNGwdK!N%*&3SjhY?xjNR#LV!F0Pr276FJp^i9q)TkP!&8&MCZZY&Eh& zJl~)lnMm~%K zdeE2>vpd^(VfO0nuDJ8%j@hfW*ld^XA)D>w(gU??SK;~~Gpr72{cBfIb7q(iVO_+6 zwb{b%+6p<{z|Teep{3K80-5^M<;UM7rpJ3RzAWbJF8n-8!11mZFxyO`lUQKxNg3C3 zbd~!^VU4MJDU8)p!36xTr>_CU*;Cf8$_#%jnW~p7rCI-SVRj8M@otPf?{>utC)+S{ zm~FwrBW{-k>XBR4!g`%nW4UGt%b1`H<~}M?>Uu`P+(m_-yPbbuM^TI0rS6IKtx+ip z6k>Y32Hz|Pyx9&Dm|GX=b6|9GqQ*KLDU{x?iX2rtbA-%rRUed6qVM0e{w0vvC~3Zu zE$x#BH%93YS#?NMso81sfy%94Bd=tHS(L$tVGh&H_S8zv&uc|H%ZU1TwB})^uMqB! z-hK;L*dIEOQ5pT6=WJ-d{r=Dq^NNtarU>SlkZ+|6>o;~Bk-`%Z!b4)b8fz@@XvI4? zMtp{4X3U5(_h?#;)pYL5K#c&0*&%ZiUv2I8A+%+IlVq+(8)4#+o97Ei(-vWF)L_!I z-YuJQ78$p3WaqXkansTEzG!s$FBn)m1V}2F_GMdSH ztNUm*%D{4fW1IaMfb2Ph%>R>znWptIS$xNnHM6Rxy*n5FqfIGhxB1Dl(D-0@9AUCR z8b6A&1UsZ2I4Epg1#OtrK&ODu0pv9FlHl9a;;U-LS?W-WRciEdIwI1^OqW{RuU7l1 zSfn~4ev$z5pZJ|R9(`w&87(s`T=+^(_qX#=k4yLp^rJgu3n_BIJf~=4_CfYii}+o_ z$+k{IEDIVwCqtSDya9Er6p#k4;RbQP0G`|jACLwH92GJ&)*!%dq)3we)Me+JIBNHy zC5RTE&-D<$N|0;ba6yX;+GWyYyR_f?hN#jXkLVNVnPZ-({6{@mf?9k=hZ!V@xQP<+ z8S-lZAzy~krulqjT~v#3XyV09D&@DPQFs;aaPi^>i~^CaXrY(^({u>0RPa&5uPOJS zFMP%{dyx;}G1=yN?7j&Bo-7vT`4FaT(ELI7GpU^Ip>msA0G_!mI_zhwfG@bY?$8e_ zFkkB5LXWwtLO*P90C$vx`qu-OYzrziZgd^<%=M)BW+~DOC0Wx&b$>Rc>VM5vBY8lOHe<4BLt6oFi!^B#sXegiWtaHRZl4j8pgP7GPi0WXkv6K_US0} z$Bw_|NItYxi2qquG}}MSik2Ccl{Gbm4+9^}icv2!Ge)3{%cQKK!A=IUA~ zGyFynPdhn*?8Mz!)6bqY1=Ye<;cfAc2}~DK&KQ4L;5B7EwoCvC!r*$94l&F!=OOz* zH)dC|ggd1knQMPf$@h}5o;1wAn7gV;)W&}@38RgYG+24v_F$ltwn#+7EF=oIwGk& zMB>(Ao`dy4>QQ{MA{-TcWNPv`q})saeHa!@+684uCYCk}*O#n?QngTOe}rTDd}#4< zyaT-kZs>kU$P*ZG8;9lTY_#Z2ml)3!30i=%6v}Q45)bC05EV<_CTMRfGat*Su?)$4 z?jMxd3}rw|R*$xsEBxTwpjg@zcF+c=;;}%*&3bIV3hUxy?bj=H5vYr~TGy7Jw=YaQ zryqrYi$U=0KPa^^1_3t;ff*pcF=H%<>+3O1uVR}0Z!taZO4^&(aDB&BTvO2Kt}pv@ zT=U9)%=LQ(-3gz~D?b&ByK-%xd`q)Rc(-J$P%o@?jvtA}R-!~xQr4>U>Zz%gW?^2^ z*nSP><#YHnt;+{HeNZ3-O=9QGgr^1)$qoStG|-YVPs8{0PMk7{~ZeiJg>(*Rk8d99~v?K zl35~$-o8kWSx5HnGsE{Odt~SXH@bJP@cY3~-3CuJYr3Y&uAT{oTIeyV`I;{ftn~gSrj#$xB1bSpb ziK+u_nWOghUU#xo($|k8qI_~?-!AvS8`h>r; zsIV#0pZ}TkAG{FT&mPUshjz1D`WWK>fw?Z87$@9Qg86E90qw`5PYpk6RXO(hXkxk^ zp|%<0Ybsf4575-xKs!AeV47>2kyh^%ro-=c_&rpu5-_LTo$%WoSX)zl1gNXxe+M~? zqHHCm7K|M=?POcCP%BX-Q=v{I^eC*?cE+-Fp${mDSxZnO`T$6ZbtHNKpf%cU*nX1< zYF`JfQ4`v>B*2P2J;nO7sN&bG&oaZ0Uci%YOZb?G8W^=u(9kyuI6@=?$49T^)`;^H zEBg1e+UP8U;||xk4)qZ2=Q4F-Eyr|`G?JR^$Bfw|^$~#+YgOEw2co~TG9YQHLScHv z=MJ^d%dQ-$F3TX*%|V~=M}#jr7RhPlbozF?J)mOb?$f=bZ3p20)c4HKV!GX4QZK|t zV5s`~Xpqpe+}yqNd@_CUP?1S;=mq6DdRUW0Bn83Z&>LM)7PU zA;Vl*>+uJ3<_OH24qp%^a5{omqJ^-El;0KeRv75@WVF2;CY3NE=3TxtQ2 zPyU4=G&8juGp+6o8jOsf!}xwyItQ)pqCT2=v~5lC-k?s2FNai!G&o9qitX2I#0=(W z{Ht$G@hH!rkKg_lnw8zXH+Vb7W5;}OP%Fq~%mWA#18uuGj0t9p_-^vktQ+A?2U^rB zdE7v*8Buc28!3me!|->3HAl&)${0~n&O(cABR~XDwHKtqlfA*|6=@O+(in4lxfDw* z=8L_-samK1EhWQeb}4)m3(e#&04@)F)}c=j%z&P~!J9#2Q^AbL8E*#Jg?<_96<>f< z-+f1z!@iDtrNK+M>%^QvupgJQaOv*@ytQoPEPlY+ppfKsyi(^n=8BDi*N}s@5D4k z_Nw+EHR=LH|5C)jq`fx&4UG6(DO6#!UnL^yis;i;jN0f9eOFR7jPXkUl7?2=py^vh z-=AGKb&Nl#eu0QP*s{P5g6G^?aR>97s8?quzYMd`ayf(0xu6T*_L4|cCTDwu5LgG_ z1A4kH>>K}jg`8!2EkfDFa+aY&P9s3Q?pov0lw4=2oMxgHmz-ue0MEmn*i&10Zf#FV z+TjY20v$4`GYRiTiML1~;mBYu=oZI7S7mxg+7vRe+$oP8uoeo ztJvq$vcXFNI>OdH7}k-~a!H*6d$P$HSGO<16J6a)@E^0H^1HZvrVd~v6#g?rU#F@! zgpc5Q?6YI*6B~b7OX=XT>)OU^?i{r+UHgO$g351bs1?ROTNJiY!_J?TrWPoh zt&~+OW!at$pzRs<==_E~co*|#uuv22+8|KBX-2IAYq?O;b_(dCMKqMf`MO;Cjzgl3 zmKcawYGD0vO^OoBL7FbTz&SZlZ7LoZey9iNd5b-v5!U5Ge1lm=AK)?dwRUfR*ZQM_ zTf*aB?SV0MYM3x33A+}`rgDuakh05UAq_CO7(HWb-G^7a6{)?N21BMdd~p4P@a7g9 zx#G=J*NuuFPx7mtPdi9n9RF%hAM+jX z54SS9QOgw|_uT&vxTk0&3YX|(pMTzUV~46AhhpC4-@b`(1pWaW{EL~_;GlmG$NCr?FzSEoSPrdMC~z#e z3P*b{@IO5i;aM%n3MQRoKt1vL{G__EI};Ex6+lMl?8P#_fX=FLy(HQQz{I&^UZ35O zDjWdpjICcTG>K}>y9!kIf>uFwFcx0G5P4bDaUTmz{%U~g{0LMf(PsdvuU$y*r#tSF z(@BM#UT&M{fGb+~tb(2KucBuWN_&u8%-Dveh+fZ_5Z8%*SP!zeH%DjR1yG@{(Y*>> z4uDAoGHu@FC9jWvLnUl*^^L$5ufR6$EI_73s9qDb-2K8NfNLed_2md$4rSEFzu|M8 z?zj_Rxm!-pX*b#xs17SojoE{~)|DwYho6w*8<=d~tM86)%#)dk@w>(}(t`Nssgo9@ z;qTqDFQ-24Ixo1~=21<`Dv~xCXfLOmx$4Kr3FUz2vH+dLTWMQut4388FfX8Kw2c|k z53r(?cc+02F_&nYVJJ7}T%YBF38f)X2Bxgq^!c(xdxlw*l>tbOICR@IRH zit@gjh*R)h-Ueuq9nxQdd!WOn+0gF~tvsl8TsKYAsGuLyHRH+x_v_OpZ^AYPZg(UO z8QgaP$AjJkwd6SaJ7eC0=YIC5#NU$BmvI6}$~84G0>^^fz_};s)WU9lDyz)@B-QFS z@53GN%4o=%whuih%Le3!+@bzVj9jh?Q07Cz43QsS!E@oz<@sZUhlCX&{}z4@tjG?o z2>nnpFl?zE7`E=J3jJUyir&60Pwb3dBrG#_K1gfR_++ zdw&l3Q(v;7`TcA02RYDxRPa&5CvGI>;Pv})qq@(&X3YJzQTRswQLP`aBGECA?~C+Zb_${97im+@EhJvIYqg8*$eP%sAWlaoP~B0bG{#8 zO6U*Yg)sfc@IULR4P!h4#8nxf&pSbHD=Xz;m2kV~tI*4}Uxr?)DhY-638CH)QQgD~B>Pd+qD^qrT- zX|xf8idyvDfaeU7x>z>n?6!a07B3hDn!Ck5;3IyLab8ZD5Zi|VoWR;g2WvUpy%a9s znIo?CJLD7%TC(9zO)cSam%`Io`1EsZgIbchI{}A9{YGd3_W}-xlzp)^X=rRB!bHXK zMTq0DFNWO*I*NhUHjb_r3GQHR5<2BIH`0uw^75OZt~g!mU5=#IiQ^K7lBTQ0Z*%7x z=n#W(EhdAMv1fNRUm*#Z=eM`Uw?5)X0+<;w&5n^NzMb_L59>E`ZL7NVVTWPJFz||4 zlt{(YB3AT+M8Z=-w2i?S6D^Ts=Jt-KERN0(Bl#>DWcoX8PCqlmL<~bY;U+AXsqbzX zTpwnqV!i~Vw4_U%@ z2QU{Rg}xQbd-U$;7%hCP@F{gi6Fk@|=8>_*^2sRqWAIdvTH5>nY5gtH3_V)k+!D2* zHt^5+j=H$@@rH)QHzr0hP z(VRFmmc=a52U|-Vte9%QVaS|1d2stBoPj#!oN@-{uUA_q4rRkgWSi(vd<5sD-K{qa zO@lj@a?FHehBuOi(g|k0TOwC4Sm?laZWzMMiL0|a`}n&%`_xBb?~RAM@3h|SsC3}0 zi8&2#fRZn?{#vOAT8@kz%7Ak2!etJe%O(z`!RpcMK)yFvX8St(XbT_FLr>~g*I!7d zlR>9n;}}1bJe?Iy0%in8*i6;0x4NKA-RegiJk)}|g-KBFwpNbZOdOg5?QB_& zZPviuRA>wDY+T-`yk)kHh2L=B(It~|_R>VcP?bo=lB{`GvfpGz#tC<`iNo5vq;D)m zd;Gtr$vyvoCe!sd{1@JyH;0c)|I_*J{Jfuk20Iy2apvmYn9eH{^}l7{MZ&q)F_v{c zzo#s`?%{8%%N;j|Kj^cB+b^hubMi|t>+BP~lx^MCTH#0n`DV%}yS&C=2{&I@FT}16 zU9cECW!})*N?OYuFN+%HWzn1lW106>l;j|dfb?^hw4oPIo6R0m%STiZHz$Y7`dye8RB$~-AFS;Ague+7I$aRKVKWil-y`KS0EeDd&xx8OSY{0b93 zv4lO!Z*})LKfb?0`9*tHn0SBT$Qu>PJ+2E&FS-Cj<%jzOTl#sN#;2PNmO`?%?Y+&l7Qh z_B52z|Ncmnu@yu){gEg!6hxV9x8q2ZYd#WH`xA#4k}DKM)vMz&3!?mmP!7=ZTAB zMs)RS`selMM*8iw&Bv>Y2Y&j^0g=&;5lBXRKcg!f_~3CoyC!vqlzkPNos4>dX)#(a zDVaQLL08x!7(iFpVZY=BHkxDO@qY4IK0@hI zm!)l~;Or+%k>gMBAM}fyt%jRHb9hxG4DNB<9M*^EEbX)pebld=ruNb7_Wm!tT(;&6 zF|G0AuRnR&Y|>xvC8q!5rwzDsg!@MAp@*I}+2H@dmjH8U@7*3+*iIVfYhZ2fsFj*0 zuHM%^T>c$ske=U$p3+m{x1oQ|20D5GSU{)0ElWs?{;rgp&4r%Wmmd0fMO$dmzKqby zEg2!tM;W2j)t2Ft8>WR$Iod*Zy9b8(ysFTV+;@gK`#Zz>+SE|}LMHTVQQh$LZFR#p zoT?ihS85H}UTF(0-P9IZ{t49O+80{tO&wlZxi7T5eP3viEoXRnrE&P~BGd54D+Y$S zJkaWHVWUrb-t%qzf0%n0@TRJ4aeSYXlaR-uJuPi5ZN#K614M3k-ijSflk~Jeho*=W zuwq(DZ(1^1QTa0tS5rU+5H%ori!%NTB9@ny__$TMw)mJq1#b&FB88ctC?uznq_lOX z;hNuCNfDg6ckZ40|9;>1@AsXalXLcC?Y-CD>#^5f%W`iw*)EsdY{k%z4>FRO(PfSH zDME}KeS0Y`CoA-nU9K2b4`FONrm(C#BYvX)`)=lUlZs!ouxEj%UfFjcBh1{yoi6Jq zihqaS=PxibRto1XT&IKO=KK98* z?p86_zta)?Ze1+4a;G!+^@h6OKurud*M+fR;7|){p$-4!2ly2|xM#^<+p~OL9^Vr( zv%0KNK3RxSD5Rw_ls%Y2e|IHNQ-r_2n|X0+F*EgUA$;L_|K|*e#jd}u$s1qKX2vIU z{JO33UfFp0DUyrQ(|BLmxW~_7G(gnPk3)*EBDXtzpu#B{{dIrUjy=DGoIqPbyrxatO&Wq zu6~C9_aRaT*TVqw?DgWEbds)2iFSw#S|vhFa@mGX;PW&uS-L{z=2q$bRQc9_Ig<_ z@C3OZhHpw59oF&Ow&Rp@tI~vMddf@E5eB(r-_)nPcMB`5bASsWAkXSdKzWN#QDT<2 zoB{f~E=pK;vN^DZY)xiNmM~%O4a{RJ0w%UCD?>y|e5`YlI6<5dVA-^&QQQgLEK|8w-6>`E!=09#0F|#lRy7>l+E3mYzeGoUr1)ASi?ki zZ{SW@MPLg1Qr7EGdb;>wXTF#wP7iSL^1pfT7$szFl*vU+;=O^T@wDzt$jL7BeHPZK z3x<X!|x2N!mpxnY78&(T?jNxxldgSv3^6Fp-Bs!gm{ z`{VShWt3C#VUEXr>&FI)UONlpg*YYKj#x!rK)hmKYzuQr=GK?OOcFa$xgRm-dfe(9 zI{7Mkly-~Cy^xEAceNO&2l6uT`~YJkx-Ayrq;h+!bVRU6NvJFAHn&C<%mxB=O@^|V z4QQvS(`_I1(~cV3sc*l^DH#H!^Z8>eF@Ah)Kk(`HjdK!q`AN@oQ$FtDn}ninJZWAr z;Ld$X+MIi1n(UUkGcS0_#QVEPZIy=^v z-MtzLOlR<@W)X7Nh%o$i3v$m^Uk>2cY~?}_FGv*v(C9sQmK|@kBN@mGbWl|#_o_kchm63Aw^UZIq027^K|kZ@~CE*``xarG0{{} zv12Q=Qq!O3#60QN1MxZ(AdO&KCqP?e&$H5ai%iP1YMxPU9rZjfCCNsI4S3SxL0J7B zYwN@)scRiF+}}=B?o*02K*?80NiCzV(;g=OggI(I0i5~#C`&h-ZQ(PSoq~uy{BnA~nvFZV zb4A33$+%MAUJ=O8``$Yxk_LRTje7&QzTJ|HXRo`E$VHYp)0h}O@MpUg=D<%hC9VwK!Idib-*DWr`p!zFsp#mOc^WQ z9~wn!BJK&CZb6@E;$c`j;MeTh;CTaD$}a4iob3zF*yIcTW8>X@x9-2IZ^oRRQMHPR zYPRqmWmGNfigH^)5EmvY>DB9K|Ki z6T72HSzMH~Dxyh5g}myEny6eq>XK*qcz{jQVu1gnTW*H`;TUxz4_pALVlmnpjtx0h z#R3~*v7xTvm|^S6Sf6t^Ro43V7!UjU^yjhBV9+F-#Qq1{NqdNv=@uz;&B7?2%Yj z?HDV5b0+UX@%92wndN2=`op-XH>s^)Kl-^Scya3QxLaZ6Wx1(P_NQps0jrR7KvF0k zw#g+0_hFcDW>}Z@NOG>2afLul)nAl&^SM#&T?hvgyM)w0LaZS^Dm8hYPGUtA|h&KO@(n_gTT_m{!ldL`Bb z{7;k45&K#>6w|T5Z;cA~>@3LUSr9S@i=pRR)PVJq&Coad!b(!GB`M4?qqeLU5$jWY z>v#Vc+f)8yY#q$cn_JKi9b*2?KVkkI0}EaQ%m805I;fnFfgCUG{})R<*E*cQ0!>c#p<(hZ|CxuS5a z)%l0m9m|(RDmDObDsK%7WFTf}`Z73U8N6!?#q>f|Y!ArO6Ph4h_CRbr$kng7z6?%I zUl`k&IS`xZ9teI{-Vs9-KXxo zp!!)Hdqs?ku`zO;z2~6_F}uYl#6uC*!RRXnh>0;`9K@|6lV1?pcd=QVoE?8V{#569 zu^`lXQ7)F*Kq`Ew)dCW|!qBM`-wDf-+kkieMQ{8p@w4}c8fmtM$$2-zl6NDVpvt;h zLJE}NzQ~EB<%p=1W-E1A(gPbThEAo}Mp=S+ILMUE$J0kDOh;jo7 zAJLl&6k(951AQxxb|y)i3l4dUt$C?JG(%KKdRs0{H#5KZ<`fe@HTyz@kkKH$o%XXc zNR$o3HBBKE%Sk~Yv|@TBLvYLTL!SqC-a~+sc3(~=<7ozAX1o(&bzDyek+|xq?FtuHf)qW3XG*9$aA#1y|`p!AISR{04h_unB&zB%$CVj!@9v ziY2&$1GD~2wenRqEQdHr(idnJwB{Hkhu8ek7b4_`oYNILM3>L?@Q zqYO#j4%B0+9G=wMMVez8{X#L?8%%k|GN*g=pl3DTBJ3@Sq&gH)a(Pw5UMaUgh$;r| z52+M+ZTV^GVRiD+&Vn@6q_$2KV5ORq-D0PCq>|>DiIi(dYT=~ZiAI|0PNKx2Y0=Tt zc6sl??uoi4D+&J#pW#afo?+A(!;O5;D1`eFfhxLoT~M}|Y#!(*50!VlaN+*Y1oUYU zF6K$trGDmuY-0XtkSbddkIwq#CCtR6-mw&Bzr+z{A_Ki$5s;-==8x+XsPGTW6wCaD z@cxb=@wG1CjutT!#c`{cnM>x&!h$T)GxGxU_Yqsmx{{=`|3-BwUyJ`n4f^9$II>PW zNDV2R-t2+V)H6P!Un4@jTS{7v(@ouofV3-qIM4$?`JWn!sEDO$HMK3R@NO!nO2x$T*Ix1KIp zB6f5x5MQ-S5RHMprjE`j;`0`*ctFIHon<&^>F8v2$=k~h03NJ*(o%G=qf@3M`qL#PR3|`weLcq4)jw}3+=u&3)vq4y zl<6&0Z&KfAKFCSTm<3cXER7_q3u&sUIm9vXD7cUp^6Ly#FmVzQ&iZfv`mDd`>!}Xt+YX}m-RKwAQxlo<-RQw}kDm3L7I9u}9C3^yF@RKW8x@l8lMN>3&z&oaPO zY!FovNwLEknd&bVp9c!{riB1hn<)0>B+=1gx^B?2(AIRC|p-!G2Fv>UIy1FxHNDj!!-u38{o=@>n6Bz;kx-hdG{B2{<6%!s`C_F z*TKZW+P)b(!W1fWc zM3+c62huHc&81FlLC7b;SAy^7(mEbbjHraU)Wts@m#n+`+twB~{5E#)Q*S#-fmtZN z{4_I{KBS#XSMdvcnCq4BDa9+{4&@TMu-pWa@BHdwF-M#TLrpwCCi=&L!(m`{+qIa2&8cRA62g}?Qs^okbyo9H3sLu=yj*r*VPhYvFx>d=enYF9Dc(8om; z=SUv*l_+k)lzFF0;%6A4-!|{qi;yJ!bj~qpP2!t!6TVkfiiy*XuOj5?#0f|Et=<=h zHGV#l!MYg>*Eu$zCYgaGVh-KErV3htHDe*=x>sE9^ezXn?}A-_|`V^?Fw(tY_cWhiNLpK&4oJ4;@>#i7#|Z>vNukiK0eVx z^DrAmcy%;Wz*GH8Ag#INj)8xAwF=5KMKTF7Fk)F}v1M8$v-Qw$a|HMtMjci6lVN7i zJG>@}_RZ`t8A5IT{uL+supBYcN@Io5t( z(4oY%pro0ar*>8f=7Jd_) zQTC@kZh2Sm|0#ZDu;tPFV)af(?7Y$(q{UG0a>z@te9W&4e*H0|BV93jfg|>v63gm} zHHiuMe6m*tTOV}AyFUCV3yqt5!6uTBhJVI0B! zjmZ1MT?KLhb1wMwW?%5B1K$J-TfPa-+yFh%46WZ&AG_#;bZ*E~;|zYSst1_^dT2fL zJ_B`3#9pn7eQkjBX1JF{V&|9E2QRLxj|J)+!Her1!S7yL6&tR3F%FOCQOL6&domL1 zS_{vgVy`)4mo~un{f)7{-HxE`s55vB^1D9J1?@+2g73@9f>)X&v9CX<4Sr~F#V#FP z9{FX!7_{Wp$7*w3K_BOgjQ+#F$BI5N2m6`2*mrqLVtq$zgY#<62A}9V6WmmeyPGRw z-ONy|V9HQTmvbU`d*+GYG-Yw*mlMo@laCFoe>Fi%R=bu^k8ugCv`hRs=e(9X238(8 z{_WM(30EPK^AY}jl;kXdH<;?*z6HwUdL?{@8Fn(cCZLnLiowIulX3w50!sU zIuHLVOWdzrlQplGHy>|@)l8`n8CcVpFso4%G9FLst}vrQ2{cXIgg(5Ij{9Y6l)jsW zH-}7av|oNNoR&;NUG8@xq>D8or_w6%vN3V{0g#rt+af8t8Icq_YT%j46KuP(p0^zC zRElH7jU|`R>QKk+D2%YinIN;NZSM~f3x3zZcie9ym(RF~;Sb&GEXtk@-m-qe{QFSs z0cDnfvV7+S#=!N;4BRP<7Les5IXXN=^8o6R&?1ur7}-2=tXLcQWhr;-iQsO1ByMZb zVwrNulbw3WGf{0D)vcDS%m-QVJ%igO*oJ#%Mue_)7RA~8EbbP?35-b9DvtBvJ=d3y z5oTUM>yF(L11roZj@_~}VuRArngeRc{UKh%Ncp#EZ0zje{VfB7Lch;}hB3PLmFQHre5Pc z86FcN-Zk)D5DVZ7b88q{okn03v|WS5+-&KH=fpnN_O^vNGBYw(04~YY4!IX)ZG!J9 zqK>j-F)mW5NA3IT+?e-j9Gz>C%M*XV*LI=TA3Wi9)rK7H-cHcWF7%PlOG|l@tEG%I zzu~jX7Wut_&sLTMK9cL`7(3*(=i4$nUE|JIR|J-%F360;~uS_1Wr8d*cqzysimiWiZ+Jczn>MMzEtuzcvc85yoYU5&&!n4yy<#s25HDr|Wbd!fW^Yf5a?3!51b8+W z*Jgt}LX=VolOKAhk8Mv#Rz&$Ry|OdT#Xg3mVg8S4AF~Q;_23y?Ci!S*nFzg$oGXJ| zY{CvDFJ$}vsvQcJ_4XjSV=nYiML=JSTrAq5s3qT2+=qS2%S2_D6;ZbTsx8Xfjg>S{ zKrR;Td4<#a!^zCw;8=f@`96gdW_U{UL>ljgnd{*hLv1haWK2p$?6q93V4zG3p4+Sf z`pr#=ceWdf%{VYkGg5O<@`*v)A#K)oalIPuBu&M)PW$jd3z7%bWZqvnWic0;N zirUD%rQN}4vu(js&7t5!j;r?kj^OzQ;FoGjB2PAc+XJ=njuLHm0y-^78Rfvg?}qzk zIer7mshEl@^6ezb3}i=1KkRKoFi`P|f8m zJI!@W_ohY_1M;Y9kdeNdu~Oiq{1Qc!Utq>_ZBsGb>y|(-+ArgbWer?sFTFCEOeCR_ zb$5#u1EHp+5%(sni^18g9_xg0c>zXi;%G0C&dtD*R|>bJr9`!rz(-aui#$CW%1;HX zT(*SH*M@@+nX71bUrprc0E~~r7CU_;%K^B$w>}tZ;DV=_-IqL{b_Q27q2Qw*g@TXN zgcDl)R!6X|p%J8}#$ae~N#yCqq}XX@Zt!%?eNa*?Xs>}ZJKKYg$}xR?@ciCT(B5SP zd7(J+^ghH>7nEN~%UqSTyt*FV&4rpF|G7$PE2^aS@>p>7xn6ocB^-RD3+jCt^4F-n zJk-KQRsHa$l@FfQBLt~w2m`b*hr~~W+y}FaiSoLNfh!N7UIrn7c7M_twClsc)xJ;Z zd1%e*j~0pu^E{v|SnphD1MW^>UjqE+5LSTky!hQrw{dg&~?NIPhA_Bb7AK#ML zVm%2i#=h~D#A}$xOad9_zF^273eGzRedN!JJgq(iJZO8+E@xwli`n2}JvIns<{|3D=z z(!vOC`mgd{pDzb6;81YB1L`YL#11Q*x9v$-=^T~ilaF}@C9!Lm_Cs|R>syf*#1 z(E_zEp_xFlay*F1U6gn>IhhHup{4@~OmEWurU6n^UuoDNm;*H#Xvf3Ee zTF|4sS%C5s7!_m6jiq$6an|LhQMSj%>X+X!`jPUS0ss{6H^440htT!V`!VYhu6hW5(sowI=1$a{J^W!I5#y~l+ z{`TXFo@dr3PLh}tzABIMf?*a=kqyU?D_-Q+EI15u%i&0dZV}8TA(}DwEpLo(h|7vkZTO|&kNqA@!DPoDx)V`VXWST-bN_y z969OpR2coLkBd%-*N**;=~)v>;#fx&%-7SvA)aT@H&7M*&~*r;$+g~I>ehcs-3;<* zz?0N4Qf_%J(5Wls1hBsEutpMP^C_OIVC}C*I8(DXJ(_w9QvbNc+hEOi+Wd@8Ic!%RkhvN-Rn=&7Q4RHq}8c1bmSSwAU` zNB-tfi=iE)6{k8ki;>2|gP{tvr3|63WA4x(x&N^r{`FuLrPT*4cl>bZV1nNhw3{uH zP8)4|;~J(add5X=8H*9!+wIUVcMKdl*r2sjrOQrv?F-P8XW-o{2XQ^awT$H5lUWg3 zZIy@ZjC^p@@|Ps^4(uz+h~^IOhG)1BJ^d|~MvPeYSP@HoY;gU-M0r>;mif=$PB_34 zTwgHqnKg^OZLOtq$h#kZJ*DL)O*jeblC~fuO9GzcQ$||LA%6lq#l#0~4~uo^r|=iR zcRU^j-UF$X2wVVg%X^){fsG>LM?ig6JA4`SO6dz*ZN_NQNi2s>=(_3@H247BbUO3)J1?FC%)Kaj36Af3~Y^NZS@YMx4(?#+l}Nzo5!4 zHXyQs?QqzTd`>h)#>6940p5oUDt5fOXS@@jKaA9q89%d^8!w!|Q}HMt{q){2sTY6C z3Y$yXI-A9t<6d*Rf-6wp*Te(}`)(s(-3x-a6r>KWurBy@8OS1vAJ8|GdW*#oY*!0t z+uGgHxG{@>ln+=iQ8sf6l(t1=ZvCOh=^Uk(oY<$CwlAsD4gILtN^kOEJXS_B6V%?| zn4tECs2OQ<{Os}jN{~l+TpaKG&@Y=>EovmIh8f@0U-CGt95DJ?SZ6nH zeh(xs#DJqbGq}Z$5zPvh13$qiJJFx)8SSpH5>}}5TH@Uo=HSW4bAX?v^c4SUm?H^2 z#zig2I}GZ65_+GI)~rHZF!DSEq=Qx^$wU5#8QfLN4CU!jzkr+*>I{Id0#2yoz%uCJ z1AQ=7I{-Tl-`YqUfX8s5+zk}72z3!NoLSDh8=q@G?pNArBkzGVnjh`KRWu3yzri2( z3k9!)%UKqFeaNS61wO?n^PoI?+@E?|`qtxqbr$DA8i%kUw*sqf(Q&_=PmJvb7oHOZ zs(N^or%6nHjy!vua_uDa(tl*Me+5{>O#6xwfti6()0Wv#veiiCn$5tyY_7&#PmDYX zEpoa0H?Qkxz&Qc4s~jK)y4Qwhj}N2n1+^$gYmlR1Cq|s8J%RB|2cEThbV7SVO5@O+ z;W-P)T5DMyo(yJu@V^MX$*w+(nvb}~qm_xDbUWHaU^6GUOkH|a`_lryY)2rW!Ydt{l!h%9sTW&}+KzAo`tiMK4Xj2mcU=e2KH1qEkJSJ;3M+^UcnJyZ3s4^g zw6hXD&8wpf2Xx8->5kRK<@*0Qhw=Zf^zKbI-T!Tf^kPlK-DuMNJ$Z7(TF3*b-RZc) zyQ~b-mjzEZ8@#ARLZ3U3qZ#!`(l5UR1nHl%TgL6@u{3$mz+@Y1)5r`MGZsH^e*(ErO^ZPOUX|D9fVdQ(%TFqsUxYis|Ge3r5jikpdAN(glp)2RLtJK<*kSKS71*IZm!WZ9eXsn#3_8GvknD5bEqJV#Wh4-oc8R zUNYgP&T-EBL#u2Bws8(wf7+cGP5g3C!p7@}8|KkGq|PMDreb6qq{uQ!r9e#xQ05(Y zMiu5a>Nfh=LFFUtpxc3QGuS~>6GANZ|C58c{rCO#js;ONpi3dr=S>)Sc#qT-w|_Ux zSWMBsgiEWD?7B)?U^Xi+&fn=>)o6C&>V(l#=FtKju2N|!8p&re3na=3f1-jg)>aPd zghq0hP{T2vjv>s{-=9qM2l52ii$MFjfa9n}f4r*?-{ZUNL~Y5~ETj!tS||~{N-A&g`jAY6D+`exM@eNrt}gf37AGW?nSY<8vIth{mkwg7=oubs zvy45FrExM-w6Fro2Dzwu0I?o90J)TB1B{PT!0%7~_FHVrL*>YsVKh?UjEYbeQ966* zhv#I#^&Hr7GV8^0e3YyCm7h&|CR;6uLeE~AW8k$3X)w`;Zft2eG0Ea3=pRn>lCfTO^%=Y9$^~ggWGCyK~nsJa?5)Cg!zJ!=^TvSCw?;H-9{W~8O#=xN#~JIU=`58 zsBV@}D&>AAmF9-r68e_;xm0@9lkzpGG&eNA?`z=owmx}HE`1jy(rragh`0m$XWEwa zC7tiPDwXDGKa)OV4oXMZTU{W%8J`tVVo87e*UO9h``E`b$zW@3#Q<6Po5qbGyPm2> z$>d9a67z+h%>R-KU3(R)ZcO# z_~9XteO%vVp+zFE9^jQ}XbGtFq7Gi^K#f@$pW;Km z;COEPkp_$lY@s@gHlZzaF_Tf=Zw7gyX&uHVE~TFzV+~eVn=c;wl4^a`el{2Ne5KUD zS5qGKeaqjA5c_)()_e${G4c)8_aYp5FQP#G_%XEn%Sag$%}YT&Ullc!Gg5AG6*U&6 zf_$;nYlqZI9gZslGXY>_l&2@v;{3c<$SLdGDHz z29keDY>yBDJv6mP*tj&)9^nL>&Fv8d&UP(&l`4v)2799#ISJ5u}rw<9?%H#ndL7aF=(b?^jtBftM$}7pG|k)a08`TbLgp z$X%;&T~-acT|!h97NY#neuDOI87JEoEfs7ju%;IpTO5mOTP0T5DY5X)43e>_Im%CxnuPY=0rJHG7kWM~h4M~P zr5xzGfO>yi8$W>GGHDIDlji7P#W|K1O|9SRt+isTG^7i2tat_-Ic;rJuebYU?<1Ds z*VJeMVA%>Q%Ir(6=u1rtxCk_*-0qiOmnts>%Y)Uw{K1h_1u1s_r~*u3_p3h0fD~_e z>mh|3Fu0)jIe1Qum$VunUuDF1P}0y z^0(ufe7$*#7yWFdMkkS7Ui|%bL@OkGQ<hO(4X@9N5FaJpTh@iKBq~MIyMG%7k z1|Dg04{gosfmOeU?w1Hv5+knR^gp~*}-^h2kWQL;NFR2b_u(|(0u~J&YDb696-Z++r*B;gkfhzU+xwtZ zxRITLyb64o^{xvzl{qbzKlCPi02V?k4XD8>FQ&Toj$fUiB>e)a*g;md~U4*p@*&qmRDV zufGm)QX9WtV&omm+vA$|vj>p_d(v{p4__X{U5Upn z|8RP=t>;_dpS0LA+!Ohd7NH*O*c}6BkgIf!1FBKwpSlI!cik~?0=4d@$Z}kHFsi-M zRuNiVOu}{*8QkRBDXEG9@*(v96@Lo99WR^Qa(&5`PAvConQh-9jY;^*n~&Uf4e&xW zOrbr>gw-jm)bjwzVW1aUr6UX(58TjXWd{qJ@i+&W-4u0%f(VF0{SsBD0ie(6|OeG>9wOjyRQBi$Rh*V2gOs=&7f3) zwoHt)k>Pv{NaTf6nAIpK+sh8u8f7 zNNbs#P~WAibCSt&#NY5BvAp9{Wt)}u`KuaE(XO0RluWbJ@#HzL+_?98Dl0Octo_CNARX!CIb_`@pT6d1Im z0=#-3e#eH6@2{d%yHH#q_V~#%W@aVjin=IS)M`K;`g*`3j8*G z=7b)RRRl!E2UKUlH~9Ml>R5`;PdO96NS#uuE9j=m=`yKa)&+MgD^ZSnawSz3G2ud{ z3*<}!IMCS>`{B1BPxSGOlN6Brnv&Sit*_m40b7vSTj(@lT-_QFkBPh znq3RL@f`RK*95p`&jG#|W69+4%KySEUrwn166q`l9`?gEEa|41V~hLC*^ilKyI`vr za8K#-r{<*T*_xMp!T?#oN}J|g!1M&hu(E^EhHq1oQAo^m_C93Klr#7PvrQ7PbjBjCoJgUTDoc4 zEMjzYJI0}19P6p1y0|s#qF$2AJrC>5gEEPmi;_ukzg^)OdxC1W*AsRJ<6*)MLOjPQ z?|E|Un~??WZ$=i9HzSKp3vF*ks&$$0e+jgUNOH{wB^hVYI*UAyGW02t1#XRb9%PGA zWTOTC3(b`@j}R81CTt5r@cAKGi~9Cg2i>+QIQClhf$Z&VIQKiWUjGS$_SdU^aw znyETTbz_w_qfoC11%}9@~2O(^50OPyANcoYRfPa7Kdg1n=IVvn){eS5@jgsgQqB=RrFYO z#n1!u-k>%pZyS#g$R(org656=+)o-7!suKH!Y|b#5%(^2xfX(cVsk>7-*=Uvhw`pj zxMHrd{o0n<$%xFPw$6HSawNmk5q2k|w@$aRqYU`?&8uuT4Q~vaA{ZO@0IzfE4k1?$ z^&98;98T6y(3AN6Azm)HfIs-q=-8y%^wuV&?fXsetH@+aeZpWf4;?QSY_CV`rmjJ^ znRqu?@MNwm%7o%=hZ5bI0w3LgO((ReNH{VwhC>Yv?r);5k~uYNQ)~y?nW_^gPUw_xjMD z;Lee~q}Qd)3Xo2#aW5$iWIce8<0Rj-ia;F~_vIGl1~`e>QldKZMlTEW8{rmu_J)PO zwR;qn`96jeW_W`3AH#hc^ZSmP)p0Q}J-fCHJy2?L($`~8kaOT)1*iK}zVK(Xh zklC^YXtP?pX?RT}HSN8yEP47@AQ@;Ug=B!s-Q_#T-58hph22s!z*sE1(^hTU9abuE zpUGk4ZhIx{RMsIj9XpBpO+{m!vanKC>d(?T{|h@(q!tfptNhc-pDNLG&jblk6=g10 z#-)QfAaygQR?FqyGc*k#Z`Yy*Bfq)IuM;pvI*jY)!x8>g)I}k5t%IZqWyn*o7Rsa# zWTmu)M=Pu|RO|1cnGWLZpc$>GKiEN!t5JWjknNypLI+JXcThnG9BdKpUzgIueRukX zs(*}8xXx5#OynJhm12Q6)tfxLDU4c$hd>rvh4$dp=KiPikR!wx?XQB=Ba5vtNg39cENz{+qTPBFC_F$|#N=_(H)ARSf0|hl_-gV!Ak%de zk%eRk`I8XQl%^?eO$BG;CX&=YRd0GYhV#byIsI}W1-!k)9J zd)&Pl_s0_Y$D?q0Fiy8Bp(AngO0V9Fa{wju6fer>Fw*6jBF+JjJ!j*oa*#b|{?X4= zBHiEQT|_xE8R_&Dgt-c&!Yv7U0y~q7K7Ay4En7)Cg0G+?4`(qjz=kBmMz9WsRH!~h}fY6skSA7s;|8@0O zHshHMMvv#I{t&y@h;gZRhYMA(ZrEYQNUwyO`1?c2>bQsMa_oOxYtsM&x_PupBJr^R z8H_I$)P<-4N%+FvJmL$xWyBZujuBtjyGMLs9~kk4-9F+A`;8G_*d1^;!~HPaZ`Yzt z0{Z(wU6~r{?1DF+^V3nYUM{?_sKT787`;Llq1~c@KG;Y6XM=pKP5A2_NAN5e;_iCD(-308Bu$zj_xyyTjEuk}5xigfr#l#=_%>MfDrG9NHu8pq9y}um_e&_5C z7H&e{thn>m7^HjagBM|qe5Cm6p1R=6WoTDF?UF~6cFB{i$pi^f*kHM1K=&Hp5VK!e z12e+H;qLsVEIZ}KGBYtU@>4L66u`BmOlG74Yk!aOLY04<5VsQ5>rZvF72p z#-cp?6)iIfSg{xAIX%bAY`-KJtuA9_PVE_-Zm8%Vfr zw9Z(K-o51kUNN4R+uj^>-+|GVfv*W;{DeHpX3Nf0jw2J)VxWiUK8Ci-e&$<o7Oq>@54i>mtR@(5>>9c1=JY{m@+21q(7ABWc z6R#9s3bV2={cJ`lHCD@_%IH)la3G3OYA9Do-^@rJUOQY7s8=aY#3SE`Q+=#drukR2 z&RU6Qvi~p?Td52vlKv2U%*9Ag@P7#2yLrUoWdFlrhUkhsxsUtB?BKu=z+zu*@XE)v z!5{sh;OcXZV8Bmws%znQ-G~(4Kp(8gq96j zo!7M}DG{B`ge`fn!wKzpP#KW%*d`)9uE6%-dX4SDw$zDti}l!+qW@`ItdUFml7}?nXygn~S z%r6X}ACSfISuiE=x8|QdWC_{WlX6*Z8x4j81bN@IxenhS4n1>BAmwv;%t#Tn-lW- z*>1IH2xd`?`&i`0lTgHs_Nqt1oNO`G>tqwn z%MG2fT=TFjS`paK5#yP&OQ^n!GqNX0J}JvEQ(mfS#aVZ1lv(cQ=ujveELc%$BfqstA0i$cQTL`k`ln?JA^<@zLJv z>l^2T@<}{?CiLx9pX6BY|D?{OpXx-LvcFxgC0_5ukW4mGw=Vmqxiq5SL-S zxBtA}by)A+Q19~Tw$a@MSu)%yyV};<(68(_vN2KFU(~5gv{kanIiunP$t4APAIZfr zEx2&4-6eltyKR5!c9NH~(G%l!z5R=kR!{w*=d%ldLDgSS4{0BKEh(I-aD-dewI*dRYchoI=HA{ZwbAsPV3gpJBxPN|TE~YOHhoKj}k-7w-=}W@`lrM~BwUt|c}@{Z&{n z4Z>3thv$C-OJf`sXL|yc4ekUi>*KIIi2DgY!P4+gx=??x6=Ycs;BdJ_o9i#&(1mF5 zE$Pz{IF|ejjwb*P>2JV+BUD+k@BS~SYouQ`JN~j?42Q46vKL@E5WLneOKfd_1&cSB z7^9}c|C@dZK))RA+$5rG>dwMFA3=vPU-a?%{ZPMaq%ZpFfn!uv(dufzWA3WQPL!pm z_}H*gUNFpGdDScepE6JRMSdP|KC;kbvb(H`&|?bBvD|te#*e9Hr7^&hXINfPTdi9e z+@&Ux%uLc!qLcT12(+n^P82qIEmk3#;s(yS3}s`C=vdy3QAp?0iA2m|CK-A$(vzW6 zm5O@t!cM~KLK#7f|ELdTT#E0nv;VQ3~47{TJI$lxn-|&f8&NW`~ zi<~lRf>)e9bo2p?@nXK5Ov2A(~;9EV+c9F#yvx<31M?KU=ntSB4$( zQ`BWP9`B|`UpJlB)kWVB&=##ZYp*0@?+-CDS>S89%VZ!krRc^xiShGSM}f}=4EP@Vr**K^PEz!tKYo9T z+6CNC|A_7;^Rk|S+^H$JW1S1>?U4S*zU$M!2kGZS`XWf*_E+iWL;CN2mVOVUUnt=I z{YUh`U!`9N=|B5f`lr7Ch_(~t#jdy8L7IP+8^cJ0`p_eRZ7W#vb^m|FPB&cR*`H{a zJp*&IBr_?>FR&O9b*2YLGAG|Do;h=GePMw zc)z5|cAT}GCzBloZ+Trsh*K#%qkO9x(IQb5maFU_aX;rSYEVm?NUxt9(b<&G_xjld zo(N-FWZjtMiLg3(luh?UWI|d*MHEqHK`#~T@+hh2B$Yfp!d3H8`2Z)0iZ{G2YaUvu zl=u1-=jU5hzw7lYopss0w9pRuY)MhskTObaXQ{!hkZeH5azE?!^BP{ z<)iG-aw~rA^{1AUSd*k&YeqDuNb$m3-WAqf>TrA@mB_g$bC+o4r5wuvZ@D!+s@IQt z-U}mv`W~w_HL4TJ`t=O%+M6>seE#c(6sor0+Srh#51fO!bfm!ae;9ig@TRJ)e|Ybc zlk_AFZCVPpC6tr2)U;G4Ef-NcW0Ia8+EQsjger`aB06cQP?gsg9bYHq_Ht8;pijZe zzo6qFqK0~TiZYXm)E5ynL2wFznK7ti7aWC@%y1rZvufctl!X0F?S; z(w3>ObngBJL^ z>)z3x;W0ny0KL2Br#QOn5eu+~-HzNn7&sXs8`xcsI-q3)#mmpkfcGgYFaMvJs^^bJ zi3XHE@WXfd9C3&<^b@x3m@ki`4tIf?P0F;1?k8`#JwcjIDcc`!3k@IY=N z^Idg9cU2`lFq4&vp`N);R(b$xR;T1LGf~{EdRdcD57!NhtISLk&R0oG%h(rm(o(h2 zS>yVp^Z71KLVJz2XsKDqb#8UN_Pt+LqG%b5J7hBJsZjRXHmWBB$5Uly@$dcWpX7WC z<~+!5Po4<+)cv80v9d3}XoNhMeH9{~&3AkiVxC{@a3u3wrh2yG@-t=dG>v(VJWIFh zwrO39MX0kkG|zADvc@%s7|6#7xgeO*o}0*cj`C~Ab3QX`j^ z&_q9$^={;-Fh{o&IUW-=roDV2!-lK7SlhtyQfS}m^Ky2N8tPChE!CWtv)Vrn(WcF= z3CfuwEFFzxo`bdZv@16;e$D#|AByr{XdZ zw5NI|krt!nXxUOFeLItp9$-|`=^1qccd0>}T71brit1yH8x8X+D6~y~X*I{oM#J|A zLr;x<0aM0Zc1(MW>-jEPJ_UGEUB++J4dlZ}k~wYkP9cnX)9`#QCw)7Ok?v!@_1DWK zhUdFdyB?{ag+H}PX@fA}WJ}}^Yh0Bv@4S_f`}Lb0spo5A752@J%yX5oo_U)c+1{?< zt94_2^MmmFE2GyD3pN6kuWR`6Uva+K!8tK^>1Kz~TN4u?*6n^r6VhR<$A2~5?|yHk zsMuElKaKB?;FnC{>BPiLM=rKGCiOjEIq}Q3rrl8;lUL03(R>w1 zAhsb|ab$?7avjuPUNq59o0k=(omm-i=~IG9zCQ6e#n#!F#vd-YZTL)=(;^0Fmg|T6 z?eMII=N5gqe`VwW{l)&a=45EI{@Z@)I+m9SIx%x{Wn_i^&;1S;{%&@hIp@#I0RPip z+M027MQ7RQT^VW8pXmQ#&vmUwEl#N-g-Jc1X$Sd?q{yb0x&aSsE*>sH!S*IF+hd3lg{ z_b+8gC%EjTKg`{jkCLjgP&6CSyKQNg-Mf`e|-BfwW_8`03)m z$qm`bE%|9~rpI#?N{e{6$eH;F8=fxb8nhsnR89~8=8Lyx=e|<4>&h<#cA(a=(|AVDI!V13iP<|JAf_|F4>TA!ei zHqe%Qt;v2ozWE);`G5~bxI@G4-@fW4g`YHzcaf>AH?4(Ct@O=r-tI^_f4J%J0bR?` zK}47@Ja}o z;{u<%_dc7LjB!1#nSJPqacjkmqF=VO`$KHnF9m-{KfX`a+j?c&_+I&_v*Uup&@0#X z?7tu6fzIC04QZ$49?~nUm(?qi`+H?}#=w2q2Tse!p>I_B8bn;PUU;ER#JgwN zUcZQzMo1*@Dl7KM*{Ew9HagV)rWM#at1BP%b6N(nTGNx28-0TTJsLBcnFU2e&Sk>vrM&hq*B+Sm#4Rq=T2IB33` zE_Wt0%UH8YTB~VV@jihs6{@5PhAXNYI1JyZ8_3E;PW1j0*L_bH{8YhMgg0ItJo^3f zGLe{jLngi&Ey>*aMi$}1~U z@*%Q}FKV<8?Z|K~yIZh0_pUeyt;QZ7qx($GvVA5SptfY8SB`*N2hYpe*UNUBJ)W-9 z`+r}}T7F;inDF~Kj|;y6PC0LmE7PURA0K?R`e0O_{>q%kgjZ^RBfK(K=XyfOfpD{n z1-aCqdJ^>8QWw&2US0l+b6LxawY{MknObQR1+B@7rBw9mx&dmh8_+sHH;+JR(Yk@Z zmhJm>T_g=^q>XakY;)7EargY`>db_8Xfe6)z+=LbLT@M&dcF2q>i87{{F+<~#Pd)x zy|T~Hhj(3z#-FD}Q~b3<&+utx)Q~7o@>PYvK^p2|z48$&^ag#e-0kcQ6{^Mtp$=_K zK2j214>7u}G+!%KE7Wxn4Nx9|x-7Kz%AI^kkSWUO!x%7L6edGGM9(Yqq2h8(9aGK< zs*36r9@s*fuY3907f(@IFWzIC~;^T7H$+Ni14Qc0#zSSKeXRQQ0S# z*i_P7)oHmN`j;ENu+<6uOYHV86+u_zUp>%v;6Rr+^uW@271gm_nTWUty3NoYDZ`#p z$`ti^850Snn0n;}<0<^@F^o#{>Y}_p+t8n^b7BwOsfF>&ENTjXM=OQA>GTHRx7)^m zPlrw6HdNe_CH97HybpQBa(S&EV`Eq{c1lrZANq6roY;H~#3r<$E~3|~AkQ8s@8a65 zLdY+h5u7tLZ6>rBJ*J)v$u7j0lxM-Gy9cOj@GZI1`EdXH_cEj$++1+8!0q`ZL*4@S zD!5(XHh|j-?s0G>;EKV`{N<0a6PPU@C-1Xm}4YcYATH7DW=o}dv&&4u3uA$x3%%s!wjOWHNjkZASI?!5p+YJHbZZ9PU{4JMoN6cwQv=eB`T3YJ4OAJkyXwY9fWl zyTjKw)4V6*%EFx@FP4hW!uZ~ENi8grZF=Pve13L<3<1Ae?L+NOv@y&B#cM#pi}S=X zaWTB@y|hR!zi1Iq>$yLGI0Ke15f{}S`>qUeQPT#fij$tEX%UrPC+MWPba#}~tc;|n zS4Ps=9Pxp8WeOsi@&wU<6C+NIJmMsgaFBDv*XhFkmn**&Z}GhqrHnnMx7!6g%X@1) z(@F*nw^C`1svF8{EtK1hU%nM(R7~-$@v%-StyMvI^JrKOgsB*e1u-bb#E>=!(FdUn zAEx;=6_CS5PQYA)L~;plnBeoLGHX~QTYrLgYf)}MG1uLI;v=U*#M^ZbL)se{;uvye zP$%JAXQQ;jLMfG8vJdSgyue2+mmEX&8N|G_-Xu7NmW};ueT{Ro)Axs;;^QW9Lf;&= z{?{7uFEXcHA!?!)^=e_exK&K}O#3J}=foFifu#07 zB)y0XHLRuP&9=+|Z90j}UwcmGcv@;fDdgf;A?~9}fljBwLkjmh9EStvpvD-`ELJr! z#S$O~>-+%vSpt&1(2fH&B&bPPcl7eKl%2;mn$hB>xXi?-aqg1DHAZIxMPwF&Nb zC?B1c8?@B&EtG9CjxIDuE+BL|`mb|b37qMt#mfR`st}FC9bVA$m8@@HE#Qhdeu)$i zm!~m(sK?32!<)51HSkGI)Tz|;^pAuc4)H7tUpfsO^vbIb5=*bV>L85Y2QT@OU;ba; zfhNvOm79W`vD?K*azhZ3gPhZ%vjJ*li_nT(Hty{`>MXYG~or)nb{^_^ZriRfMXlWwVu*ZFa;0UH#T3Vj+=7)_jGS zu*Hana&E)^BH}(lN-DPOlMuQ0;}FA?H7z|5 zkh^c}dWV?18q1)4g4|g&(?^SJXFm@f1i5p*wkQY`&{B*1_MwaQbyr#+suw0jjtwTy z==XDV;&6c9$x5z9=cAp`aJm%mtSs;7*DSV9qI{ zbs|Je%*4l<>P2Ro9m?+80oH@OK{`1~`zk#KPY*RS#n?OV4SWT?14kSfpVYV-0(yoP zw9n}iq4cc;>!V(_5Uq=~R+CAQ`oYssW3(Jod>#4*1-WJc_8y`ZAa%ki*w(YodZ4}h z*@v}j%BDrBfe&G3(D_=Md{Bx6S3^OwVPFI!JQu7I)Yn=M0V$pGHu$QQ&XX;MbspWs zgD%LC2XnT%iD}32fMc^qf~QDH03a(v)JxTyf466U6WQ05}F7B^#cG!M3A4 z0i!4DLH}&;N8(o@!nZqe&qg3m*>~?Xrh+Vm`z;P~?NkBGLp*3mlOp>ru~H?$v{@_i z^HGSgovmyT-)-zhNyIWEKqij~V%l7k;UH%-&Q?;PL4^E4_)LE2!`aSi+zC|1tWm!E{I@h|U9{*_z#+~Nr^(D5dW%_R=fwS=^(XU%qa*8U zZHBSUJ)@~A=yBmw5c?lF(4=zV=)2iy6EN*{P&N-Y&wvyS0xhxzuDbABOxZ1H#T(PT zE$+WDIIHQa5W9f5s3j#3cQK`!n^W!B=)EIbb{%*U$R#X;o9y<(GZqnW9ub=&410GMnR?!`*6*sF>dfyBg+IfE-|= zHyxL=3}bNqyaD=BfQxaNx_l@t_1M;8f!>nvv-o9N?K&cN->^ttU~Kr2wD8X;UXvn+ zE}eyulopM|Hol218(eC=;Aw>3$>Ut1SPc#hY;*zLkm4SjIcBE=VV;XPxt%gh~Mc$96{cn-N7B58-Vk&h*l z6=FQQ;wOrC#Xr9l#y0I554Hl*j`Cg_ge7YpZ>78+c&~{T@^mVD!L)gU&$y7#XIy(KAFb6$zRxN3_cviULVXz#PT`J*C)_Y zJN%>I7;q|ZI*_kgtp6BUTK{7?Uccp#f*;)`&Xp}CDdx{X7Wj*7)Q_IsX#k0q&-wnI z1dP*jA#^>+$=O;eAm-n-K1FBCM~zQ`zY(b0M$f5fW}6x1-rtcHqmWTZM>ZJL4xnT3BAbTG$!?yt!&<#@xCXnMGX8fSnTa$3r~QC@qh3y0Kb=fvg{cBsb+pT78!ZFA ztM@p*4JbBRhFF*{RzEZsbdMjOo}|r-fJfp&{hnIjZ*lU(RC;sI&|T}b$}N$LhzqJ< zx+4acJ3POW;3<9UTscesN|b|gnyyzxIp$s{FOW5Q=E~V6q+m@RH)}3vE!mUMHq9$h z1JIS|X_O~(Ln=y@cY|I#B;5ID7%8Fb4`Ql_8b+|STG8GOl!1cEcCT#FBW`>WEzGT- zEr0joT)Ck9#^56P!XeB7$a&Hlf_7@bLEfH%ygk!whj(z0r>7uK&otv{V9?vNRB}+f z#l1-GF|Ko<%#Y`a-9zWroskQ)DCjUtY}s@#@9 zjOirq)(^KOGW`3Z=_=^wcKcOT%!79(>_@#C@r(^`mZXWAtEvbSJQhu-oE^`}qdtcx z?-v1YYaOf~ZR6}r3E9w|>m$DFliE@a)gl91-sC_ehH$(i`whMl9M9AkqQiH_XIv09gbPQEcn9>l1 z$#E5NFrQG6y%m&l1+o5%8ssJtUZtKVb0vt56%DTnE{%V&5AE2z26TwDjFw%Jh{A^Q zd9vQ<49B$kk#n>*e9k_1_~9Z4@VYeoXj9tAITP|R5}psdJlqKBHQD1|G_%q=iuP?jRcvzeSH9*?JlP120tws*LMi!zf2#Z8?xjm)Z1RrBM?RNjLbF>mrX8kLhaR~ zz0#B*(oRt@)(K+qJC!FBVso&TZpE6`)|QfY_T+V4T>~;=hHaAnkHhZUNrZ1(ByZK@ z-a*e$uJVla0p;%4U}UGdBOka)!Nnk==fOx;geUkB&3sMUUElJ`*akhanz2BCxH|*C z{^VwIuUv2ldZIB^5blzBkJ~%pT>+-}m61##LmT71s zKjiF9H$DRwp$*23UD_;mYVwS%1=D!AFrq@7uGIPRta5CLDrv5IzFbniGLp`yq%G`y z;{EZO+3am-FXg82Iit&i6{3+wJ_FM%kP!>mRzB3 zV2kP%X{#zvK*UzGo5D;hi!*oA+ekuvg(lR)YN?=7l-av4`|m=oS__j1a1z>0K&}vt zG#_+=Ex6)A?nk7b+whz{o~{2e;?KG35P#172=PZAaVhkbad-21|7#9jLG@4awoZk1 z8xXZ;t6npKnK{u#f1x0jM(~~%A`>U?Z;kFUrjZJn)k=PB7pS4WkF5tS%sts1XKifp z8Fio8v&#{a3wMjP!#8E+B*rhqdv;lgk_Qm80TBb9?;1rDhPf}6 z#6H+sj@SsByY5ktmJ#(BNAr1du_1{*I>pnvertunBY#7jm)pKg9L=~lGhJ? zb1Ss_C5vD-!Ip~s91+D&@4q>Ik3NapeH_&=J>scnxa<_7>z`_ul`;+ zYroBKeEH4s76r$$;_@?wo8!*%*>b1x=J*Qm>np!GzO+1f>jkkOv9%UxXUpfne;N4i z1^?CHAB6V@%9FQZ9yQ{fpPS>Z^5pH9Rd|aA-kZylw`3kRBEksX$9b_F@5>-wr#VrHxDy+-iy-ZqiZk*o>lt}^Pevk(#rs>6uC9eK{zYS2g2Y(K>=>Ki&4_4%h#O~Q ztF58}afpqIZ@2z;U1#JHXC?UMJF*nNz4~{+&!W&xc8pj2Ue&+Wbta_g6|bY~EI$+C zY?#(-U4}%y+1=GpF&Dyl+rBQuzE~u8)Gk$8z&L0jh_r}E9V?W(FiE`aLIJTq4VGJ= zJtIO?u;ffgHFGa~iGE&>NX?atz4=72Z+8y}%p$ldTX(z;9|t{{GyDJFuUWbYKfE?*Z9n?_m3Kl_IHZRV*dKs4*um z8^({tAh#P~JlO(s=nL#*D&Q^9=8GB$krLa> zTjJ}FvA3oQ@H>ze?lZL{Eer&8;XaCUCLdm{#k)N)kM1^yAJwi1fBSlExbJJ6YgdG? zgwVew{LSn4#h*FGN>rs+;yuxW^QtraaHu8xS0#-6Q&=m61(mQg2t&W-@UgvL!<^k5 zzPK0W;C=Xv?Ko8csgYIyXRjb`ddd9C-(LCZP3XZFCw_~qcGO1B^>>5r1&Q~c?*=vK zb^jkY%6XCR28p*1WSgMl2R-8V9<{inYM1z-YqfwlQ|jS03$}|Ow@+Ciq7K0;E3Tl~ z>_iMD)<>q?=|3$mC`t+9Up3TXa($}GpT|Q_-cO`zf~TSQNFGVv%U6=yG@)28vOzY`F+&GU3FO$4S&hN8c%uCog%*7Ba$7^8~0C& zQ~?#HsrcoS9i(7VWMN3r!gPp6)GJq2e-t9NF+rzmy?jEq2GgV#pr-lA$E1i2sPW6{ zAz{I3nJ?NS5ssB?lo#s;s+hcp`x1oauu>JNA$N;;kw-5<3M{5DM>GLnt1sY=e+@hp zhw#QbdktBv7v`8_GHG7~rN;f6bBKA7nS*K*fm8B+-44+F&~6!Nsy1XK82AR#J~;?s#bcrdZk^n% ztLSPHZBSmfUI5?R1965>OHrV?ij5tdjAu{~6+JCjubiXEi#P{4E7GyE_TEKjBJ~DG z=2^4}&zV1Mq5|e(?jUFR)<-M@{?S;%pw02O{!>^VmK6Vj>&ju)APKVk4*3L>(HD@n z20xY=uK6#~GL}btbFQN`1K;|hQ*v$2e6GHNGruYy)1m+P!FT0jN(t5HBz<3%omvdx z2Ft5C^E&ysQl40vWH4EdJ{>K}3e1FDzH7l;s$DzcJ{@PiOXdw2^Wr5=)(|=qdEVfN z1aRdQcOW#u;_Eo-yR=UB>k7dKOM10%L|&4U3^@)VCw2M`{<Xx>Vl3 z)c>vFy7#v((JowzX?y!!*_i{n`K)dI+0Y(s;IeLKNCEtg2luUwA${PU0A~T04Ph+& z9){n2;GP6G3*5KA_@OOt_q2(ds+e1~F>d^70j2mYFqYh@ErY+A?WnDicGQ-&!&4e3 zMOrWD_Md^aqK$Q%c;Ji=Yh#w&)ToUEsiMdvo|XBVZQy@o@QhrgO^xzuYEk)FeiPJX zT5u<5ICwK&dk(4TbF_F)fGgH167x+#luPWQgIFN`vr=WSE+#+3i?~O}3{bu94U}1# z{f**6@qzdq#oR6T#qX#&9e!o$sqh{LTI*RBj#U3SJahG*!_!^!!{1s2pbGhyRFjQhzs-jvu*PXRQiaj zo~43S8KZ`z4_(w$H;|u`o09FNQhrVnA9kzzu4+V%853m+A#(CurD^ z<9WbI_obh8=&!d}@A<<;c1!XUM{;)EI6%CAy^SFa;P!xHz+JiJ`a0VpszpycD<}Qx zmI#sD+VNtqY}|?Ga555^@@ZM0b9W`{JuMq@4mdLK+}lHCqP;>rob5!^d7{gUynG3% zdSye?-Idg*2@)Pm!~19&Qk&zTcpslG_%w^Zw3)@Fh?RvVPr`$H(FUYHYm5p&1)VN{>4 z_M4#;Qi3@8vq4Oe^-sEXnyNO(J|s|LZm!pvG~#-h%UNdX#k=V6w<-tZo_U;zyKQ-q ztPt+CsfXBkRuNZBe{ku#UO9VHudF{fdP1fJ$`D=iHCGO97C0W98C*6vAtpB_&jE~{ zC>$fAUzVtWx;uBlDi(=YdT!A%RNrb9@U7z#H4Cv|ahh!suzfAD=%kZ0YhK$|;^e_$F-&TIA&> zilZy)F6A~Xo;j$99ml;rO;roAWe?=@dfm$TT329O>7O?C?#`ct_0TyI6q=Q+gd!5 zmf$|)HH78bWqd&9>X9!NG-|Zc`BBKAErQWtVq`#KL~21T_NcesfSUiTrrC1me$P9! zxYo~D(3+UupUh!NCn7xLJI9e-%N+HTdfWv*K}s3Ft_ri zvqg8~EZcmM5oa|qmZ%?NIw7VSgWN-Z?D|=c5;1aR17-BE+>?OF(4NhK9jTYyfBoD#rT;Fx;5~( z&WIS|o%?~mM|PCfifK+-ZkP4?93ieCgB#B_1$8{8hp`-5M>=H zJxg!Wc(Fu#nh}kr)2L3Qj>p>3J%}0@^nP&)%D)ohA(r1>dF3tp&@A5CklZ`QHB4Op zxfMgN0Nq99>4F}5mJ^?3csiv#9S6E227EdPeuovm zqu@8oim4d%*jMmLugBA17#hU21-*mNS+nJcKDi#gw}iyj@pI(@y>9q$Jt7(DhJRg; zIL6v&5BVHgzIJ#_>+H}>`V{ogcB?x)&yA;7_;`39%)$z?ultZONF_b_M6M4;mLVe7 zA_a>Ek;-=*udmpYJ6n177?ZaTUqt&dMno?pk?QryvYpPKk-L{6^70-@!{I3}EN{eq z|E8q9a<8{tB8%J3K)d>V^l?V96_rn|H_DIK;#mUs zL#C@FLQFPCiu|~~+ch!rZ8*vE{ee;YxzPTf__IuGY!D@-WrDl%?z3D%zX#e-)*lCl+R9l{SKiW0=#nO>)1mzS@I6{Zfwh^@s zwl6mfwlAJq!3Gz|`SmZ^rdH5tI~}rLu{@}<4?QyZsF&v7dDkC(Owpmh;VLePp&+%9 zhw|!StVF&yD|{obZ}cb2`1D#1%6R))!gv1-{hIv(QZ?6t+;5ekFkvL>!ccGtr*)&H)y3-LTG!ZUM?mL z2T<FwBBQg>kLT$O$ezj`jb27^F`3v@^K_asq@bv#KT`_)Jez4 zoaKw;ca3;vsy&{^x5pBTtBYBAOpSw4?XZtAeWZ+6)B@$pj!{|?;h_x~ zYMs=ocW!ky3Xm$_?uZ2rx{`J$*S%QP^l8)26-j~<_*uv0S)^V(3O(Y_Z!^;L5gD zqMc~8wuQXUg|`FZ2GD5QM9eEwJX`K~(cUxz_f$gY-k!-3(p;j!j$jVCC!*(aN8h*V^MWM*!4R|KPa(#HvPa7W#sl({TZNMDlf-Nv zYH=3VTR_4sFKlz+9sGUs{zLe<5^vkLIe&c?=h(B*p6)&Xyi;-L*GoUaQ|t*QT4P`& z|6`fZJKK$IqN->NT1PUHqDo(zNQ5tt&rDq?i8`!NqD^E8hPp{#ui#ehL49lQ( zq?U0(s9X9W^oDuz6la=(_D73!0mKP2ipEG@_;rbB^OGDG(5bHkEMh6NZcR8Ti}kP# zmB0P_mbWp5Jg*}6JxDAnKa=0qZzr$z)BOMH$8y5>*jv!vhaH*sts$9V_WOxK|t#&!~dUM60`^MniT}L~NMC12U-g-Y;2pM-ME0RJv zF9)@9s=P55kZIast%%>Di~G^48;0Qt9F=5OHRhJd>~eZbzRWDgP#8H{Xc6Zn zmQ;UoH#ulT`TbsyP9{bQ29lVZefoS6@yUo(d2L>v7^xqGoVFw_4;;Bj3htD%%O6@X zw1Os7!(YjfESR;Y&(E#EIeYp(1sNLcr0x~*eEW@oKSs6b^dy;_5zLFEFpvZDTN0zb z#%;@XV=Djl$GI4-CsaX7RUWW(yNp9HBXg1BG!YH zMI<}$nzX@*d66%nZ5_3M(!`f?q`)D341Y)de&$1%wUv0d`va+$^UzW7>tBxSwmC*3 zjod9KM>^dOn6>{Bev}H~K2ue=&wg@bx9QYKCykAm%VHzE*%7~^_;3Doj`2RUd1=PF zD95n^Erc*uYn&;kw_4-u@@ax9LNyxcPL)Y!ZX5Fd>(cGKgmr^b&!!f&z!b4wlxv73 z#RvSe1ZFUlg+~7V7-BltCTs3g$-T0{cuJ12h_;GyFP)gI$H$?aPB*9pTJ)CygdgFG zAeDlje~G6sU?wOhQmqEdOC>p2#H?B8_=Oi*3n7w&Y;Y8Pi6w-k!`W2gvUr`2yA4-1 zp=|8QQdva>sHdpE&GEKxER4+?ZU&tlToO6z+W%|+=;#B_2>5Mnu2WDc_Fb=(d>C&Y9qXjGIAnc!8R%B&)hi>6YG*veKPK#q4>+|h)qq2lBb3I+8$Ju& zU>f=RwC*40_xkg8!5gpNm)RojuO`Dg8pxE#ghMVe{LJ0B@}%F!NQXf~IrT7NU*cLG z!*zh{(*ZINBiU*GLD06(c2=zxnBYrQ%q{TiY7n{niII&19dTk{N*Mo62ptek>aH5tUey8e$ZCPY*fAR41yVLk7)yF!YZX;?6|&bO%ngpqfivS1D%_Fon&P#B$xct`_Es%L0sdj}%%Rd@Gsgv^;E9scNPiJGOQ1O|_uZ9tKZ$U@gY)FPY z5&GAAITIs!7Y@evvs^yp&yZQe>?ciE3%45`Rf3tHgWdH*Y`JPrPnu z!yCcv0QWe!R&X|Ov%sAzWXQeX>L)X#7=8!gw-4M~;99`tPEO+0jrM^TJhMZ*39;dp zX%4zjtI=m9vEjHBwGXwZhHN?=Pv*ZVtsD2Ih!%7axw)kl&!UV8)D5uNhyiF9`@F2F zJE~8gEtj`soFk^!f_O7)-RSvLX13f=@|^g??XgQ=(_5(dFPFlpyF^CXM&M}zA5m$G zMYPv}Yo>F>=J=T6gHcv{axk*^b82RkW!S)X?!>?vAB3&NzNhy%#Wr$k<@h%Ux zr%r}=g+PDfpy$H^pqmVIec|bo`@%D36asBuxQy=$PqW?#-{=d^tQ;OO=QB|o=?mX{ z7tq;;NAe#5+WO&PzEzCwGdPE0Ci!vSu;=zvfsX=!5ebl09Xkbh2mAGU7 zZTxobXxOS79^qBK5vvYT(1ip4zSL1K{aM@dRoBp{(uf)Byj^BIbT!@pw6E@2tziD~ zvt_|JJESLg8*6rGBE(;IhyyuwdWmfY@lW*moVwdBh37J$O3AP)_hTRLc7vaVGe%nL44%!Cv#(AeIDY z%dYa%AZ4tl)nf67FsJf z@6+Nvm3`i4A*HGkprYH550fT{He8^rTO4wsftXlH4>NOli$UNjZol&Lw-?C^O6mq& zT+&KXlXhwR8Qn;@l`IOS(FfwsKs&hzS__WwIu?L4W7~{ z;!BqHLp>W$hxAH$-TwE*ZzI|=?zBRzQuI6OP1-4KZRzu-LC#uRYVbTe%jv0FJBHEd z(o`v_>ACr+O_jIc373@-GKZM4rP&l4JZ)8LlpQpmOx&DH!A1=>R33uduN9LmN?Tmu z!?sUB3hU8wl9sraTngZ9yhye}ZudC*XqboI9#tJh>_aL_am)uLMk{V!3t#Ur09oIUvl8w`2x$AMZt(T`5 zFh=JfYICntPV!+puLPNPN+*u}13s&mVR8VaG4k}FHo8ZRGuB@M`=XS^kYXjoJxI&{ zj+T9EPxjLiC$1VVAqDsiw3$pTd%U=3GBHvcLJNB^&d%s`g#F61r8;m+6*1vFgss79 zRD4U@96wh$fzmV9%O(%rN(l!roJ>aCXCl#@ROM`UiC!DzB9vN1v%<@*gLH=BrZc`A zSxqpOy~{oDwV_k`+E91ZEr+n&_wS1`1gR~!Ca1&&y>W1#f{MYUhtGZ0k68T;MzlBk z0p2nKibV>Z`4i>F$VtVLVu4=&f5A*OM)qEOynrDdo*`Y}rh>}?w|Wvo%JQz?!(+v6 z7q$FnphFxdZWa;SW1`kJbk}e4g1SjPU2C8%8pMjCqu$&3&butFwk~_@A-lZ-EfL^Z z^oGj4UXVj##bjc{bD;(?KW28jJdQda+R-#dULH*T?iwVfFNVH)h$2>6>@#Xen4pE* zE~ixNzU#R-;kV;n2-EFlB!j)G%Y9ix77lPsCx#h5@Y!<}DpJ!mHkY%ihT>z}LBAr> zowT|OrJ_DB%0C4~d%XpwjgH-Sac7HH_kc8S5NutyTlNC)G-z4P36|E*U$R!n$Ld}E zKSiG!Er}W5mjvW=4weQV%#CXZazdjV!_GqM1cyOx|FnyP^pRJRNR3)rp-hGtDZ`d5 z4a)`L`_f4DL2gLEk9pVy=@@3UK^mO+m+!d%jWocBgwKrA+vqJSiSTiM>+I2cz<9rk zh4v8~un$!cVlvg&U2$7^C*_aGRazcGj$OjPD+UpdWE7i7kLwQ)slopU#G6&PH>XG5 zZ?C)3YJm20iJD%Y2XzvYu}=8j5JS|_0y2tFWQ-jBfKEB&MGic86L|`&oCU#oY;q7S zBhQ+WGfXM)uFW+}$v%ncGSXZIzp)?EFGcI#x0T6w zszjsChHq%3=QKR{M*YcsOkB^#6O*`}tshXOsm1Z)Dlrwc<6^EHbikSgBY&;(g=aJ; zPld;toZ%}!I@?kSl>PoIl|BB`{r~^*6%m8*o&27tn{iZhN*@{U>~f}!46k-VE3gPk zJ=kJTKD5!XPC;aEo&B+vnjjWKud2QyrUR1iXQ}?fQ8#D;Z_@gTD&-_>hpi{WDYVYM z-oI|(Tn2_!Rba?y>wn3k+P+p?Ry9uC1fyhfpVrupyR@ki(cKm&HvQhSAn~7-txjWP z;Nsk&6|Jnaj&WAZcbt`%8JMY@y9F#OjTg;IYoG;ZWjE2kaaQi+cf~_!!^UkN+Q>Hu%(f!n z`q_)xD6hFzB4tHNPO>k)cmVU9^_FU9v4}aw{Xk=+d=TlXM5HW*clM$th@qvTgH*x$ zzy;%vaM34vM#u0^LVwO-NOSfNI|MMmY94JT-VvAiZNu zv_(q_6d7XC4ee=oxnY_K#;#syN(_ z+>(OK4lIeWFRgQIJE!ZI@VY}(x9U5fza>l%BW9XoZ*vx zT*=rtoq~SG@BK6-pZl@Qne^11)L9o{2nX_M4b1<>h)%+>*iCVMJm?}>osyeyPGg7e zU$%=6E|fu=@LlINf}1}+*-mhbOcU#_JpYgpW-_-pA#xFtha_vazl+^c>l&CarTK-igT9&$6!14t-UC_*Wz3ZK>PdUd> zFB9PARbc(zQ*l%-pzk^A1~TT=4P?#-T5>cr+f+ATBS&RDO*!+PB&Lt`Q=SwYl{s1+ z%si=)L`G%WOF-(8xr}W8w5m+=(NHBwhBJc$pYPFyZdf+J!o>-u;r=Rm1;km#> z@r#+zr{OCW%0|;jk*I+-G6C8MHEXW2QARplwt-l>=5y1;g%vM#8TFMFY=S(XktSE( zYkbOaIe_=a45dkqi1&`z|1)9$w_zMZ>cNTNY~XGMS2phZu_a}js26>*F@r?+>U?s6 zNiBAWPv=&PI>|vECKDph51xknpO%fKD$8UaW6AVUOPL?fTFyt=L^>UsO|?*hXfJxN zL{zwb^p#W^9Hr>{mlVe({(Fkg|0Tr{DLr@(LEM-Lkp+W5!2<?ojSeq61G|S@b zXP-#x3-NT_r_$E^ce|iuuTBCv18+jr4HVamPw@M=L>kOd2HV)gRHBpC=c^>GI!{nT ztKM3~O23;t`Es3t_~=g}oi@>bvelM6BlTv?GNwgbd07Q=`zFy>Z3OM?F=0%!KFu$a zPQ>*3AkH~Ih-<7`*S}V*0bjNP(m(Nei5p+k@(ov>kiB>VmxzOHRns-HcUlQJpNj-Fi~qVN_J*lw zuw6P9+h;+N$Lr=_VDp%W<9={^|=a4IWd}DL?*e2xl zzr(*e9tl6umI&8(){LC|C;{o!gir3S8aerORrqAvNcf4}BM`qieB?D(_{fIl@HzNB zw;?uC+!Y(K>{%KfYO9XlvG=!EmcBg=%KdARi>l~(xuo-aNV|oFdd7Q5^)lO{O6V@D zU8-BuMfN@rT;`M6;P}LplKfq4g1MYT5H(LzkXWzir2+>dso1=Pr54h-!X{FO z*aUZ_ZourwOH473Pgu-oM}|Mnr;(OsnlINeMKEU`ICnmj<|ugUJhZ^`GVe)w={&S2#8O6^1R7~0VWcVs&!&m- zFLa)7(Z?B2*Qq;jJ@mX>z@`33)BWeOe-k3sOT=3?mLUr>8B&%0!(2G3FH8!&6DPrU z;*6VhL7!iY=aNAODX`1zoJv>Sz&iG|uDnFnU7AGNWle&+hU05_KrQ79(2K=yuIUIr z>}lx5d#iu2On-yPC+p`?Y5gc3fbO(NqS;g~oGsTH8{--s^lJA!<$UPuP$r!%Pv826 zW41i2yxThwbbG-#MVu|$N{o@_kc*|3*>d@kUj>K>S8vdQ&6X~qW-%Fiwe9jL@p0kf z%g@Ysfn>TEsh5<+XViP*jLj2gogUCtaYcg5(n&ULdE7t_#+lB8aW;4`uJNF?>G!=bflf&a=%ft< zvCgrU85rZyDtZk(W2rsfkD*xOXm!`7{Qek){QA|636Wbb5^q-;Lkv*Q3)A8!LK z{x5Pwy+urc*>;%JSAtfagv76AM9UhPlznk5h6rap`8@tgos(Pi>G8u z+bJ1Q2d3+}C`&5D*@_MKCACl+FTUsK_D)r-vb0eiW-ju)F|zRjYw%Rksx^u(Jy2rnh8nwD<_%i3O&?{`eDYQ+-ki-5 zeDX$k;%u9{y5qXehbw%tp*=GzBT;C2`GR1}g>bhuGch^i*{pwvOAehZ&>;JJu((3g&Lv zWM!Z}{SUT3m?L0~VqI>*y6kzR;w021qB0kjzI>s|mMx5Z&;Z|dce#|Bv%080j!G>FSo~t?|qtNUQooah4p4YuafdEqW-xZoD4TjH z&KO>}u#C%0mAJHbBVi_AgHPIze9X;<;syiy zZ0}0O(nz1sTjLoD)f+%H4yn-ci%lolRWN56-hr0&4zwerM$X@fb3ORwua{#fZ|Zso zd~{Cq*}Z&o7uG{_MdNZTdoxg!00m9xqt7XA24Xcs?`|*dwD0GRu7!%H$lpt zu6N=F-c@m5vmVN3JhaT0yA~=o1*vH)@P#*@u|A<5jX%_>Mb8OXV~awmw9SF<*gqD* zDEQ_C=B9B0v(#WT$>Y zq~XH9vkx{QQWnaFIY3Nd2%XaH{b5z>zda<%rGc#fiBHb*&?uvO`2ug(Jg><7FW6(B z@Z!+xcILUG(xw9ScFhZ~b%_--^;rqt$+p`BRePtD#&&j9RUnSNiQEy3QN?aaJCV}V zzmrl)b?l8)@cGS#uDKOxRo(OeHEASu8kJT|whf(ZL_MHbW)~lqHcdwB4+Z*h36oF} z+dTPIVZWrJPj{_=??GKPexA*|Fq}J9oOE@)%z^ZknKcgNt~80HsyfO9S~yMsX{s<+ zq%Ra+uG19DniiEr((t6S`UO_nRHA8TJLqjh+GN=VI)`Q_GqFb|J!>wQ?LCn5lJ;-V= zUvR?A!?iz>6OhZv`H*fQC+MU!s+Kgm^Zzex?;hV&l`R18eR7hXJZRGgr7g9jFK+sf zo0i8^0nMw2KF}6K#K&mL%d`}z$oOL%uBLztAZiN8O-uDFic&zGP|@q_PK%=qDs@^! zL~5PjI6`tt&q--JCym#9Yo&lXb071^_x1NnlXLbtd#}CrT5GSh_g-uHxy9XnwLx$1 z_N$6g;i;K+WfKQi%pO)yQ`)2yG#u-u0;m4X{4zT=ZnSIqeqGLGXE}du*WaghYEpEb za-fY?|F0_WZ2qHm?JiB9!O3OEJN2@fV7Ud#clxz0tIKgc=hyb_ye|3#M%>iPT=W^V z6CZ_V%K0{a`FxC=y3szN56^01TYb?dHH5M2Jd>rB(dX!m1$-&5?K3*DoYQyR$7PR~ zFOHT^<*BD%PI57=m|>CfMrhG|AGV3@WkP$_@p=lg`IuI$rx@zd|D>McXt~03?fv3t z4e0$aUY->#hcPs2uGb&$yw>9ce|hvwrIqiOQ(T%p@1>s3*T1FZm8ixE6IQ&F7+}YUg;J*r#)sXrcQ0JErUJoVuiG#%N~aP!<7{ zeR7Fj=b3Pw%F?7aet+%M1?j9-F{A=MCGbguZ8eeJj2gOx7+V405p4aT2TZvd`4 zP)74N*p==V%h=r;>?-%oeJ_+H^Pho5yuqF(GkPwIIH}#hr%8jNk~gI>Zes^E7WOHc zxvVtw61ZCG6JG-UQSqdM7^n0r|E{!0oRy}8GHOijQ~h1p`(j?&1Rue8YFft4U|upU zyYD1sw=g8beUjM@Pn+P~c7R9w5AVMqHUeB2EsfpRE~`k?KMtN{-6{PUPA*H?Y`X8- zvpdBjpoNY@5*MxM$Fj!&ju@zADt{8jl4Pb{ZJUD{w@cW%=4$Jv`=;{O-dc{QPbuSr z84I3f_c5&qf4i(B?Q*Q%0PW=WAvU=zMe{YNP4Hfsc;G4|#F+jwd|_#ccw-tHg_7<~ z{8^8&G+)e2(;%MI#K-QN0r2X*#BdE>Juyacm7t$+{Y~0>4PKqd?%T{8)0il}XluKi z9PJ-sEXn(i7#kiubRA={e}}Q}2J_NX(N<5&DdgH%rCi5XnqKpFZ=Ax~D82e*hQ3`+ zCo$J)*UB=)4j(go=D;V59gohD(BEm?(uuk28s;UG+tdjanCIZv_DAhSp=|v^Iol8) z#yuN-urqE>boVBv+-!}hu&MDGe0G7HtuGE`82O^9YJt$Yddm3E1X3>oGXAyKoQpUK-X&YAcP$IcD)GQQj8 zsSY~pb?aE~-5Ge2nHCiFbo)s{Y>=g3ZE)ygStT)Qy>ER6<6(>XWt|VK9S+y?&7fi3 zF6Xr2Nwk*Iw4Q1?AK)nEZ^x4Wr~Xp<*Yl+^`_X!Sk<8vmZ~0Uv20HC!UiymUe2J^_ z+-@t|cdM`8Pd4qg&OX0ZQmBZqS)&p*t6mH;>R3SqGeTo3X_^336PdG)@AosYx*(-y zITPB&a}>L+Q=rvs?>#b`{$%Osd5rOQiXZ5KIv4HbPaz!jua{mxIBGNb1po(THpp_k z5~Pfd-@^}n)q9b>K9c`{MD%*h4|zuZqSe zB^P-V|4t&*fQTbDB{sTUlJk-5#w-{Fu(8A?>v>5Yc11!3yq3;PM zJwTCRS_(Z+Fu!OO&lAC_tamNSpv5#li@)*}$@=ah?)Ns#`}?)h!`^vv&zwfE?zicx zq?p(*WP^8uT^nQ+*L+H21zXgrpC_M5$5UABP3YS-Pd=x6f`2y}6Ks=<7Nqd!I;nvx zSlEYW2JVu*^YP4b;*`ZH$E>$>;#uq>`;%51o&bKROI(1SO%IU(RsB(X0Am5&SA^$* zE21$D>Qs^8%+JMxo6d+Yzwm|ll5ekgz|4!omNHQ~{%3KaMI%ZyB5pX|DLzVX7o{4z z*wA=3@>0#Y$N@{eIJDIvu6ubjGIiZ(BxmVp#JF%Yl5e#`y``bsDn9A;T4}G*N2a15 zNE>Kan2Q~A;8B6+6auf>ovN~f1lss!h?u8`n8_1TYtobwjEHb2nF=~NjsC8oHKpyH zHM~(~CW9`dxris^=iuo|^cjS@3sW3-$dk#Jp!dW(7Rr+`ayf|=D#;gqwjS~M5tN`G z;Zu0Fjr)+V3RK3T=fXVst5#M2>p&mqHLW{MvTyOl24k+Dhl`$t(|pdq@4I^59P=wT zXahv?!k~8_&!_Mc_!lykLtnX)rJ*X#km#Fcj|>5phBE&wK4M-derq&GhAfWg^?LKd z$hXGo$TwxU!|o8JOdj6+x_4h`PS4RnX&=%9V(5sV&{Dys1pZ(-!KMJ(F2L3N86mcw z8QVStdNjiTl}{Pl8hTk+O0oe{Re_}>7rrm`{&(yFJ@)l3=(%Yd`oYch z;qU6G9~|^bi{}Q2K@rd@K(5B7^D|SpIm#fTToJlG+P@W{XDS$hDF3g1mWKY64}CRa z++z~d6THSgpN0OWVt=&=iwgSrMu;)L5n`*AV=jpm9E6$<2o9xLpCu=fwvK!`v6@cb zSL*M*r8Zof9@b%Y48=V8(wzIq6CLKEXR?Xz#Zs=bYWTpzOKiWiZSoRfVyQZ)QydJg zCoosMktWR$8SlKPhh~FrlUE%~QlJ;Gga-d9jp?BsZ{~ z*RC5=$Fr0l9cv8DlUvgd1u4OJn7;_E$J3YTX!nq423x9mXT)_Y?nt<9)(92a$s^SU z%9nLotnlt|O$HgGqZmcAC@EE{!{{qv4y+}8cw*M#FmvT4;SUOZIML7oc5Jw|YB+fv zTDw)lV>Jn3Mam(81&;43j1Ovodg}}0zgjD)nFV-i1fyu$G!{JD!|!$kwoSq{0gDZNN!tWGdnxHoVo!_=Ky9^l zjBN^OM_k3~q?B+%JoK9(W6UtgK${9I5Pkf*ZV{+SB?Jg5F4P3q628_y?bof zyQHjzVb$mX;e@KltPdv}#+-LUi_)C;{bV!t@Q|RS|LKem#>S;%bk6^yPw_s47Hj`c zr#TJz9ev#O(9;P&>1k%*Lc@Et-Vs+-bTrJ^^LIn(VE6m#+XPBBbxiNTT`)zM8A2aD zXiN_oN>THjsJ#^p$B+%HVIX-Sl_m3tGN_1Q*0wQK|UuJt8NI(8Yb!B6!4 zb9J^}hQ2zC8>lg%_X~ea!n?8oXvA0lf^eL2=ns;91n!WIMyslyK&$GgAG5EX`m96x z1}rmfaJ-H3ni#pS5P#G_Z=!jgPRawM`tNJ;EZ7C8^HO@M^o;@ z;@-qSAx6*b8v3_J81v{!Q;c?p@#HRqZcRBF%5cz*-ILvsqPnWUR%7F!dvZ%hh0PJ! z%1NS08K3k*Mq+e@?2-}JSxMt?!9CO4P zgs07sqnT#$D9nS!G!)sI-6ehNLJ#1$Aun$lbXOh>dSfv@-P@5i#mbNv07?1?YF}za z`=I+tj2isu0K4j7uq$?@IJo^_aGMq+tLFQNqyGEI()oiCpE+7S;(82y`bj_)I~-Yh z3ca8PBVApwVfD6Gge~l*LAS6}bmw&qu-b#c9x@zp=>8a4@=e>Ie)22A6Eu1s-dP&i zYCITp=?=hsoOjJKyYq!XeI@jE=hNvA@c=FPm9ss_DrPu)qIGqVKSu6*fxRg^RPkG* z=xcY1UE5SaPBqV;Rf8AG@Id!5*zJ|aHyQdoSXO(QJP{BdSpn_UtQ1ee$UW-#W90r9>ctOEn#HmG7)ux9+8V^~YQ5s4H6d{^ zjLlayX7MW+n@8v$#mDO%;&-U$!1t(XG-6<^q1$4fh(?&jsDafO!2$5m3%zP6WqR*K zdoPv~)J&TY%K@3(wxY9Aq?btpthR3bUYgX{60q6k_HSh1mJb@W%P@ zdljA=N`TR+3M`vgJm}V9KGDrns{((>Rtff;eH|}%#Em^2Qiau_+3@tlFc~2o$>D@M zbs!HFQK>@}N?|IG(Lj^J(_jY4GXs~jDg>Xt!j9{c`7pB|?;Y0zB1}U z&%YyrhOow}!2SfF;mqwF0BIp=jUZdo_X`$kxe^zR01bSO*NpW_oQ^pSoWFCIZ>$#k zi@#$9W+D|T6|ZDafk|!!E&%;W!OWgHR3z;^Hoi zxM6>x=-8ewHXQPbiy9A!3v&;Pi&sn)twx}sMn-(pf-#PB#C7Fa;?nK-%-iA;0~6V3 zVZ@D@o#LX*EOE`$EU|j(n-M094c+!HM|;sm8^?6lPLPAAtUa+p1x3B=sSM;NB2*B- zxthY=o$00!oBlS~N9ZdEp8~)}JtI)i$2MmH1!MwEBr-x}7Oq`$KQ!SAb{O=z$*M3N zL(k8|F!v(-4l0C93l*kW62gV?c#fC~1r|oQ5RVyWbE6q&@A8-Ek8j3NwYYFBDg1Zp z^#8}6a;l^n{Ga{XX8ji>!=PjTclmepPww?zJK5pl%}&m71OJ0_;*oPQM+$kpBerRQ zoJ`Jv4l%Lm#n7$WFy8Y*xbu4_uJo{moRI6^QLY0zcTH$Yl}0b+Iq5WAQ>Ihf)HM`x z^+ZxMq+>Ji%LLE3Jg$uxjB(BVa*6xNbV)4ERfAQ-7CmW&--bDzAf@T{(q&=?%4t&& z?pY(R!#KW9JH;8aOM2R60*NQQtRK`DfXp?6B(A$AiN|HCqmX^&yGR%2W!C+Q2l+ey zJCMCQfOG3amwGs226=4W$%3`Ko$Mm701Y1=bQdHXW-{mVIpU?0IU&oz-$s@&n2lh$ zI8Zj#=@sw$a5>O2e0O#G6P9O%9{3NpI606Vs``(-(EJBI;^Z9yNXK`@`wh2?Jr}aX z@3=MMSN<$<{`S41#^DvKl}p8d@sPL}DEEGleANW0_qUPzmwLs=fPxp-;JJ{gVs+j$ zu?r}8VPi;K;){)(0c-5cg@{;v;kV-Bx;0|gcgSaJ#Gda)Bh$XIg>JiepqD+=P?`z7 zVR|i5IR|zu6WbiO4`dy|DDmi_fnJ;r(ZwAF3k>BNErI_JV2q2Zz?K4-aAr&*Lr1Plt{?BA5$RI1ooTVU>ju5aXLGHLtr-8hA1!VszkU;OYrA!E# z)o8)6Q*}4ggl@mc-rOsbu5xF$Uw^zd^uXWT;;ot$p{l>-g!p+L@zxzj#OmBk{#~)! z!HDZK&xq4}Sz<%pRFE%z@v*-ihLLiJYcAmk#EPR7d(QH^UzeRL(-HG@a-0Xxi4m}a@dPiu2~TmsCk`;3XoNXr zOD^C&M-kTiaG(QEJtzgU0W4+H6Vcqmu~A&-*!1B76YhWMV79=y0`2Cry+1g$o_3ib zz;Pa2>7CujP0RkQAw8~H9b`bdm=#P>i*-&X<|u8nCSUg!HklQCP1IW$?AiQ{#LzgUsBEqmJ>Gew0c- zTYLFm*0VNynIP>fIY`xQua9Jv0 zD>bKO@?eE=_>OsC6F;7jHC4KKfVz#*=yR#kjDgH@v)w#Ibe7SU#9!NwkG7<%okbzN z8~v}nA%?UGYQpGT9ZXm^M8XQAO5jPgoy&q}9M_(4O{VY!?nuHDN!U)R#0}79GuW?8 zOF7YpQ{1#DKAcpeP6Hc9e=X-LbD5HX0ZInp^9p>>J3_~^<>RqgQ>E_&gQEvDxrVyjwV1G8GG%-#pG?L>f2A<1Km6i`Am2uhV-4XRqav6AQ6l416U8={( ztp>gr?o|&w8&c@89N#O0D^_oadhO!5Al&yzm2P^6bPj=S^c8&GhYwT1Wn;{;RH^Wt zodUf+Mo)T`yJLCQtv+FIHs&?*B7Jmn{{KDmaKSxz9 zjaCJJvQx_|B&r0-!o}=s7YMPA5y{G{-0e_*CU1kQ@fjh;ZHm4K?9-s9#R^ zf)z>?Zm^_Ba2;m}?mCW@b$${g1nkk#h79c(8Af|FGbu%S3v6snm_~Ob4Pb?`Q5*ZY z4z#jsZR575ipB@yid zACH@Gt}9r=YR42K1!L^6&i5(wazaOZSpPjS%=Mx7Z^4*uL8K-S zBLy%DTIjFR$24;$z^7CISJXzO#|h@OAjDtCVAkVD6Y|G`c^-5d-u5FRb#b8ZIz&j#nKKT z_Ue1PPb-8!PzrS^qVGeg{-~hUm35v965X-V38!(d13lE^;;{U=-YO}U`}M_Ufaj5m zSNQc8d;BC}dm42puFq+ni$CJ>^|8%AycndqPlHpmOu++vRii(wCsSd5U9SkRTx-S+ zys9wwGt^UPF6c0;l)@r4dZ6V=3Jn=_&rag20u`z|(L1P&o%^}?QvDh6fOBc&yQml1 zm8ch5FM6R>;yOgGJ+6llc(yo{>}F;i3o+)ErEHiTP7SMp_qD^hcCpV>sytsDN-;0D zjo$%I8)gb(1y*$;n5CK;`0lZUD3n)W)U;-(3S-fGNlSA7)i)md(o@>s_szwtzTny! z=tC#aT!5WJpE-ozC@Itkd%5!VaoBJ9?_ocA4fa(kfz=}Pr2zdz2Qw3)UlD~qIjjQc zV~1Y^=v$)Drx_O7vc_VEcH57G%|L6XXFL`6!CAHmtbv8BPO3|aWxcdRxqWE~> zY_LWjn9m4=gW1DPaus222(y?dHiHhMQtdrz1^Q#q>!YNTW;d0>F9v>PrI{ZHlqn|c z#*#89NwX2jWiyjlw>7YPopKjYGxKByTg>jlJdg>y(Z5SOg$ZzsVi&W!n#AuW?rz!O zit|nI#I`ir6FiB}#oP7v1b4iL&eYrE0pcgDM4H+-?#U$dX*@gl#C;@3-GY%4s^#oz zpl-~wt11rZyWR|OWIynh-u;&I%@FH*GsJ*SsPe*dj&9k})h#dac{}3VRe@ylB)9T( zKeWy93;11pj<4IV?ZA9))MsC=3+drz?B#MUsTZlsY;J3 zz^PO=TMyQPwe|b8@AUiQa#c5U`;(fALP=dGpd?RD>N>>F3URtr_pA`3n-x;+oE2i{ zkg!IA>wEiUN87}(hV0;zn{R`9s@;iUN>ZCs!dgPJB|bi^Bomtzg2Oav-z$QHV6I;h zX1-qn^K-fxYA1BY%cXzpz4u~*=QW_aep%=LPtm-X5E3c=g>Nt34~Ly3WdsYn}5=unY`4b3+fz zbBjq8*?Y5I%i1+H^uU7#aZ!2f-II5v9ZuWp7L)6;M7|vCya(^ch;VS7z6-dBS?HW5 zo+!UzSL*WyC@a1jOEPw;q9uh;GG&(rerH1&mgK{o*TqF}5IPQm{PN!{?FO9C1UrH?r9BW`wmep{jY$^^T8+qV?K{w3SlNt;3(Z zR&+7Yb`oZpN=%I-#KSDQkP@1YQXe`JE8!)d`G}@+tiL!hGh# zD^=X;B($fvu=YxNI4?^HdeQ4H^jMiPwsS-oR?EM%F@n+BG2*(lDloA$J)C{V6_-{nF@qf4e^6JRSf4jB<)9u$#Zr2xn#l%0$y0{H?CFl>S(UakSb*J!icEgPBpD;3i zBT8b>k%vg$GTs*I*8Jn=Pa4ovu5g`-pn-Wmn8U^tws*$lB!Q-ZKIn1m2SI}>VXt9~ zy+T2=%CH>^8Wg|Sy`&Q}wbfXY_L!P-@GTFY#6n!k{fTwr;r%lINbi@yPEf=&Jc{uj@kGt)8c z(0QP0y^Y+6P%0&dy+UTS%q%(wS7!g(`_V;&Ac(IDQ{{A=~0$m>gIeEp30W3c9$JsFWy+Y z$-1sb2HWTD+F$XNyQ*y!=o4z0(~Q<)V|t48&E?m4TKH`r&1~Q&_(+yoXjCw!J7l)L z*lv^A!r#BpU&EmN(S{6 zbWnc}X7YY(@OT;-r1}3j_`XID&wbb1bCDMOu8%EXPMe;iS&6>#mQ(Eu57mU8(y%u> zBM#OPdM1A<%*7b}_)2fNr?kcN+-B?Eo-)t8W_}+LHWADk*pjwDW@pozRZhj2^2&6( z8LmcpH(xx~e(I1TD@Bq5{j&Z7E$o)tFLcYgZ@Pw_{_We&D#=xft1jr3MDpPOYaH(q z7x-|e5H$ykk{Iu%C39-&7#V79XG=~U%64;8X1R~GE2ij6(%k!ldXVOu+gVe{IRU6< z=58Wb6j{4BXJqcicpl~C=BCCeQ<~a>9L;E|J7n0sKgcQ4cWZCZ-IUUl)a>DJaOboi z55}mb?>!#Wt3I;5GN_+i+|<-ju$N5j_Qxf}C!x+2+f;Z+x7!jDH8hii#E)X*N7V@=uaG>`c?)|BQ>^{}Q?cMAMZao+$GeS_QJVM+|{De!xW zI}`qAy0hR;mOBG}XSg@iZYm|x=+-h0ql;1|Us90Drf#|3x2>~+D{^PT^=x;dr(5Rs zPc=^3)$O17giHFckru9%VzgaDk8Vba?3XvVkSa+I31~>ZTXwby8ZrYQFuVKxQ*91FfH@k23Y_s3g%Gqyq&-Kt5{m>F;pX;`GIJ*V3k1fD; z^EM-Iv_3Yc8KafVXko$Tx#ihzf5{d+SyB{Ai>?#G&};QRO*@rV6IEtXaon|&PUz{ieMH0cK2pE$+0_Cr}?xw~F`uMu;~;rw3X z1X>PN{?XtZjudvS2t5@TpEu`dy8XA)Sn-@?P3VCyaFwn{ocnP$P<)Qqb7*SlnL2~` z{YO*9uG%bdATLMUxIIU#E?*-KY+oaGp^sgSSG+G%5|=be;-dXX#j4O#b&Aka`5*Q= z#X~6*g6`Nl@d!5!CKEa(3()&h-Tqqbonj){_;C?DZ5ImdMuu(4lP(ghRg*` z#i}~hjvU4Vz2kSOyEXc4cUwKKG@IM*ISE=6bEvGPS}yR-^Wcg>#h%huQ)sd~HJs5^ z?&dvE(gnKIarBKb%LRHatdfQTQ*)$o+gIPuT2;^ z^XM(lmHTG5Za&=azlGwS5bpQXy$WNxZ^DS@H!wB==ECfja~eVKJ>D&+H`$!*oSIO@ zZ&ff>Mpw6g{xaP8SrvNVV2@a`qer~?@Y2vTA1@bWzd;;RuM{tRv?BD(j-_H?JB;6P zNvv+PhMxMkH1yPtFMBInHorF8GY&^Tw6V$B@1NIt>+qJ>%+33OR%eFvh9mqvvcUw@ zTAm(eZdnY!xiB+Q%-_ym;6DJJ$=H|@W~cl3Q~Xh0jg|(F>!?_63R>%eeHT{yW zY}sV}qW7-vqMWGf8hWGgDku7)oH!LZvE(Wz-UOUzLr&x@z=>wyL_2b#2j8?c_shdR zmz~;eO_t`Z_FJE)c1x4Fna)B^q;_+Yy_v!+VsF|CoVck`cFt+Osinbo>)vkv+)Frr zm_liKq@aa7?lO^l;J=EhfUTTJ2V5o(o39unFjo^vahhbN@hX0qukcXC?wXKz_z}KN ze8+O?5d3dM&)Pb1tfpumJLo=QZ!LFE7)jp%oVIVdc&r9WEE7iB9s_Q(z=&#(weRMg zp{EYBvzIzU;-hUVL-T)I5&8Of?$t4@URWW1y&rh&-(d^`==X{I*CXDvBQx}j24gLR3NuZeI9Lla&c~nit`rY)z#&VQi!Y^I;~U_k*}zAYR3Ms?W>!$B zpDdlxWA~6;jB#1vHUsZi6v#V`TI8HY4*8}L^a`uToL1(p@WA|7zC_OLGKD7U%Q4b} zj~bY8{3#*~>dWB1Sx>^WnS|NH_sY3M6;2|R@J(lydD^Xi?)?aKf{%lQsr2AVR#l*2 zK5N|DQ5DFt@$Ep%M_rffdhO|_LGvJ)6Gjxju4^yehi6awW!Y8h!~dAWE`tYqWA^NQ zzzv^(Cfl_66~Owt6-_9$>J9;K6nww~O_17<`H$@E`p|7(p!5O>703xaQ_&;N`53Ub zqdv0c+xkfT2VU_pCFX1OiW_rrXDBFc?85y_gSdQqmbd{VSY5e6T$iVde7!$5@->|= zetrCQ@#_l_@t)(j2jK;)0{1Hl#qY|eiN5V@=&1@Ai5;E2m8~e*W;de*E8d6w0j{`E z*S^yl%~`_VI=ufiTTAJ)F9;Uh9Kh?mmb>LlZ>xD~$70*WFxycQimwLlJBqmrm7o`D zgl}i^twa23y)w*Z&FH9>GppHf?C__2#jdiJgs|#P%**)QtcsRL^hzP8ys}jl*6t1R zrO%binTE3%YfWR8Gaa;Or}aed((jHPzJssxER^%zrw(s!hVM3@3vT*RwjkzO3?HpG zdZzu7Z$i);)!NX{895VZohkxE2jB5&ElBzO{*+||Bo$IW(s{pMr=-9G4$)f!`m&*@ z6}g~BJZgbb+Ik3hfPy9*TZ8mJClsmM+!d*QVrA&*kLAdvEz3er2eLv>YmDN@Wpbpx zOOCAZEsxY4cq?Ki2AC&uVZOjUyzj+)ft2Cdy~fbPHpw5gjlVk6y`9F6_lErdyvO=##({*bV`h&3^s3N-D{hV zkity9th2@nvej@0$W6;Ip%@8$v7GBjvsY#u2i=UC*fGVGgx{ zKC5M+oNKrv%na=S(Kj=CMd#u*u4L}8pX#-GNTxNtuH7tWfgT9|+th-VfOJ=5UbHf3 z8Mu|0OK;s(avt=_vA4#yjbQY8^%xm-W^mV6{wGQA`Kv~M*r*EVC}yh047|7E3BQX|!w5sqjkQZw~2H*pq=yBaj6+rSPPntaAyj77sfz6xUaUCrK;)aV@kX zcAx(F3R_~BTu}&F*1umHj^@s8R!=5*s(uRXdV{X2q&{M(^c@e<{}pb0GH@Q@x*E|* z!SL}tj~i9z$e3aX<2o5&v|@f1)d@3&ZNs zJHqM_g}^J!QIGpl#v-$MVsGmqkVzY>0*CZ}D7DD>#C+v1fY*8-=6B=H&8bDs@0S<3 zx_qRQ{FxHq?tN7Of5S9Mp-pc#0KIQ+zlR6@jz?SPjsepVpE$ju+h1<*iN!mr2WC{x z+kH69?$U%wR`o#9y$X<8iw9;_mbq=uz7%F}QHE9D!A!AI2^_L$ph$ojb6+lSO%8C) z9RoKNTo7k0;rF~2W*o6$1!2PsiGk9^10}O-Kz_>+r|VxZGxLB+D~p%5`NWdLKJliH zfg;LDShbf4E0(E%=cYz$zj^iraaLKE_>g6}c&Y4KW@dPHF+kw3KMPNjdHYJ;cMr_g zz7{6D7T#PkFsBmWe_s>kvH*7DqJg>h+FD{^)_EbU`~w5pO<8MfSiO{gJ`V1&VdZpH zm>BHOm$zsDPWI-1g_cYA6#$GCo@pQ%^WhpKp5lUd%LYqmbHxSmW+y;Yvr_!V2y=G+ z3xaXLFj%j@@kzWZ=va@Azo#ASP>&%F_J`>lZL2t|#d-s&a_p}~S z2%A*gKKQSEMPQi4QTvt%+1dMrWs1_MeVgdXF&3Vl^Za-=wqcj5H3{_0ue|;8rM5?F zO?(lbBH8_y%yf(!(%YhYkDZn=t(23t*pBCb271qfHlX{48dZHHi#2APt{QfgkjzFO ze!uCv0BJy$zgF`}=y{U&=I5}+d-yw`)-1oNd0Mm%)V&V~gq3=a`Bv@6NOhpgX`tT^ z;YohzyP299kmFrqypww#V}VTYv>w*-OJTl(QXk^s(Jq{e;QPY-$ngI8;_z|M1k4ff z(c_G`p+<^4)+T~xF&fEVCy5(hiix~Q=ZSAVgxPTaEb1QuT}@plevqdVW!fYDB9Jfk z!1dWfU?-{9i0|ixA_IQ_y&SXWX7ZrZ9lt*3T%Bv~Eyj~G7|R9b?G3dsqr`5r{-UQ% zpjr-KWrRjGXm3iL&`8;$yJc2Oqw#7~g27(zcd%i_7?ju+$n2`B zz%qpb?oJuo`h7w;`5QK@8%B))*NhcmRTd}YmzCn`kC1cw>0Y%5SE){dUWeH?5<40@ z?zSa_^?!+;w#rrr-xM#@Ghw16N|5nABCf09n%aRN!w&WSJT_627kYTlV(48X|8PnYuWK^$3-}6n@4#iF^JShE zPS|sI|9P3{N!IU!%D4>N|C(IML*EOgN(u&fl_2$Cq{4Y@$r+5h82Q%jh+%Y=RCMd~B@9%?597b3=DHEb;%I?!Ov!H)I z&Ab+5RGh#l)KR@Qx%#G1vKb+wx2(1;w{@G%lXGBZ;y^yv}N$@E=hk}ec<~in; ze%avLVxJIQsnWIJO4Z%z%-pS|M7r#v`Rb-_+3fE2Yj&#ka(%Ls&F`0!+NcxPU`Wot z_(%?3M|bWf<4SSApR$Pem8opWN}wMYR~6cE@cXVa;!D@#UJgd_e--zG0^q}>jhOHxX|KbBbrcKu1P?9J{wO6SY*<}&zD`rH?S1!R{MFrsXdF|1 z0&f`#m@-ogz+0*TjBuH`#9+Vks4h>}Pcpk?U2>PKPo|kJ*{`4In~-;)QLHN29az5?l{ z_(9dkwnvXT{w(Kcu4P2biDpDBOsNR{Nz=A^=_(>Azy@erfv_sk+cl^sTjW7~Ie$i; zyRXW+M=xV;@foZ0dAwK2lRfZk^(5&LxPPsN z4VWEVg&DbDbs3$v8@q-Mxjv6#@XWd^t5rcRmIz%`0dpJ`7`0_+eWpU79L=m6-nOtz zHs}?>Jv4UY|EW%vD#V7h3Pr~r}H zYg%SneY6Ji(lI3`t&#z0BP-P9s01 z6-|`;8fvNATg$kmT7}zG$GRKplx}a`P@T$+qX4v%8RM2#2kY0ZeSH=5(LG68>eqP@ z&J)lZm!u*FTXOI8wd?9rLCc+xUjwk!gyQBk*9MoVf?jHY(La8fRH}j={GFfR)Ho26 z(eVgWrQI(pO{*DWI$kv(#HLj>t3CkCHmss~2LRg#I?F%`@|UmmAeW!9 z7IZ)816th%fc07E!?Vx_XS5H7Xdlwy{)Jk{Px>G`kvagQ`i|9{P1*aF^Q_-cy1A73 zs_O#&iqen~*PpSslln;^V!o~y?=!}gpJ5+?W{b}PO~J@2@S1?m{VDt=hG%`I`pu=T zp?!_swVSL3J=52^`E~V$dN0odpoA1%DjZ>cSC=JE=Mc_?f% z)ebF728}UTk1`J_GFf_OKw4wv$p#59xRbCQrZrtLGoS~D`Px+z`6B`$T{Kxbafyr& z$0X@fe`5#H)!0q|)xYte!**;r!h9!Bg!nSd&g&w-_hCJxqhZAdulr(v3k{gX*0hpH z?Jne*v+|<0WA(^2Sf=z*L&t;o9^Qvm1y(t<79#^=3oi&->a^{KG1_{`U}uGwxD)Uc z?XZ*{Tdw-*u>LUEnN8NWI`zCODtTN`n`!0evdM=zwO03GdnD&`xx`z^cdRIo6a=Nh z-Fyr@R{>hXz4p89QmLJs*H zE{!+Z7s~7rcJ2vzPuoD~R^v&rI3F{R^h92u!Kf~GOM@qS#)DWqr zCURq0=W3Xn8?B>J8Ynq>A3(Fo`uh2M;F}nK*Q*-Y0u;NmdcK@vFvv;Ohc0?oBkX6R z7~E1#eTY4HLO*XbvVrSBFU+?(s{$4ddLNBu7#ZFA>bTtBvJNF2!1S1dzdxF#!Oo|2 zCT;7Ly$j;Twu~{wwB$%M{@Zxe$DHYkE0Yve0Y$b2toFb5*8KJVE6I~AWr)ZB`nHo8 zo{Z++Aca#pkFH&iHl8s$rL#If$-nfV%qd=rQV3{RQONfKHOEJzB95$D2Gp&0K3lV{ z-U2jlg{xF&yd$44s?7x|StLmH5n=&2gaJp z{)z2M3_nJj!p!K&(wOl7|J4JWo*vG z>shr8phJEejn{r}GzT@OKQGTQfYnvJLw?u1)f;c`_A9cx{pd+HOp0m_2EV6Zmoo)8 zGrC8B``27jdC$u;fL_nbi-1nqPPExrV_Hv({4RW}da*`$hc?tEORmdzL7OZ7hp1f1 zl2d@Efu~Y9s3l>mcSbFf1rSiULnSA`VLcdZ1HE;N;$ToF<1^PZfZ&{;=$DcX2bUIe zW{NTH+&Mp!U>PHlJ}t&)w!&5RNtxB-Id@muNm)n6VQP!QH0!cuXpg?Ac8R^gf!TQ) zug7@=s}*#RZeEeY`4Ar`qnS2!gwAoKKhuMdZt@-s`jihj*VSh8IG-g;_lpeN>(XAs zV;;W*?&e&^r?F3~{R|B{%lOxw_rQJAWz1ALha3zJ(3!ObX==gD91MO7wD^AXjUs13 z8jaq&1kXqyb>F;v&d(Ngrzy{z^Q#!61!LC4_~19Kvs%WOp;Z2)oNhQLv!-&Z-u`i# z>dZM1I%X)@ZEgK(|HXvhl(^?V!+Q&@Hn{7yHudejXaZXCzDs|Sdri4}6aPqAEFW@i zu+u!G7M;6m=&TE~#WwMCYe}Xys3fdg=>huN7t>TNg96sBJ(Mi1yv(%|x27FNOSu;0 z{M|g6tjxND#|$oJ=<_3Dzh6_R3F;N9-LD7?rFq4;9y56%zby1jzQcfPF?FG5D!5jM z_)QM3sMtbJjtS^_;qW_F^qRK%Hs&PXl1+`IQu5&QS$3q_N|H_6PJ180gH(b9=hnb_dC6utL( zk@M=kTSTv;DiCPEyXT9l0u^SwKcjY{R4hvWP=ir4)g;Zr@lbhUnn`exMt07vrA2C? zQr&2Me7N{`x}W6;xE5V2Svuxt-=Tzm>&Nka+dI!cC-Vlrt9%WAEjq0s9qh|^B2X>n z)9&-fW%tQ5^nJ1(?Bsa+JpU~dl^{3w3ugAm{Ndx#KFoY3Pf^zz?32xIc%zLK%2`e* zS3;jio3Nch&mf$^4-4g#$*Sa`CW7(1fg9nS_2g}gg=RwTTFz1vX0ibepy)@FEKM84 zJT}X9jU7x9ye(K0yAB$x`u{RQrl2X-C|exO9Cjli+n;sI9Zy%Bq_r+GzvY#u#L zax#3;RZ8)39jvk72M>7mywG8Pu6wy`L%&Q ze>#l)2G_fxWBPp|wE^n3Zu7?MN$;3H`e`X)>urRj zr@l|mll-#IYAe*c6YZ{fn1QO~V9>5P7@VP<541<5#*TUZ8}uY;p1eUfG3v8&L(#N6 z^c)$dL-ZG3o_DTud1j@|PM#yCQsF@}+ zpT|!J+P`mr^};g*<2b^1)>f}(zZTO;)VBsTW2WllDFxg&Z6mjn_vZ5mJiQ*1=hQt zpXEc?a8xmKjNWU%`YiGn&lmPfv2p5XVxdqV(uRo!Z@mM?8W$Q72 zF;AwIb*{70j&aGV8dF?(J{tKnF>F|QXIM4zqVVCkXP{%;GcbPgKk0*Yh4IA|;rxCu z=v0~ciYQ(h1k~kVWR##^ux7rPFNn@GtpUJN*R?T$-^E*DZm|>8ZlAiO&}&9cHfTnA z9;)E8;pvSqnk&J!SQ+|LekpvHhT`&3!~dV;#zaXsNIFN4Q6hVPlp9;TG@o%}?7Y&x z=*|L%J4|ee!mal)EhKZlP?4#I*$6$Kta6^dkykXcMT#ccyv31SV zmB5*HkRUdhRe#fa{Ca%5pTx4e3TM?goLbx!U`tqcyiWmeY5+#uS>7)&nM{jS&ew0^ zS%8#{LOQdjDsV5MQ>hUly%!+Gvj^33K1k(1_b8gFfo&zi1=EC%s(?eyc+S8$&|7+B z;scnk=)&p?0Jp3RE3U+b)$em*rJNqljn4?@W?_tNGe{*yFj@fq)C;};aXp&c&(G}# zz`B{h53N)MH?V(}b21RmFY+zdHJRm6P38lcb=y-M^`-~U`19x)`7IY`rom zxX1Q($Yi1gV8JSt!soxjybJnpk)bNEf;%BQTn#H{_kAfBwgFDa$Ql*QhoF10`9R+j zr9EOFOy_o(A2|CbAgd-y-wrB;9ZJxRxT?SoZ6l8qfN$b^Em&~SU=)2u^?sa)?;H^P=}*G%9$j| zCC(dNNJ(});OdW~3l*e8QFv#V8G2E8pV{4kr*>hyu27I$I12i6?ZF%V*Rwl}?<73I zSxig6ly{O}tvy8Fs(FI{ zQZ|wKLu5P(wDG8;Yv|Q2rnN3{LEFR8crCw;JaFo;UIF5(%A|yAb?hH8Tkf_cjJS#u0Uvi*y)~H6+gta`j1zK$Po1>W6m+ECaM)b5 zGP7gd%HWEoj-xB$laxHkdT52I<_4fKC&6{g30dCylj^)$DA1**&4Kr83+sZ|g&&ugunpG88&Mg7U zBw1Q?DMzBTj%Wo+7T70By8=pKIq3N|{)Al8rUhMJ6V>(2m%fxsNW(I~vL5O)A#5~H z+Xot`>0t0A_i!!J-#t89jqhL5`8IjqcH(_%x(B^ID3)Sb&Ly-6PU=)(i$fi_x~K(w zA_cQuSK<=s#B%xv!IrpRFf5=#q6Op1-OZ!Uc2Z~LEjT}1!uRrcgQFU*wp?23thOGH zp}m<-h0ox$=zDCLj+v`z-myAeAA0+A1XC0VJ&)(boO?f5qNFgZ`OTaUqwT(rC%uB4 zG866{GS6I{YWpmvDzK2j^ITMVoL)Q6#SAU5eV%={jS=E4`m7fO2LWodzHe@_mVllI zx|{5+x620Cmsi*NpN3yfy|jwS152Ce`bM0`YBzT*k~0mfZ1g65IM;wD>axwly~S&z z^LP@@3L^8P5lX$9YOJkxQn{ypK@e??D%q_83W z6_8#O_-1DZ3%sNdhz+bE6KdW1-b2CU|AT`*f+sl3%++s=xT=oT(GKK)-T5hF+lF4R zA3qtG5)bW~@RTs8Psd!S3?nQ7e!bh4s6&l%ycAE4yk1YjOnlYo59|Ig8d+zTv-M5Z zs(^)zp9q9&g1D*Xi|ZyziwDLr78%{~8r*daug2_Y?Lme}%j5gJwZVEKY$H8>lKouA z3R_Y*F$?p~;EAb|3O* z1B+=5nXCs$;=by#A?6rMQxtnAVYL5XZoY~+5?1-F7;R09p5>M?CXDBT zN#|yEWQ5rvjKaY{uZZw%aaCX`!-Tc*pkGYB+^YucHY`VYP)Z{_(Fo50jz}xf_r|oG znxv(~P_(S!L8Y%@Ej2Yfs4)`h+r}y>GLvN90~C#R%|h#8>j$0N`H4I&+E#j{w{p}4 zvNK|O!rCQ$+ISXZ9ggdazPEzPIC`39PWBboLyswI(DLlf%R(*=}8N?P(`)QcqPmVt3@BL6RM?*iUbl{F0SeR7f}Cuy3N zmMH-tX$#Ya%9nDp6vQ-54yB+ih>GCMq}(Pg7mK1lgT5LN__?T)0{W&!q2R56m$rir z;-!UAUv*@ral9b4V|09l<}@cKg_aZQXTP;hS}y8*GvEJw&;Rt<&AIKp*Is+=b=jA- zvXTEB2OAJalBqvE^%)pR$`}hTRXy5*W9la(>U8s(q46|&+Ru!r@7YlEniH|n6g1xN zL38?{Je*&KR6su_*<(Y}ng-m92>lE=P(gnmS&IE#`0w_2iPGQa*96?ZXh%tev)qk> zO2vBVbZd)Q>2agdIBH(`+IP21XBt})tB~|a?mX;OiQb1@~LtDy95?(sz_j5n- zAaX=tX=+TXSPGhdVNt3>A6|FGR|86h^1ihdPIEC|!NX|RXGk-`2|kh1QL`$x>VL-_ z(4PD;hy7=~VY2Xln>SFq!W(jtHxPw4T+I=fkI7h?^~+E%Qo|cK_6Mm!P^jU?|1)an zT|=`P-8Wsh{$)#g)S>L5WPXPPxOM~ zQP3T~IY|gkS-lO%BdFM5Y7w1fs4<)agW#8*GK)Jm4kJ= z_OHr$Wozt!0OJ@e+6-yE!19j1T+n#$5kRvs(Y$dT)Y?)_g>MT|E^Yn+#%{RMFkjBD zIMkx9g8X9pd9ryS{0W*BX# z{C4Qu3!ioA_Ns6{t}xa^#@75+FKp44UvN0;`P1@)rgVu3uf4m%MW@`#GcCm>U>i7= zl=xa0uoudquG$iZnZ$gK_vN`1HMuR$zjq*1`|;u3)B{AY5mFRee7(WT$1)ae5)oB6pKO-!DOtMUtZVG-^Fd{JQ7ChxoE)c*9WXIth1ug;kCQp?T2vC>^1 z$P3C;z`2aVGSvt2Lfc}~`|_#+%xM&ss_^bEyjuii7P&@33AbShY9ZbIqM%|S$0B$( z37@Hi^b#oTa=w~tO7Z-EEM?s_r7SDJ60ju5$6!fmlXkT<0DmooR;ABV329EqZ9}g+ zP%38>mVhPvzPuiK5br$7otNUQyY64?&`p=NMo8<0{Rtn3SKH}^o$rC2u8A#Ezt@$N z&G4}$>FAj~Wm+|^Mp9v;P8UlDoqZX_mB|H$V^v%~@VuCpEC$=8=v(rlwKpz{m)i(nSrs#>u0)Ie)`hD~eFHi3`v$U{ zuJ~We&IRAB+U?f17sS)-C6^56(PKTGxZB+qWcczhXVT_tTN%sXoTNb1FY%`Qdn$E7 z0@lW?u2BR%{$l&?=}HY5@=Za!#~F5#wV0>;Ecvc{+-t*m^7wnIG9+)8jVBh&r@i8J zoIb{5t`fdrMa$9*>6Wg->d)kSp9wUO>pK{*%XSbZ_ZY~;lfrg}vCf;QwWa-S(i)sk z^_lOMQq?Nq1J&b`md1-YEoe@EMK5%uB<|*Vg9U5wJTkSj(+c8D9xY^Yo8zo~_ocL+ z9lqW#KkrIv0$!2&V0FQ=mxL40wwHQ0|LzpD+rx3f4pTA7<y#l9y^%k694lV^lTtU(QS^m4{xU;du#@~4LEYu2u=u-jMISJQ&tRh#Vg zq)@l}cl=3d ze`apJW;M$C&*Tb!&uSw0Oj)wYl=d^<<;yzLkr<=b#Tea4(A{pj;CaQqW}&Mo{it8Aa z@tbYNSPx^#ap>@LPfDM+psCUr^N=O+-s$d<<%DkJmdnN-WdQZZ(xPzLiF24 zDH_ts(Q~DJ(eK{>B-#*qB6`4s9yvRrFZojX>;=-mLwH}dCi+7$-Zu@bx2%Y^&Gts+ z$|X@(6}+>h^yR{Lx07+v+c#RGC%6CCz;6s^qAxa7#-ktJ5|zF-#_Y5-Ds5a7ZSUfE zO|*t`QR#psI$!6FezLu3V1vaLeelqd=&D0a0~H&k0p;{Zu{5ygK;M9N7x0cveFNi^ zxD5cg2U`Hpdhra>lDKO}NxZylezqfCQk~Lgxz`iFx$MVZeG+}%_i6M6$I^HdzA@Yj zupDqjr4x9fLzU^l=;4=Zy?vXBwoJbGJc`nrhFnmh#-rA|Xl)4+ocLsY7?0IuQP2b(N_CkKhvY|M!| zb_%*_s-rM zLVYQH3oBrxl;N8<g05WRRF=7Xc!=<8G;}UAYgFh;YhnBUe2BT+LBYld*UuGrLV!6($`(0=JEaV zxXXs{;CBd%KWgWeDAe_+6J#mK(@8sm(McZ$qZ1Fu2G)gQ1M3~|e#ffl_tuoY_0DC{ z@A9&g(fFVf&k(PPo|~Pek4?LV1>GMe&5_$=ShZLAX6yz)gt$urW4JLo66! z8Akm*d>LT3Ix26U3%@XSPgF=# zd)lMZKFm>kErj#1j+DM{C&dOH^aSB~HuQ`?S_mx-O@h`J!#B{PLMY>=9qrMZ!p#Ho zVVqW2HPCCwqu_g>wF*^i;GvKf#?c6V4R9$>bV1iR;6c#R6EIFHS4A&D>nb){m7WPq zK04qaIST%snhX?GjHjbvlmkD?+T@R>meod6ZO8!_7>CUR3mnkT3V+Ip@_E?8-01DI z6Zuz7M!G_7)HJZb>HrMXMoneqeWb=8HF0e{4-*#3fbmz|JWxSi8(=0{qWo;g4`aIm zy=i3~9dIHn&|>>!ghQ3!=-~?kYeIN>+aI0D0R|5P20lz+pn?F8E%rp6haexcM>`o? zk6Z-qcdUuFeE=hT^0MgsuCdS)*Z?0VpnV6L2X6O_ zin#P@;)$NoVXaW!Jvq@bkS{eGUmMT@HS)?B1TMf) zgNI~E4>O<3TEEX+oTYWWhK0hbIHmKqqIYaDppX)#51+pU{?v=YXVFr4)2bt0H5| zC6i%Ci?bxnklJstU_F!K85X}&cV?bE)?yHxOfjA`pYo!!Cpea0#XsA-t!4{feqrlc zY3VR&kN9lw6A#eqM|6BHXSQK(nx*(Zx^cWb%n}c#gnpdkK@q=Ha)t0pCzKI;CGIE! z9ipLTS$TLaQBDSANght1D$}%QVp{bxF;=xa?nKYiwujd{)3zLJh6O+e^P%sqhUKgR3$Y|1VJj8@ z6kJ=r3t+J?ga0H1kT~udvCd^*p6;@_%r0p)!lX5stN@u~dOQ`NIncUe^rY1`z#u>q zqqMPg((>JTQm~k3p{LJ8K9?=NXJeyC)3RgAUX{muO)E)n!+l^%EYpM}(6OmqTIO&Q z$>&Sx(Q|-u9u*2!)4XU0wbv|{RyD5zowNbh?0T0|p$)WnX{9zqEWN9nS7Wp&t2~m} z*PL#*$C)>Bv+OHym;ZA>iB>4dE707o4mEG#t%GI%T3EZIf&+YTx?YV6po^2&U zdyO7i{dt$hW?w2TGlopc*&c_^3S|S0;EpqEY+1TA_1y6)`_j*3M_ak1(%)1H^ro6Q z8?JThUB7cBdz-87TT1e@t!gjM`b=4lGf8^A)|T1&XP)F4;6L2!-c=JrT~%Ap z3eO9%?>Q~5Xl7;h(^&ITlHR18acNqD{t{}I*DQfPU8)OVZ3U}(R%6TP#2ro|?f2a( zXPKI?7efUrh)`@G)}G}>p@U$vlkW!xOpq*)?2y{}2L{?9g&;|g9Pq3Hk_S=^Bp*EU zLu!E3gzx_+yocn0R0D}XGC{IHvO~(Mr6d;;El^HUx`($D3sPd4Y$X>uNVbQ+hj;V4 zfFG8*=#BPxx`Xs?uE}=w2H^|)KhPJdTZyr$T_jsfhn(-U`uJp?+8+UX+rDO)-#w6N9|DtH`V;k2y<+R14``nhk_#HzxOR9zn4Mu$nmyAvE2x$QA; zo5FKn5A{?dCeR)-b=X(excmTjsmtmz<(+OdS^as9t$wRhW=G;WyA*sDY}%(nrrf6L zYilvLk`|o5a}b2!yf@(7$G_jYm2Y%5wm!)lV+`>-aV}tD<;yXKsdt`+(SY?DI;54$ z`FmHo_)M26uAfe47=;q$jQ+)7Q`O^h-jfSM54i4K?r46rbfkB^>s}A>n&O;2%c2fN zOAXmLhg0J*Seh!e{VHkF@}%rny?DM*1!-tM^(owwMYiypo5u5xog>Kx-oCVD8IjuJ zY}4kZmQ|&|6%udWlPCL$)O3k$()O!+Sm9lQvmxnHY1ibc=2Zur(>gzsU2RC=H&&mo zzTlWzT? zJLCnU-YP?%>uKH{%Dh&EGOuO0YF=xYWbP(bYHwcByb?GhjKDO>6`i=6u%F4LIIHNk zFMC!nQbyQB5H?z{ZgqLs$9>ji*iI~Kmq<%fLZ8WYAJYA2(7L8-JW;&18e>~f7`5A+ zb?wyl9(*m}jI#bcC^adB`);(cbgeFyuG{VYd;9ciI(|-vw2EXA>y#CD%1i~FzEEZy z^If?>DSbhj=jdQ?pX+(_-tKgsUrwjfOR8S5;$zLr>9i}WziHBU;-QuyFD)60mzK-x z5~k`BoVoa{Yq)j08Et#ZzhT$^0=QwL^SRDk`2DOaJ!i_AM(4gxYv`fJDG5OO>CA;D z<}i{hoPXQdvhN(NSht$=kF-o#!=z&bv($dOW#Z~a7>f_SV)drR^`^D1O81+s?{ygk zHNW;Op8szHp3)8}1WAJAgXD+Q0I3P@fk*ivH9%^DmM@FzM;b{Gu8ql8oTAo<9wuH?zHWS1cgM(GR# zMwY!po;*Y2x&xlwA?N3Bbsh8GA=~FNLJ70U{r|Mz0p-q;b61(-W*yWwE}mKG~EF;;W|QtFq@kAEPfsE^Y?|L{PR51bi#c@#~YyOGN*~v!H*7= zhqseE%<=E-+pk<7aUweah$ffx^oN4W9 z(4&i8cZYSV5iTf6^$hStfO$ryt3qa{(E{M=)8l`;1E5AJUkNhM&JXnczpt3$bUG7D zXX&(O1g%OfJfd0(MNgP7*}5QfNO(mr1xX^+K>fb&VatohgRofzd|ps0#2!- z!byz0shtPC;~E9L5c+@aD2ywz9iEhqO2%Fvg}wfb`*G;=(oq=y&NXVAYo}{o`>ZZ~ z2>TmLg}zo#uYz9p0DTxXk2oT@2!$SU`^bXpktF~1vn2kLQ*0voGs9Z&Os%XXwE`bUlZ zqfzO8)IT=ppZ))$f24DybvX3Hvygm{_CZ<&iHFp-m0lHZ*uW2+#vr|W<}tOTH&n4# z?lQ5vm^_u=68uDdlu~O$Z`mbs@^#gOYBZl za>(J#zeIYTM9BoYjRvk&A&nBrb_KIQ&h}v6-rlbnJJ`22?AucJkuRHCrnGoV(%9ks z!=Wt}hvG9&@ld1DCj7A+yRg?e)@F+Ci{|7H%H{Q%zU zVp=8_Z3=kq3={2d!XMO>hwsF;JIbX`)k^yvh^2Oi7A$pVNp)iXAhYo-+k3iZ7qRkb z(huTZc`mnEZli2KRnHV+|DjDmYOQ>UjtLBtOx<`AgUK}nJLID#=yNM0SgjlLj)JX0 ztUKkfiH7#Z!zOc?qvdXiQK`ZXvcYvUzPaFyxW`l8_ZN4XSJkin{%)y2ZE~$CIU4s_ z?eSDHx9?PONzdddr6q5G=5%vOs%wM`qvMtL{dUFeB^d3{xOQ%2SLfzKmx)^-xoX?^4np|2>&8#7su4_`){^gdq z_aPUaVLA~r8K-)2?QW_!^GS8b8-qKXi1oSY2@;O&9Ri z@Yib*VznYG)TeQ6c|_=Cff@^xnBaJ-4Wpsy!y1G3C6-@-F&ci!&Q#VC2foQcPg%Xe zf4Gh9+82p1m$1U4>LY?d1D5j~I+F!m4fO9h7lT+L)}v9dqWc)i-(}O}_R}EFRL%%V(RJ<>@yDw9XwODT*#m-ShUD$bqTlJH=z1zB zr<9MlhkO@Off$ChYa1D13qyo0fI~G&oFAGRXXhMhVZrK5@6UKwEm-IF1Q8O15MYrS z1OvtWimc_aa~hzSoupGv58)~b>(@4-FP`%7PGXF)+!3&3RDS4VOCD;_I-~;wyjR!Z zPUNJ$Y`ubHw4dogJ#`bt=CR|Qs_tl+gKz^eyr(8+JB)gXK75q}fQW0XwqhLw; zF_!WGOIAVg>-4yD(67^Y2CSNA087RMmQ=^QJf5=B)0vav?BX$(eEsTveJrEjB_0i2davyhIF`vnRtfAH*`h@q-_4E$N1Y!^!l=n zVuE_qEZIKlg;*~4yDE^cZ!$tLd^33zuKt1!2AUUYOCZHZLI3GCHGSTtrW2JtxKPg41FL|`qa>4&-XJ6%fsK?qv>ZhIl#s= zd$U}OFqb(PD`wE<{4$7IK>0t>0@9@JZsnBUz;BH2L~ASn!K2s*IFdDCVtf?z%NgCP zfGXdPu||2|JJ-ti?{_SVhDw(WeD|z8aBf*RdibLi(a<>|dgR451LwXC4Se7BHBfRx zbS4)XxYRJO+oT8mj?V9%F%q@l1O3E<`uDBfH|j&tn?BTv{+08)Q&%?hYYvfq#;@wv z=x*(vF}|X^Xr)f{AITH_d!daBH2ta)cwb}`{flqwo<0ZikN3uZe|SYS>|GK4wscwa zo6=GkN6+>%9=2ZvbX7P*-Or$|o1X%8zz8isT0;%~f4~T0`cz^$BP;yPgN!ZZ zOQezOLbW7KS|&^lthkJGhjbF`{g*?A7*(+^XGY zy=LukuW5&x)a;>o@)Q%vGxDXaw>Z^%_c_;|K}srhR*pW^#k~86OONz|P2TTC&!mH= z>r$o6DEdSmvdu?=ZLT?gWavzt>7J6xkqPgqrhnxTSAH2n3TLP$=Dl042*b?;`)3^#WF7$`RqZzmnglLM0<)sf@k*zpLI`c zXO5(eR0;0@&1|NlucH$au^iA-I4`p^cv(BWH#p{J;JiY6N1Q4D1jZrwE(Xzu$t@jB zqvF{JXbrh>51$n3l-Z^s+B@$qZP()-mBtEr5*gZ)tE#Vc-h3vgr+L69Dc!xTHK1=l?qOHgXqvspZ)gO+T4ha{jNATqW)jRX@qn z2xE~>?2M2DT)n`8wDL{x^|K^Lxxe@_baoHWNz3n0&pg(e&>(M?*|)Ug$6VUJf2#R)yb-m_GVP4^=#=XA=;Sg_v~Rm7dg0KLXkXQm=!MydeH|N$`v%70 zD)AC`{O0Qa2L92C(LiiNP$FILXku-+OWPYF7_XrAgI_SaEO0gCUz7<`6?)DE4q~6* zZ6Ap~QWC!!>%4CJ5p^y6e}k>{#DCAR(?ih$o31VtwR6Ajp7`h6y9?$V=r;stzj0eq z-GP2~KJC|JY3kGphn@r+mz|)kSJyGX!I^%;qlf!7POe{_1z+Xf)@@&@68&?sMgPt$ zzxsI}fZsI}h9gjEJT z&+^|XS~AMIrSY3@_*?X#wlCrB6m`?-=npa5g*DFw89kRC6buaL%YUXt(ARU)`wf%O z+GuxV@>r{(V<@8MrY36B1+iA8ZsYZ##Py62?g70D24ixJ)pi2~B=Z^`J=QWa$nM+V z7ybI+&IP*rT>PfO_UI(rlIYRAQ1l?^&NTqvJwx#Q8KJv{ARQhQj0oLI1-ixW_HX^u z#URr(6#LV|kKnJ+$)`=4T|Rz)DZ-K*JEzmWyxx_}6YD4?>d*pzDL?3Q?I($P?EL%I zR4E+&yo)j7X`$^tjqn$u_G@BVg8qCIeqm+(V5gE(U38(!pAniZPiX6u&o!y{?d&(u z&$>u9F*w51p-cB=-dX$}=ypkU$Go&)qjP?{CV-qLsTOU;@~~q@sH3w>#gLq}yiV9` zyoJAw|GUikn8^-V`%wTJlol&O+&vt#HM{&@x;|iV-~PJv_ASY z2{|muzzYMd$<`b4a$9e(X62rUjbLmZ%NOVwcI_wG)A0AE_OBv0a*Aaon#@@eWi3%t zs`iM5cY}>^ien0E20ZK~WncPZNxi{?&R+&^4PnlTPfo=%>2ohJ?EI^Ct?;>eTlvB7st2~$i*&fbzfrp5_!bnf`Ilak07@V za^FcO9CXH|F zd1@nxWg0cpNlwOy+6Xn*MhHize;Q%APa|rxMcfEa7~2yO)rQ)Ls_2ObQ~gAQE_xzD z%Bbjrcle#gW&}H(jj?Q!$e1;v%B&U%yi;>5{7w{4a9g0%zXX?mt%n+yLXEjbZaNve z;Hnz;M6_jCQ;tZ@dm_5BPa;V+tnrhG#{3DCv?s!Dz|!_aRMmST%%VLJTDAvrGU6tL z!bZhM;8#~flzX)eA*lt(G|F$1O|ATDZ7lo}fN)QQku-4 zgefD?ZmbEQvSIrNp)5-DqM9ShuZ9ao^jy*)bhQy~LjqzgTZIf$v=YI0QdT9;ARFf3b?Q_gII<34qhk0 zwRZ42(dI-ub%X8X2HU6~>=DCYk0dKSQX4UHD&;qYV-$EyKk1=e!Se8iwReu$kI(>iS z22K)}jI10D{}*e7jl>@z=4P?jd{I16<`TbMTq!;`vQm6z5vJMxd>cjKSwre+`H zXaH;e>mYlhD&N4LJo9vf0tBAv&7y<5D7M+0;y2U3ikQYjolx(=@fSqTc&wNCbU)yW zhW;pitsfE4Uu7GcL=$I58Tkfsre8;f;+B(be{$f@rW#82-F?Mw0DRpzy^4-s(eXcV zwm|=26)UK<<>4*5^6)m5CN#4OS5arXGF^XgFKEYk-QJG3y(f8%u#+YGaP;fpiV-%m za6QNon4)IU4c97Lv*?0*7I0Vs&z!c~2Cp;VIdl1%*+AjiB5IWTkAu&g9g5zL@gy@{7){a|V!TZv zv9-(I$+eMYv)Kjm#fCBItRms|N78`PQtqhe}%xtdw0KKkfJLVSh$)(q*~DzJq1N5-191zJ0N{~=@h zBS3-?0x;+P$e2YNMN$UPajyZbv!V-2D|)lFNp#zqL|0KNjKYbcYLQ7)+1QCNcFXFW zV@0-XlE^F~@Gcebo(=aVktrf@4P(6wzrt^T%HW=s(U^&5DfhID#7yLqj+$AdwIgM= zLKlPdpdZEWb~d)A^E5xBBZJ@0`*<==CunFX*j#DSXI;hJcD+{65PBm!^^ammPiH{+ zTm-$V>!r7T8W)n?`BLxJ)#e=5R5TCZ;d*`6K% zJ~9%$c^{5xjnBojdXhbCA5ukHKn4z@6MWQAJN(drn3i3CNL!$)JrrZ9z82?ZIX`2e zR->P91bdW$5zjHZl6Qd(4Q=DK<8AyowO&lc3W$yva-VZ(|2#^NCpY6eJU+L z#_l^u7PgsF{Aq!dx>NFkkQ!PIwGtw1q-bLRb#Mv3)V2sY+0s8G#}?|arOCaE)_eH3 zyFms`_YZQTi{hWEk%L)9rYzpgykP;lI~^fMc=5>MS>lm77sYpWTom7}KNBH;-XNY^ z`JmYKz`1T_Ww@Jp;EP~PT)sGUAK$qC2zs54iOc3R1E-@icL9|gjIq$h6TlnWs{a-t zJKG~r(1ks??1YkouO`K@7dt6eRN7Ev;uqe4=%*(WLNs$*IY+kZueus!;M7v z6G8vAXNIm8ztP+&XNHD1cFO5ZYxzaI)QxuQ#}V?tLBMJdY69HTS>p3^O2y|FM}suf z2^dX(dVM3tSjAQNHYf%90IoVC>ldT9*UkvP@>H{~4odA}vVn#glAh{;7SbD`^quDc zQYiNbmOEQ)1iUn^EE5~&M1o8x@$EA4-~;zTZT|)G0s3;KTm0{pOM(9b2VdDFuGouQ z#dNw&d}jJu@gLJ;;y)JmijFx=q6gkApN?X;x~Gfdk4EICqf@#zON$F#(TxA@lF_}3qqf`lg^ZfKcrgw0pTRoPD_mXV5iKs5$l)`uruGQ^0zbLH>97CIumTvwfGL_3x*%2j^#X_oCRxS`bChR$i?CV zz9h6r&Ih+SQ`(0gHekF{;RNF_L!awuA%26ffz;-DgE$8D9i{v{;1Iu#A~$JP{I?W} zo}K4|RU*#A7hbb&jrmbBCj&2htDD`)w9?Dg!=qh;@V+gsqD1S(_&_FG*=Q!u*wG|9 z%#K+lHrZb)c%^P8R9@-@-JUlVuaAj2l%GYk<8-Xue#}DVxn`o-^t&*FWpnd}L zq9)eXbHvA5)OBn<5w;RTW+TwbYdp=~MU4ED@cz|II*FZW0T@AsoA{`_;Ee1PcVXyp}P*p3=Fpmu8F!{=kMVeu+>?+ z*3W=-p5z}M(gx5cl%@`6^A@_aelEaHlibMJIKM7{zdCL^= zRa&L(XC-x=f)gG&G~$#B+mzpD1ubE6{{q%KEhv{5LuZ$6NoQBm0!Gji18@?y=o`0R z`CdsK4J52GaDqcs-1E)U(XRQf;=b=EL9NDNuDhiqHO7Zt!mo(C zlh~pQ0~XRAwR8Og9?j4`qmuY$E(%;@ozLUWZGSmN8Kl2y@if%Yim0$jh1%I8&=Vv6 z_Hv9-F}7*(VIC?xqJsC;L+|wPPQ_p@$bs`7Mfhe@1a->);!c`i?3%Hx{gHQV1)$xX zk{t~j;ThNqX1s?o%oZt0rFSKsFv=4~aH^mt4Hf>RdUEjjR;46n_>+S==Ps?@&-p?m~WhvKeB<1O$JMskHjP${o1~{mR~^Mf5K}0$K=y&k{{JsmB0LguV#!N zcgv6T6U#9FaQ`U3(Vwb3$$%#ozs8>wvIL4=8od|2&yH#6gc3&vHalr>_`a_~8E};p z9JTLE=&c*JPJ#3)h`$>wfzi9u_Q=f>8nK;;7!PUJ#CRAHxH`r|1z<@NNeVOqWU2u0 z6rv5h53uS2*lKe<_Ry`$PDK-7@k0U1ufxa;+LH9(h`omTA4g{3YI6?_#bwAkG!&Hq z@p0Y05l@qBmo6pnncS8`{CPZs#%HNt5d;(xh4{W=9qV@b0`vn z#R0l}Ft;)55VkG3mnbn9Mg+L}AID%Y)a{cWbtUNfy1~eDFKYY0CzRl zJ*5pK;x8lxhT<Zmfw^5U%RU$8-?ud~W(BI=Jxv*jQlcb-MM(!a(Y8kbR_P-Ds z6Cf?A{@s5#os_W$t*~K!BJK92KHbE3;`enKdo&?xDd0!@NA5{|x>K(49rHdXaP06} zhZ+4Lc}h~GkE0bXES(dcHu0w*7mmjoC~4G_!;sS|krwXi zI1RZL^J#lXj!jY-=JDE)qmgDgD|q05i`kjhC%S$VTcJl;8Tw{u*V5u#SJ6w__K|VMLnor=(fgftSFN)z zXsLHL-v%}Noo}9T9G(wX#P-MF%)?{$x#b*^C8v9io=pp6_&d8sd1+|WzKlI%>ToaP zs3)(AqX4oe5l6w&k-RUt_hjYF!^yqAL>?vb>vDc1#Vv= zmco;jhWe{xDYy^){7>cK`k?sO176_kK z;wgOa`iOdj8ropThzjq^n@K?K0zFDc6yC1v_Y4vAt$!u1g1QdlD$Fo&*Thvw20c#O zbgczqlI9AeHFr5Bn-)`o`w-c@34?JfO-kHK8fFX!Vk#rfQIPDk2sN{l(jp9ZuN$m+ zbESA-WMZ!J;7G9YZM$%0IA#J%|D^KlqWCA}7xmjvN2OcyWoigZ#uda$x4x4IkCFp| zfi?l|=`_@Yi{Zyd-ssTOp^lD8l%m1cy7k?ekW?#`jF%knS5WRI_!e{Xe z%TDUhZ5W1L8<{B z-*yf$=)MYr$x#1W-I|>V++l1Alk{R(3zi-F5@xI~#J<#GJy0WP!tYTB)aW{==CM>w zpcw9+JO}i)2>t9@Txx%z=-83eh&I#yNM8L5v8QybzXmX)(yIJi2wLC+Zn*gGW*%=> z;Is320I50~MsSl!YKt;;x$ykOD9$p2T(RwmWSS8|3qK;uE>aGl97#l-?WkC6L-1s1+SubVvLhKdBN3x-RIT*sNDO;MbGd?9vJZRJt& z=JJh%gYpKA@}9XL5DWBE|y! z1N|Y^DgNxg06yevHq<$L<|=z{hUj_D8a9hP?>QsV;V$8otVOp^lB-?ahjJdyA;yR0@<9o{CJ_ zW5B~6i>S)5wL@O+w<})m?S|BAMtulIJ#)>dZ#hR=?@oK9=+s9ay#>E-)Ypx;3G~?M z@cTCEvxJbsqJ@-{-G1#jLAp|{-o$S#4e;A9N0P}`_}3zkTwo>ZQA$q6F~lYJ%<#SEtQ-nq+@d!AZ`IRJYGq_0U(Mwy6y*>$}I z_YV1af9zZ#(Z&4isVCS^ z6Y4#w#IXU$HM*EdU7=XBNbN?Y-E;UM@8`(bb-fd7m|evnQNRWfmx1hG7XPb_hhJXt zb)Ij~Uqd~rA5l+=v@6Peo;`y&M)BX>Mu}lTV$T3;b9N;5^q4O1={cB?yDA#fvSe&J z%HAo%B6QJxitN28zH3_o@;4nQd02!gLpgj=e5a@c5#3m7QC-3fq8t)p5^pc5(>kPK`0j65(+T?op?c_BfyCafZU$ewUUCkE>}u=-BMG z*1NP%z*F1@@@@NaUW2oTHaY`m4Vy$~5pX$W)%p2uTC@e8;rwMJ_}8;ncBK7y{mBI5 z?LSv*_^;%TlJ-Zw48H|DbiHc_k1{DS_Pk0Y(8-y5A`@^O@547lmdh87$ES-4%VmhN zQl#}Ft{#6PyZpu71$y*@{+dYY_eb78yjE-&sdWMGSzF+01B@W|{Y3T?lu*c{c0^t= zqkd@-Ik)Bvnu95FMm8BtGl?~m+0g74MBJL>(B zU6(xK4*lU6)qG1XilLpkJeG1=z%PE9`@wE}KGsENVlI4d7rkal(Q8haZxZRHzXvJ6 z|DfViYk%Ycvq4Ngju>nbs}2vXtbz`531wKqAG+qLPQV9Z0NVWFxRrQH(z0oPWHl#= zb9dB7nrs6iIlMnoUDPOS*JlAd_0R)N;#^w=PhuCeTujn&v2O0G$kKX9$BE^u$dV5s zogsk3J&|m9;|XKkUqya%Dan6I*7_b1nKSz%zp{ah3h$5H1$ZRkX7QFww}`jwNDXy% zv3gz1qtiU4jd`Ga52qJ=`lJBBW>4}rfrZ<+F;q>n4y;12-=!n(jk>i!U{U$rEH$bQiEQg2fg5woZh5)$|SKu!%EiwiTtSPnN0~0$q zxSlf_wSTrK1-LAo4x`oF7yKoz#mqoi2r}^vkd<%aeuP!e{7zTCn^V@EQEs9f{rs7l zn>ID}->@(Gumji6;kWJ7M{{p_w&p)juHJzB616*QhY_Uxqd?zC7YNIB(Ef#B_vL`r zxGxOQlIPdpKE79c%!KmrRyM5zt{IjBfsdU3i&36KU0^ln zW0W?DYs!uRH)TLGD&Pym>x+Z)6z=+mFKm8g%XbNUZF-rEB{_fvZV(G*9M>>j%|loy=F+cVflFZFw=Yv@QRS#a zdy378_Sgs8L!mvEpKZ^}=x-GucrWPwZhz!HZu~X8W9Odr&si^?`7WQ1)7vWH zU9vwi+N{v%OgqSB6^wbHA>a(G4c96Q2cK=kJq19&UBNp*&JMdqEBepX3E!tzv?8sL z*sBC%{q4CQ>{s;Z6)T><33x;+@Pe5uR&0H{zOHU2@Q0Z-^C{^o`>CH)l=oA9wWH}& zmp0p2%lW^O+l_Blt#_I76ViT_p|BbxBFBLhsPAXx^6;N^4r!Rrq?q6kYFH;P?q*Q&-9ME|H?|&(to{}fAO<>5pVYanzc&He8m(b1fr zp`oOOgZP?X40!5^6jr}km4K(-S03JB_)T~xUBV;A5L-K#c@tUNxW8$I4@s7UHbp=~ z8%gi*iUWcX^bXDh7*dnoz&gMdxtCuNcOZQ%9-=06TLXM19R+;G;^3^f27HbHIv?bYYnk^gbTd|_6YjPMHzs+ z1Mxhu6F5pyYJ}S0EtLAFqKUxskOPB^Q6%j@zC5aE2E|2qw-DuS(HCd`4Ga!Z-24=2 zeuNd?A)m{ZhT(n|w6Iiv`Gt%cuAlVass@x&L9Fm(X^NB+Ee}`gaTW*9>}L4y>%f}! zM;?N{KVE@fli2qf%z>qoSdJRnx&TjIvb_zzTVGvl#qV>o;cJXSYdX~HUt0=2m>b6D zr99+ZlxwYzkopv9f`~J8hhi3zYlSZq%eFqE1Iz0JGx|WrxjLk(wO|9D02>7NgQ6tZ z5z%p3nY$vhKziS8o|uW&8e3@U^*x-Jtwp=-8=S4>(CS00@gDKu_>+Sr)DEs7<7{|G z5O-5FOxDF3bf?|>gTn(DF&2Cq`evCd=|(ooU0nz`zivZYmKfRp>h1a58h zh)+MX${08*&Q^2?)u$PCM#&VN|IO>Mftxb+m^*9$b1qM;rYw3r?g+w_@F6{f>s%yT z?d()WC{dv|EcALW21zKHPmYZ?mS5Pu*4X>vox0d?VwAoU>Ge4s2p6>iJQWp+=c7D1 z%@hwN_xgVQw%n%42!X8nVSop5^OodD= zjad>e<}M6)981s}Z}i*Jp6Is|7AU@06CO(EN59K;#>;KXq8F_H2>PJ}tefP#Ex(w6Rp5Bm`M?>4N* zF7Eg<)&upe?UWb#xqhnEgtWp=g0sKKkxsWyi8F5$@VQyf3p-g0&{=hNlQ@Sf?!Hd{ zs_5uIdik!?qSUcQ;ne>jCXaj&#u|(<#e)6(>Ift7=3DJ!vPr+`Z>O%L<<(m%K8=@Ll(BB9H5FcA+v+(~F}%1L4PABeNYRG#_VHZ!-XiMiGebwkDlR>M zSivt&v|bQP598gbu9QPy$JgV$a`mO*b=V#}@l@T;G!ENGVB`$P$i{wRQH7|TPIb6P zx8r`jIkF|x*8wnannSers?mp+P2Xir2a}YSGnggkOyB`IRi?T=Afm z3>qxgbZuk+qqIKKDbHv_{Fyq21w4wEHj69u8V`3Ehg8{D89WHIcJ%8C(%Ge18gORR znRz_jyv^CzI+6E}YC=yhynl4SdAc=|*ZYxfgxSKJ-?=nTKHwU@JAGGLP*r=bv}^Wy z*QKi8q1U1|0-Ag3Uf=ChI7w4#P5vxPvfmg=-HUz~9p~q6{HinvU#)kkYKb?+Z-BQZ zKNtALO9_7isu3^5-XVrUwO=3jkasu@{!GgH^`Yc|y4QAJ>RyyGn_V!Q~&91SDU2i5v&+}B|UaJYUXY^D{wQQ9FMbxu<}iRexf%Te9} z{MqKD#14s&4evA%bzOt7m4IHEt$3g!M9>&e=fOC#TR3*wm0e-zed-Z+T04Ae1t?UY z1u}Nm*Co`|0QEgeuJt@+3UmULG`Fq;bz@UK)-s;2pW(AarB~SH^ z_GI%?aMM{FEr|ED;9I3{&mLZ%7}p;I4lh;dya_y;YSRGEDgNVC#?bMq1=WBN&Y>R!Y%YX?}y?~feViDSgz50GTPA(Rr(_u3vv-HV)b zvnx5kx;6sFH@WAxo1PxQ>jS6cxosJ7GhkbzJp>j|iX?U=X8pc#15GXQrXAnKsC%P-cs);(n`pHX`{dE4ug{ z`u1M&$5k{zO-$;a8?mi%3dHWa0WtNzLTnsBtXhE>r_e*ezajUQ%1xk68Ufq7R~ow+ z55`hd!5q_T5jJ{roCjyCE?{C*0laH!QN{6v_&oAd=uw4)3Zo-Neyl|UGkd(-;~d?D zbM*G2s27@AhEir+Q#dzZY&7=GHVZ9wBsvZ$F%fIw??gHqC4<{#f58a85+foGyNtAZ zXhpS{gVxwgv=>K*7NZ_$L4O#cN4qA-M;6>gfbmut>kyBkRVaH?riBwE*QG{R`OR9zONs`+e}Wj(GGFw=gLK60&>UK+PfdU=HC$1l4i+X$qm zF-;M<^2HGxa4WBroqtktxQN8%gOoTM?HwR@yYS7^O}A7Gt0IN&g*J!B{=V-7^-2!Y zx$u1ViDu%vekgQ)?$Y@)6}brJBFISZH9&he&tS-7aLt2j{v8ABWKpHpOjmd+^ka9tCNtHmWyQ@^}`e8ngp%S?iHsub-dE3B~)`CtU?~$ylnADg?_7Fffi?5`t0zDByqbEGiz^ms}a4tZO8VC@g|I%gbK;wsYGof4uJ1A~v^PB`9z{(7?&D!*#GA$d z#8ndRMr2%D#W!406~hMElhCV$eHwUW6!OaN(5L?0zjoEkh{)*foc=Xgz`geo|Eel0 z75fsd-@_L>YcPrhy;SkJ zzhQ&g?!}$I2CCY_qSwBl@6pTE;%rM^>rOR#=f;j?%eh3O$e4^&6Wqx%d$80aksZ^S zOmL@)?9oF?y(fc`q{zDV9Ts%}*NM0)W};wus2+7+T>0hpU#{oV8y`W3~RP@=|n$2&BV+STRvH-r4FXyJxS zSocef0(}r|*_q6Y3~0}0xHAFt{?yD>2&udeF${MxlFT76`kFppjQ{?0+{8h9(gxPe z9e>zD?YT2ns*Y^JsAmT%sg9CDO;AwB(!5qDPYVC)F;jMri89Y-Q?l0}-y?Cs?c4%d zDS|8uvtYZF3KWA2ZqwjAMZXiA!+o;0?rFtSfhjY{>y`KLop2$Sdh@7M<3x%s(D_-lpsCR?fF4>zwM zLkq7E-_fZIIXi_R7#AY%>h+C7S~=t|ma+8=dTNDu4(0VYku@PNx$H#h#z6|#Y$Epz z+HjTot?;`C(&)JrNyGDyk^AOz7ch{WI;2tm59-&wJT> z+w7#)?;dHRYQVxsO;H=rT_OK58;Ha7_9-2k%n){R#HM|-U7H!Vuu(ORxk$#wDY5Yuyn+!|>e}x% zkFu@kZWr`)3dN|Y1?}JQ+#TJ1Hjot_Tfj^`ZolTge^k7W8Ow~D3Rr>mD2~Uo_VQ#_ zOZFNA8%Je@Qw~7SH7FHEWDbtk!o03>;`+R~o&?LdAL%@bR!j#)4+P*^ioO+S`YlDg z`AwKR$IRfS{|{<46JDonmq^Oz7;W6}tetDj0qKhbp0#H;td&(7O>hfGK*lFQj(;h{ zY7N85UOiw~?{uJg8fdi@Wk0N$ne?nZ4P&HeOi(BCm;QP~P3Rx+DgB`yO7=SGr*zn$ zRy8y&rsx}RcbHql-2rVX0P3oF7ruCjo}ew4&J64gMjC_K0SlLE5WQ*kGM>ptT)9ut zGl~1?CdVGdQ{X|MEz$Bt5%f>~Orj0$&xU8Ic$qxApIOL`%*G)&}mQ%(O@o|dvBsBh6BKw#( z40H~y7H=s7{xRTRQ7k>Bi^y!WC9*TE-!kqDbGjBkbI3+r0P$biV{f92af%OLWbbjb zd4xSrbBXv_IX-qJxoose7&6+Z)jv{a1Xt;n;pSSk60%oa?mS^ z9Hbr8U(P3Zy-@ziNcp!t24&~=mA?ndH|OdpbEv}w_{d#}&6=qEl+gR}bm7w05ob$lOdU!?Q!4uglW826kDPxsb!>~|ZK)h}JBQ#%s1 zcA*}$rfTtGRaC&J?6XGy2J4i~&XM(q^>gS1dXa`2PblN4y=jHRATPb5_>+MC(m#@8 z!>sO#|Ep%um!Uo;bwy)S*yfH7-L0(_>#Cwd4uOopv*11AL6F8@IE-a_#8*yv#6xFq zE_?D73p_*1cz^y~ZPsz)2DQORhtqr7tg{XBs4EeV(G2>YboO;<%R2^n=#}xx_X#(B zkKY(%%|N?N(B=^^PVY&d>fpN(r(20__@Ued*q2#hH32zXaKSG|FL}{lEqa7~;u|J} zV83qA-{NSwq1{c56=jr39o?u4dAPYptHmuHx z8^hBTPum%dF%=7;to2oMDFMFo@5_1qKbC`KBtRJ?G@n~oLqcZtzb~OgDFLOu{xZfW zqv>iJ{~#|Qo>^FPu7w}|(4liJq>inLJv#D@;<~8vrWPG@u0=g@)$4cC zbKz8Xw7gMT7@ngad6PVbyLg2`@>sNdI0$7Q8g@AL_3y1vuJ$1)otwDu$jGDTS~SL0 zuVcA)(z$P7nNZ`0=UUi%qU9M6hX71H;Yt%tDN-pwH4r9YPQ3!ZtGLyUK?*{!%y^#$!&^pa1QM?vZZBiO6X5tVC{(Y$rDy8<9nHwHQFnY*hXS08F zjc}-`B`O(Lyf#6pDj~d5TdDSJsX^bWXnB46yM<`qN^IN$ba}K8nHLImzGE=8Jfu)U zLtykF;jpeo{VbH1$Q~-alErPQ$*0=NY~T`e+FuX_&>3S){oZ4^r*>C`{-!ZB{NaZG zcVrCDv?^nfi?KVee=Lw;w8?1?BXAc?lHenfW1%S)o=3oY*VFjsGlq(6o94S8*g zH|whYuI+^+sN!=wM&I>OM{vl+7PjS{9@UQ^S!w1R&f z_XvFi{xWxDUG?4t-wGr>*SBnp5ZpEt;}oEu>ARuj8Xb-IJxH@5^?p#R#oTPg0~N0r zOF8H#n9lWSL(8?c-||n-g{4f@(zpAcMk!Bq@bpPaUg*lM}PSN zsWH;uw?oF5QjjI18=0h~>1TXv<)!Rh!_`UmEBE)acMs=aB(ThC@#Rdk9l`kCvq#ig zum{!R0TYb%EodV_J-OeqlkM6gs`Fv&xG@q}6z^M3n=lT(lk-EJ`Ccij#DO|+KJbu| z4v7=4Nt`J^;aF(7D&9AQZg5UfdLc{H1y}z0zS`DHOEWwBtTDNuv1D7ar2{U|MDjnB zwq|}LZFQZ8S|>|Mu1+Z@b7=xAy;6klc1lhc_I6oGfBTl_LtR|FZz$d9{J_6mvFqh1 ztv}KCuA?N9vX$Wbj(kjkrk{_goB{Yb0e)J5pW1cjCFqswVwjk*hj)#Nv7GV6Bs1g8 z7}1~vo)s~7oQ&Xh9KZ(Zi`0N+!+jB!mXqJ#KI($9OVFp#QS=>f+T0>mQso20N~%Ri zCM#)Ntg_aF_OzV-wz+ODBil7u%EEU`rbsqRl%z660VcQ$OGS;TRE!el_h{scEh^{f zSSDD%u7K+0&S0wJOxQ^H)aXWUD2AElBZ*NNT+q*A9B7;A?gW0?nHH68`S_Q8FNc;Z z`>#}eaxu)RIP~rtTC_gFDycwo$sU+hS(z^ZE+$YMU;Wa^2uA@X@D|X^bT~h=P1ege z;B9s`1|Y!rFplnNjI`n9zbALr5PByhcBjq;EDQazeN!sO9H5EELbw{o#_ZpRbIndTK;$fwqz)FQU4Fi8N*) z`lVi;=lfTEIO6B^!9Pbs9t8;F{wX~1JDYufY#ygL# zI3GTeu{dG*xEJXGTI0);mkYzk;qP!>fL|e{9%!WXq4&E=9?;g$zUrR1*J<>tgMVND z8>24RxSkKFgHDzWo>$VdmAkywrMjn9lX2md8rEJF9t`*E5&&4mHABQ*AjiZd*)1&d$u7U4$ zBK0;H#{ zRC!RM;F$2rb!!0w_i$JB>lyh@fT(v(7IgyzwIP{&1ZBLn%KEScB!t6PaF+pxr)*#K zF5n$N*#vnR&a3U>5WyY#e7kjxI>dtv@n^+uNW1O(pACmNCRw>K0Nvdne zknClDkWRP$B%J|ytgKHS+u!n&BnfJ&r#JGRQ#5Q{S4V-PE#tm*Yf)M|6ymj64gq@n zp7gD%%q&TAxmKt$hO0 znMD57JsfbO3hn{JOjRP|+5l7=D0#kFI#4o3QWyP~qyd^ZTk!UI1?62hAyI{wnEwu( zL-q#oFJwsM@oZ!Z7)iMnX(<)4_rb>3m|v< zMx0?PkhTeLDZF~s07qG!uTj3wF=?cjlCHi;aa1Yi$wB4d-o;f#$`?SvDYTv`vaX#d zWoMEguiFPRai7>}84Fx28MxXMi7SEr0FRud^kQZg|0Z%_#cR6j;!)&;--yE%e2Tfs zTl?+peSOO06pV5ToD`GqP%udiuzFJQKb{1*7`RM)ts{Q!MLyBK=h?(9-AOb2+cxAp z^T8wUCf26UREIBtQu)bDxO^+L-aC+&Jow;)GkY&LPPqN>w3&Yx=^2YY`QFuIxoXC9nPFs8@fXj-RnBQxp{Vr07wj#owATfGTUSn$(Fwnn0Ga=L02V^vcu)%SZ>vtKW!!8v%3&Vda`G z?acIuarsuzA~q_Po0KAtIJHF6acN1tm}_E!WlXK~q00%BXq!+k=38o|kMfJ9?Qp+3 zbF!ob2tV$sl|C*imYx()I-d-*uU7gSymJudj0Wz%g=a4ck^Ju<|0SVT`bSBz^m5FNofM*aO;>q>$7VO@he1qt*H zQu)}c63KlSl8@8DZW@yTQp6gWrss? z&||R(S3?emc;+nqy|n!l(pI9xzcrd7u_yV%9B}ha$<^9l;=_k!NWC43i^159L=L%O zv|2Y-QhHMjnmPD`PPRqH?#)xkFDOY`atj_!UR8J@a!2?aGR!u{sN@%*+QpeJF7%N4c)#4q;dhCHvA-seK>rw?bw?B9bxCgH4F*X5nZs3 zK};}9sixB+IjiY-ZV9H5L#$hi=i#!D!Vy4x({!CF#?wTQHx#cSsq$)k!qp>&;+jzu zMnD^knJ|Ms04i)45%Fq0ZR(Vk=&*&u{*Jp8GC(T;Nn9CqO;fcvQqcghbT#g9Q2dG! zzx#bgXLkMf(eg-Ip;%A+wE5^U$pEv1y?N!{gzdokv8=V1&khIr>Lb2e?rGltrt19k zXFlkfp1SJcc}PQrFO%O@_I@h=-1b8cEL-$k3PVnU-Uk#_3;eQ3)Aiz}N^z_O`d`AC zxumDWA(n}Ng-=20iHpiiD#>WU9X#j@4Ybq6!~JBKkGSFsd_n0)251L>rItP|&ZM++ z0N=CnQCmaG3KZ6bKB=5_PAc5z3-p}7Qj8Vqk}Aa%lg*sC2%f6$#QixItcRP1k|l6@ zpwURhSZo1MF+1)x9nek?Vv5!^Ktb#n5zEAm#7I;E{T%ciu57zFv_W14G^%mq3NnU- z#&e@ZS_RixUaFjF{#$CeL#HZEE_z{;P{sVFDn;WjFp^U zE^gI z;k?MSPL$TbFMAERB3KQmc+ol z+Vo$2@9hya4OZz~Uw)J+18_Z(&qEpg`C8yk{rLiv)1R+{a{BZ24buk7k2XyoDBl42 z{qM(^ZX0;t*pNGMV&9WVlBDF~eW$eMY`-O9b;Axjul&l8%Vh9rPjtH9v`Xm$W93ty z=$!ZF$(2mv3Bc%T@uf^Y^`ehA^V5z?tZTIRaXveaxklu@qNMH*AeUo;M2m8cbv68T#a!KJf!};h$GeN+H?uzM38IcPTKu|d3sPG%mx6M<8)g}3 z+L^$)0e@9RV*+(5V4xLP+rFz zSz4Nz>tmAVgw58t^i=w2{kk>I1h2FL=9SZhr?|fuS5>ybjOJo%WosZoc-!P$2&-+kqRjHH&Qta9rw9phc;eMH)szh69Fg!d<;s#Uo1T_2DDs#Qe4RdQs$?!zGcr50!PhN*uCeYmJS!S%E57 zdArCHK$*`1Wp3~TwU{L_t$vZK@{8(IK%If|Mm3a+eAOI@Z7mnIXXi>P_!Sy*rLFLu z4&L7Zzxt}#(rfS=eQJ*MD*PJYy;tBjreT(p(mF><20EP7nkyY_oh2P;oh>~HdD|gx zTkAjCnASi0FU-+&Nx%OhyeL4DA9J7_VoWsCN2nLQv2Met_kR@QGI5O%MsQw8Z9@ z;qD%3vkB?qJd<@bxw;eeKG#g?Bg=O2SNSP0(~#bTY$4|0qtgacd3wAxJ6`57`3-re9@HG`fV{#8KATCi!nN7p3IShK^2o&VCdSt z&Jeo9C3d?lhOYe^mbTw{aN~-UZnw+PZl61*+kJQnNRrFoX;FLrX`y}Tnx*X>-y+Sf z7Iz`#0)3kilDgeR_6}q5gYEY$sTOxYf3$%6&x68T>5b-t7xw{5@`OKlD(qPI8!P$WrFUnX25Fj`e3yfE`l$M0tXW%4YaHaJ%fwjw}eOxq&o!Q@I?lYtS#;NP0Vhc><2` zg!W&)wY5K-ZtuOCj-{MG=xA5&To(@7+czqAd9srM-VD@suQ*fyJTo!f&y;DpK3I|kdN61_hi{kW!PuM!orEa?DHdnkl2UQk z*^>&LQ>QwbmUcf=1Ud=M>w=E<%RAso@rYXmLswn#vGyxRfO4Pe?4@(kfu|RszgKU^ zIa_bT8XR?7@5N}L{deay(yh>=0?=)2CA-B31k%0hEWYdL?MTmUFHQ%I+%hDvG3Mvq z3x=P2(H|4!oS!kDqUMVnx0zs9WUenwc`9*F)-ywj8fv=rd5^fNKX zR8DbJmjOg}gC+yoz3^F>V+>u-6{EyDCwW%)7J=*9zQNt?o3@Xx>fR<4j&b3>fl!u^eJD*Y_%h+ULe(7jXmOtk0k7H7d6e)aKr-K7?HVm8b#pv_B+8B3u4=M(ou;;{lY z6_6_Q!0T==TYkU^Z`Dett7@gX{936E_{FCfFBvr3W}$0UNcs?C#sYW#psZH-wX}38_}Je^@27 zd$53q_m%+Um64q)MdiE23Z&8n)#BYQw^)S~yx=LNrJJ;Fu~K<&zoMHV-QJf!i_Xr) zvT((IxA;8prVm?laR4;$fnw_iZI=v)G@x;-~$67&NU?#5K zEB;FGhmC4~c<8VX!k(v@q-~-~uoQgM!j~2<`68SWHT9;G9}Dp0$D4M>=;XFwgx_Dj zk~p*fLVTIylso&iD35pW>eMJ5ZPVXB@@)hwSU}y|s4K`;#}{=DTD04Bx2JyYBdI)YI=J z7#O05>smBJ>h%8{8)~z6D?O>4AYUzd%ShiT@>V^Oh0>7}cU*qiqJDiS{jx=6MY*fo z$@=djzaz%L+g&9OMEmoJv*Vt-KI-_*UB4su^r)`h*`w;v1i$zl8$7=uDv1qlXOrY8 z_KhIRe?+%>cU@uhJk6pLX&{I5nnElQaM#qSYVkQ!|DFV9g0>+pu=~KNQ18q$5gQ}s zHQFLkB5v~Agcey#owBy;8xpsbS=E2cVgs$|apFDa=g zL;70@u5eD05-}c7{x&Fco5+@cB!MfhMTyTPP~H<*#fF#gf)j2Q9zE`?tv zq-TEgGZSuWjP&;&Mjzrxwde$j>WuXCHY2nOU{yjwJQ>teOJ7e-*waYw%$aC2pD2Cg ziu5qC2zxdZ=yHGWl0oCf9zxGD|JRR^%F+kT53iIfycSAT<_J3|#-mQoZ!I3BPdw1W zOvLt)L}DR9l_B28ByD#-{W-Bb<<~>&>f(K!+ukIWCptI3nGjSFU+3dW>i6hhT)d3- zovO;%s6Yz_PGE#9E4%vzHC)4f^zXJm(Yfr+N1FT2or%PLQGD-v-6Lk!%`naQOvjJ| zT86N+7uPq*MPF|kX)4ZPdl)(IYTE)E*3TdhPvA4N;cvIU)7e^osrklwkIj-xjVgno=e9(op*6ZW6fP@s{Gb zuab!~)#3;fz=9Y8-3lzu04$gg!a@}+MH`>xwv}bh zgg$7rHIhoa(@=e3)=lupz}8LI_ZYeWCi(q4{$>B?H#~RyaLa8SOh5UjcX*>iL-jka zL=$XhUB?@4brsCZcK}|m7$^&E<2=(=!uYSN7Vj#<^B}6lJFD_qo!Y`|^j$+Z=5@Mr z(`>48A!sA4hYdOb{}_*^!bXTk9hlY!<1Nm|>!T{rI;!g-vIeNbjY!9{26>d6kX#@e zx&~}hS$#ICrxlx&PW~*&q+d|>FsmyyReUGT43Z7v4CZc{koMaF;mTp5n*&7!T6K3#uwSd-X1wNSAcawS3qay-U7w$iYIbiw0ud31o9;B1IOI0gOn zL_XxMh>YDe94Qz-CDK^y-@VctTw7F9|7dzkvR$hMsXSu)$ty!k7sfEl6?kbEMkf+< z@`RR1+%lI1&x?AiFJ_rBazd0TOESRwE>l0Y5Iny_Dzu~m?T5JsvY*Wq!I2})Z!1xt zp&w;Gu~tp6A7Ki-ToTEs6~nXA)5PI}Lv`?=R9{M>u9AIBB%cKS_t zG8gfme#rB6AGNLP_@xx%>wVqA?gKa+{yI$D0Et$8aE=B?v|0?60W7Xni*f_Pql}W2 zb76hmxfbTT=m54erv~GxF_UpO!*VB)w2H0&zw6_>IVE;S|H)l3jbDd35=(zeRST_% zWfsNfP}v!W{rb_5CmO7vo^E3Och5WgIPVK2EkI|U&?&i;xVi5E{PQ3+TS@)7sSBW8 zD;4&^zpzSye-)0Jb^!hc1^!LY13YIMN>j$+<5~FaqmG-$BDxCoL*}NjSZPW7m$7)X z8KH^MEkk_~WJGedi{{!6WI^J>j!OYeVv%JDvAteG1kv;AR^tAs5dba-qdc>Rv(2MQntr?EU zGmNc@&!1J+VTp<;el?7e#&53p3ncz32V*fRXGf_l%q2255%*TD2fnC5$&j?Nnh$&Dk5%oP zhzu{p!KO_Nz;Z+OL(~*(;Y8_Fe_~cb6;Z_uA9z@f1H*)k)xN z?h&AIsh(AQ7_u_G%Ic%7Z$&Q460ykLv|8pG{LC@1y5+FEgt#&7EA-$M)D=FI)smF6 z;Z$dQDoUPZ(r`~YjEooOa!t^!t`?s(!Iin4FnibvW}gz#%!8If74BLT7NPc*pCYM? z`u&bH4H`+E&)g0en{Rcr`>Wtxl&0ChZYPm1y4Ta)&Jc}l(Dbn0a^-8U^ilqaKA#%( zCB%KJW0o`I_XVDUFWz&jE9Ov?MzkOy2@!0_KNMmJd{?CDA;(en&-LX}X1uNupj0x> z!Au_`MU_B{lof%FhJu8roTH&e?eWj>e9e*-Qk2l|X%hp|^xsM*Ir{y5Fw$U7QHbKs zzID!K|8AOEi#zUP0-sTm+Kv4w!Svu7&j-#=)(6jh{=R7JH=vf@Qr zwB2cF!WJJ55(4eWXQqvn)Fn{>uPu<8N=j9U1 z;AcyP{x!FyctVC?buhPI@Q-aq4_mH67vgkCjbKEJNQK+^MCY}mnn8Qt?)}O=`FbkT zz$8Tl3S^b4;h5XRCb{|C;&{pz8)6?gLz!q3;@sp@A7jipB&;O_+ zXj}jzYYo)>c$(mv9po;6+*pi&qxjJl7w6zJRq8GN(?Q1Qxcd2m(;Z70H^Dbb``#E_ z7Sosa&!uJc<=@cBwgIO1P9em^?cEp|N@&u^ z1?qzISbtt;@gq~C7WwiznVqAFcZS468lo>NOfO!r-MhC*SC52i2 zn!p3CpROzP=d~_2uu81cm!ajhj-IUD&fi{wa+t(0+lG5=GFK8yg70vUFtG(IYL`$8 zeOHOncREZCOzvh^aT!eJF_!=uw$S#2}gozw#=T`c-Vn{!_oCq=)CN`_TiV<^!FOE4X*&EYtz@9Cu7_2 z?8v$_$2#W}THC}2wr^s`FrZ~X3Hn6$PNzoMiE5?lzPWHD$Qanf!w#+-`#7?ht7Ow0 zpTqbX&Vi(K5Fm-Idj#QCaD4)M9%0EVEnZa*Z8YY|EUyWcYK{bX{ULbgg!8jTKB!?n zaI7(DL#!5eC^z|=pe&Mp`U2w1QQCT$+wZ2S6sYsF8B{A@XxmPUvuo_Nes+S5($Cp_ z+6nYLQ$@yZQuaU{p+{-`>~QxxxI0SYC$J%-+6n1L z=o-i|L)zSzUIFP;NL%~TzlF3F(zd?zZy?Qv=J%!M1^Pz<`a@PWv-_q{w)s`Scm}je zZ*crT8&c|3mBtBKa5oz6;wRuKvzUs9lo?WHB^3*)R7hEslmIC!q-;t`1*!QNcz41H z^A2Du1h^p8hqAf-n96XB53@n$wjMuV^yQTBEk`e}#@b{pF z45uFic*=~OALf2NWYh6bx|a3b1-~1#d%~;-IU4U{Xl^0iv72}wJ2pL&SXiHZ)3MNc zcJn5C?XV;{#juA-irWJeBy$IKfZqEwT@>bH1o`>)&D4wb$i@U8S|m3+_2_eM-R$J+ zcYSxN>)Hccz5QySV6)REZ{IxnT3Hu9O#>YHAjr^R=C|NmgNJW;3*OW#Z*H^e{V`^3 zAWE@tZnLZXL(FU-+DrnmW`ifL{F7s{Su4L7j(utJFc|B^n2y(3LaTV$w@?({AD$taCqak(r#l^XI(#5lY zB5%O>N52l_=iWDKljCBTGh;NQ8mk~Dv@_=6mEVNtjJ**d%LLM(VURSam0R2A<<3*S zFefMP|K`YkqArpq(i#UoO|^ta=7cnx-(aIkRjyk5$b^~kvUP9^doy}Y5M?tlbF#Y)#rKa$r10;2*hsZ}Zt9zinz9*l;7sDX; z;O0$<(6jY`nTd=!e&4X9p_QCx5!L)L0Z)ioLFA8n%+SJjrMTzey<&`Doe1OnS!=9k zlNs;Mv}!7w!i&93d2A(=VkG8amG@CY71_(d*u2(*P<>uP%X1TaTP?C^u+bvTdoq~fBbr{9)6F)@A)o5Hp1@-_>F_-xQ_&V z)^Kg3DeUkzg>&op7j&T-9rhScy2KL$kARH#u6vkS`@Ft~{KA;={6rEoFk9lMQ05vR zQeESJ60Vpq1XhW<#y)HNGSA^=9_Q*L#$&S=Wn^S~G^FT~=&lLqKK&4T|Bd zHL2y1GqW?%Us)wF`JBXLmWoV%Dde^JKMm@Q=Kv1+9--$Pz#^}e439*gQw0o_WzLzM ztKU@9A95E^v~_NBM0@6kEo7TrZdm8gR$-smV5v$S)G@=VhCY9S_T@0@pq9lpV|x?m z31}_w6g{>#S>kbK<@c|5VB4Yh&=zLOHv3opzc!~*Hod~fnxXg8Vm-th+n`XLpyR8IZr^SH`kFAS|3?m#S12e)u+a0`K!vuZq}N+hQBN{pK&*rRJu&Q%gD zQ**)X8pIQ2_1%JJYwrap-swOJfqS90Mb2`-(<=qgd4vG;Mpub6&bYVK9sgpUP!IHLA=+px5$PvHgcL@TTAywjcDO*NK@) ztMD<2eED%n{f@ic=+Os0m2|qO3ceCs#aMUP3#0=&A|2Sbrk?Z&p(syXkQm|3I+8?~ zxN?#h>oH=iv?_h2(S&mvHD5zTSgO;N+RsU5{Puq?C{Wgy!*}%nXQNP|D=)R5^zn4F zJ!a1k&({qxFT_^763WvKtxUyJti4zg=)_!HMXCeuH9@z4JDE=b#l)4izeshh3))8M zv_SD9yRB6*k4pu*;=WT8%o1;!rs&OhH$R;puqGpn?~u%mU;2ksFjI!@-)2AU-`}jQ z5aiu$L*ehvHY?DH9c`M5Z^TI>us3(LWsD0;Hr*%C8?-|Ua#4_K_r&ezPI|$i8A~>1xH8|mx$vv3F^MH z%{<& z7-h`Iw=rhmFKv-E0PoBv%b%`(ZI-VuzSzCC2Z651AwvH557&EV#QfY_40>ky@2~eR zg{vE`ahtC9p6n##dOIPrO0M_*^Zx2>Yl!C_vMEEY|tJ(_DUQ&roYs9{e7o%DIUQ>pjhY5`LfZzIXXg1(FK z*~@5@Cn~@LE+2MWcBmZ7X_oUy{mYa|?`$MyE*-y)sv;@I?7FCte$Dg6bQFECJ35M+ zUf*lA$n6JW>w7Z*(E}10E^xi3DIr384viUE4=)( z!X9IJv`24#yYP;cL&`B)(chrf!fdSfSLK5IMJtX-aVZII zHnhubZEzd#ltLbNA+nyl)@_C%l~@9<>%(QyfwulMaTCsiXNkB0`-wTqqk2{cIL1}- z4&D=0sRnF|Dp&0*wWF12NJUdP-piz#8yVbdCp3l4by0yKO2m=%4m>Srvoo%qN$+aB zmp(|xM>eFT-6KWP#5x0qiIN$ z3dT=c!CG)Nl6OR7tW9~Vq;mD05$^h5{|n+U*H)!1Oy-&9xC&e++~WMA0cdjSPmQq^ zBxy+HBH#qm59DSWE8#oMh(HQRHA8EX@!X%_W}*phW;z_qMBIyl`{^c2ES^(=I{v~OgiqdZfeB8BoTAw19Y^(QaumShPRE$M~fK~)Ty2qNzuI{qCk!~ z=^IWplGDXZZ}rzT|El7aL32n`5#P6>$NKtr-)iMQzO9v~w?_BPBmJOxRM>*^=yxrO z@2zr{3z`4doQW`>_D22OyS#;v-?b9**NxYEQBT|K?LYm2DL*|bHpIqopL4VjWZdQA zqFUhEm3mjde9!q<(A~5$X(bgA4*)#FP%FDzVeUH?S+6t`U-$|k7q3L@y>r9duB~2< z-ky64@UKmdTF{V?W~-DliBZo2=^|s7U5EP7f?f8g`s@H|Pb!o(mh7@m_sgS<&?40A zd#zIfY>ieniF_?d)CJhS7O);Gv|vUn3oRg_MGUs*{1pb?;05BeKD_me%6hwK7Xol`#YTW_=t+274N5 z*X9-*wRf1;ktC1@S`QtwU7eAn)e`f9?7&j3{==3S=G*2y(EmX54H#_pM$`0JLly0^ zAJ+Z7-_h6m9ngCr;2r4w4!g?V0^^~IQC}qE6MP5(bHUhvTN#_ZZ2$$c5V$c8sgV0_ zOJp1b`JO8UwRjGi2557|6*4~FXD;y8qO@`66)u3~t(7&zT!7=t59%j7&`(2)mC7)l zJE0%p%Qx)zufTY|mWwA!ZgQYy6~{Bh^Q((rhIuzh+eZyF-bXB>{DH!LJK-=oV0T<+ zqr5y+w9uz&u|Fx5dk57$#@o*y>aU<4(Bvl*u&i7BWDFU5ps)tGFlcAPNFqrp&ejFp zDl>5B=x&nQa6#jsPOW*PTT(lIZKq~iB;v!cqm0Q}lW zaDg$(^KBtw-NttPA;1HVu4c-n*FxJ=81rNS&0NWPHmy`Ccv;lGnHEOn$*g{nV$nuj z{k99b3hdvhm6{+?Pp;=HwEJwwNUG*Sg_CPMunO7;Sg~*r1aF4l#x|1nMCSu1EX2{Ius0wUP!7@!n;_?r}*17}B z=GI4ttV*dQ`R#6E?kjsp8og2S-Ac)cZ{F6;j?g$Ts&%yEvOgnmGN{rFsVC+O;am^5 zha|#x1tbY1F=D3o(eD5ni`%vPE>MJiTGN5TsLCbFinG;~2baCqZ8bBA$x0jW?7=kJ zU)Lz8Yq9UcaIV`%n=g~GCs)3@d|K^`gGckYIGK7$3c+Yd;GVy%? z`1;r&oPEt(O1}x`>I@Viy2%m2O3*3_t0+p4Df+L%dh%5Z`7kHEd^|uqbh1#0WrO|= z-rzrC*am=>rsKORBVc_Xr)6aFStC4KmlH0Wlh?XT#{{!@oAad44YFBIc=hw@OJ9X; z1zyOZbXkB*D5(FcC7R2iIpM8(3x#JSC;XWHwa{Ytb$^-@-n3XNYp#41o(}09t8o=< zn;UUpr`;yM?A`&q_@Mi%u!E4Kh4Ah|_=conOKkFo?uFsvgs;LpNt9JgZg!Nl&sR?M z0^RIg7=CPV0$mv1n2<^jDH;&Kmd&LkvG=F-y{y;iBtIT;zZOb`dY{uX<_VO+NXUFn z_<%l{_Sc0q3Exs^_jKFP<4~V|yFIN5eixGzFVvDsk3!j2EStl!Nzz@k_a{4)Pg2KS zfVLT@n#cr#{zk!4;qoO)=YEGRF3!fD=58(}^8u&BqW!yD8t!+_ZX#xY&bv7+Nd#o! zq=k6)Q!=H*Jh(k`0vc-h&PowVBzdPj#m^*a0KNwjpf?QkCIQ+312>$(E~6i{PKA84z>0BK(f%c@qeT{oHOe2+$O{yZII|9xZ|0= z!wxvPn)5G$d9k|%-dGK7EynX%i?b2Wc=?_dA@!n9Fn(d-c$AYfgBl9-&tQZ9AezJpVZW-_8FdoB%D{xihbo0)Cx1LM*(ZCt)v8Caj{7 zSz&;2D$pa=VC$J4(k%pM2@7FXZE+lTPH6ZlJf3Xz!rVcsWO^-Bz@YW*ooi3acv@(- z7P+9gg#+AcKnSfqsjT)4QN}qZT(Fw-Xu6SWMK$6$Wo0Aoood53t@M-Ta3Mx#&e9@O zcpHE(st zoe8D(k$$8g`b1tnAfg6vy%f?K4(6~G`m|TXGOZLe<8{1TEJ8oVZ^51<;7R1M0sNLp zWKwu}vlyw}rpKq)e<6N&<}1PPRJ>zhc+q&#WS-`QUY+rg1NCK)^6Y{ZKBgyQD~@9gCXg3-kVr}D zr~a+jH$C(R>e>Mi+XRq00`qU{bjGoWvU`CVwsC;t%h^TLcGU-MyV6Edy?$maV~!3) zN+qd4AIuRxg7kZac1_6(p5i+9))K^jZ&DSRk(gulN}D1-2F1=RCIXTh7; zvz^eQ@i;rR&==qZxKhW}sb^BT#z+lB z?siA!IWe=IyKwXuEQEQ35X1d<3|$26Tn?pTdDvQPmiB_qg7cp%BBMbqVQ&u9X;w8Be^}0b>N?I%gJ8loR7gD}%I7 z$ZLH{qY0oE%}wSeo^=v=w;R3Qx*E}TuCHH5&2&h_i~BY_XQA8wqqCS^t|u_UnjR9u zHQHs1m}z&-&v0DxH#FZ&ZIC`isPGNiGU+m6(!1-h)x2B>qq}i2_Vvl5H;?0qAm%=H z6vuJv92gaV><$GxlPTgRjKaN<@q9ISGluR}zI@;)Qfhz{j6~t|uUc5S8wbe)PjVI-HT?6vN ztzNeLJ}Boz0`y6Z^xhHx4jh8JcbT+X8o4y|?M?E*8`EC`P2D)I@I2hF)&)1KG{Mc9 zGSgjQZZ?xi;cqw0CA}F++=DH_@^pQz0Z2^87F9!wz6z%jc>XR@Nb~W?X%p$DZ^X0_ z{jm_y&Vv?PwDs_^p%veRHk;RHGZtNAf5e7;xVJ+&bKrGlZwFAK>18s*Mkx651TyZE z@N_rKkmOt&(iW5Lld!|grk(T=;QDtHl;B8eu1`as`T@PFuvh3+wU`RFA#^!n-R)Gt zx^nvyo!j2@fDYJx+qGZp6E342cUt-|OQ!HWA>TKF(%DUn=-qJUSWHty*BYhhN=$IR zu{fu0kVb|6CRwozP-mj(V{>iAIb@R!H6yYJW=Z69>Kit`&E75aUS+=x)3?_qS0C5- zb72B4uVpO8#y`BxZ)yY`)Cjt-u3nWI?Pn}8{It+7rkU%8_3GO7R7h$oYS1k=T&XW$-`cigmE zOe}+|3a-N#rxSfT|G#>sBlo#ELTE~u)qUx=QRLszt{ZEyuZ3*NeiMF>?Uk34S`^>D z0iW>uF9K`+k`Tu7u3}>-&cW!(9~SbP5VjhCtp;GLQDEzRj~T-S*fA_X65y!jX(CWz zxxFxZcp?{cuvq{*j9!LsqHQU1<^uX-=o(S#R(t^*+;af4&$RO00H^x*uulBl?Cp;# zzuEh@7B+b+Eyz81_0YPpX%Ra=RzZ&5m@|-bpo?V1cDZLh@cE}O zbEhk(Md?FUH8DR(KZSYZ?2Z=FXcqkC3*!6e*3cH~a9*wtVr(UBl4%FZ|ekq)5Gz_HYLps%{8Aux;eYcS*Am%ZBd2HbawU-pGj241%eysQO zx}gJ4UA36=zw2}ZIgEw<3L(q|@X2S;#t*JAmBJt_Q7k^n8SSi>_Z8>xvhm8La0)RN ze9)Z#1tFTh6A~rg?_<*X@q^tIC#zUanYAiQf{%q?Ce5IXyAAPzL~w{n6B<>UhR7u1umeORQNboL&T0=SgI2C_CW@=Pw44g*{|D-{4PVN}1txO9Uy80c*)If`qN zcustjEVTlL1#n+NMkD%Q0+!X~wYqWd4SG*&H39T~ z=1Hbp`#>sJiqE}uAZ3$=4Se_B6|XHy{_Xq3cl81x_dRgE_t<^cdvE>i_1=ud*L$CV zbW8sA-uDasAJV=Bys0W%c<+;wG)+rMd09%uqz@)dfl13_5K+_9WK$~8!k{AKI4KlN zpJ@@j$~c;q$`6QI5a$Bp43$AeL`-=&MR8IT`4PoQ04uiWM8~mhLei5!>nU~Yzt%|$ z#qr+%1>Z@}WAC-!Yp=ETdVD`n4)1*M+yUPW@NVe?-w%8Y?+W1lC|reb|2x}|40_B@ z%AnQaZhuq;J>o|)=os;@q2V&<81a*Clzmzv7mC%EZ(+|`bC!@La1GZpTIb@~=KQe? z<A!VQ;1=WL0}Vi#`_Vmv0`7zSmr%eM%b-cDH(op$ceJ!CoNQ5bKzz!`7$(7hYj z4yfruKgy-!eRQx^w7~$is_UkMwW1cRWqlA^HOpTu%RjjtWy9p3Lq1Al#G+7Mq@tP+ z5DnRmdHJxKLoQiiDz-&i%#v(LM4$2CX^9k_kPf>}NDa97fHx=rkaG~aciE8(4|SuQ)##oZkv=5%Xi>336yX;8`? zBbvIuE;9l4*jE!H8oL3K&eD7Cm*`*jcx1T({pu?FfoFIL@Z9BMC^arzLKpX|A}9gL z?R!pB*qf9~dYesu18(Dl)JRT9d$cs;L|5v#Bz;lh32CpE1kP6HMmA)hkoJ=#c=MvR zP<&z$37o4w4LBKi*j(F#b95)VGVVShRW$6Acx{Z>+YQu3F5yl~CM|L)rzM>h>ZUyn zyEPv^Movp}iqxX<648BCbwX;<=6788KjOTD5c4gB;NLN@5$-LHo>0dQ8ucZ8KQJ(K zErP9vhk5k4_nJI0uFf@WA(7wN2Z{KHK zip|t$Y>)3(^!(MnE0_iNbaO&HEA&4V8yi@!Qd!30Xou6T_;O!u>kNnuWv?mY&5qQ`PB?+PYrXgcx+P!?KG zB=9~u9pj#qShXH%l-jGI7sko9%}JIQ{x>@d_WO_&R961X8Wf}T6gFS_KcgWZ9;{Jro7Zdrwxreq3TpP|#&%3Z5r@thO z@D^S7 z>Bpz#jqnUL%8N!TYf6L#yJ!m4zXpf5WD8#$a>9tWIdEyAH;}l zTv@>vO`#8rO`($*1$y?b;Ln#~47_O}Wm!|`c=C{EKH1`B_PUU&ASa}RIxEW19k1;+_s|0D+sLcfW@w@3Si)N!ARLy69KK5h% zV$IVY*!}-5RU{x!0{iR3k6QSEQe%#MpMSzi>{S?%eFk!x^HEAeP-1^tvX5+b`xrGI zd~mNDF~`_8j5+al$uKpFx9fm{Hn3O2`7)=a8D`lZBUbn|UY7&+O9GtZ!GL)r2lOvh ze$kQJd%mb<)mK$Ii>buitKf9@EyA=1(N;LgUF^_V!X>YU8@XaT;;g0;luiL%>pCy( zEi$dT@?eZ;4_tA)Zk;P_ezDp8Yu2;MS9VfzHmVFi{{l)gK|5~6w%|HG1iMsAq}{1^ z9CR$oqnX(-OW!fR&sJAqe7b{jjNYi_QG&yM;{yyo#$gFZ&-t!NR|;k`-btw{z( z8h5QUtt7hB($e2(dY3+SLfWi-pNY#`rAHfo@a+oj*1XRUVyjfqLiHHmpb6$Ty&4}@ z$o9kU&%V*We_hmzmt5}spT~rg5@uA~D>bHk+C_@~)X7^+E03()-~#BbSHqi?a-7+N zXS(<~pLOwRXS!5M-0^|8JfIc}I@PXS!F8IOg2~tBjPOM(^pD=5>3O7LSMYb5R;k4I zBI3vXxG>rS(#)CdM=Dn{Q)L|R=nomu(2Y86gI`M9iWW2}*eu_m z-~HSFo z(Yu*pK74kxW6laMj9uQ7zPNscn4LA^qO0fkPwsU$_l$GA-fmj{WtWDpUiqa|O~jRB zebHxl(pIlL6wV>xm2n;#sHF~ZG&fH`Kb*9mIH^9o0?!}B6=rs1#e$>O$lZ(1L?f<; z3*@tMwS^BaquDF+^Zw0x?6)%@EYsCI9XD1jMDP{RG${vwviQh|ye(~YM(zb%rF4^!_FXSL` zjM_NFTts0L%Zb6vG#&*|=`aJRSwPRg`@=bu^i${I<(F3${N&hYm&2$pfl(im5A+=$ z9ruT;D{IQ^c`7kp9zom(zmzJpTZA#*`ydUB!EJK78lu}|g;@<)$Z5hpIaslS^;FK2 zbngAWFQpBx3G&H&Jz9eUjynT1-Ey1z#S2B2tKB|{73He(ODiV<>r ze|*Fg%GcP+NC;_g-uQ0lea*X}_mZ_C?QEbfmc1NMD3$$OmRFYZf|B!A#tfEngk9Ljq zYer&UHC45C-F%vQYlVb1ECco1u~qUY$zz56gr%YlC+-bs$9Z2r5&ykMpSSi@D#E&0 zrDv1%m<$E*xa(lWmy!$SUCOmL_m|Ph$?ef~72|!{s`OBu3wT&@In|B!J#CHm?tyQJ z-{!@t^IBqv=&L0<%#G((m^hw#ctDZgPxu^PTz!!T>JL`r~Pe{!r>ETsm` zMHYFmxX{0eoQ$k0mQx{=wbhjx{pwN;TDt}WI>Cqf4C}0;ArC*8M-w=GEl;hSM{eP) z>XlSy^5J~O+$wQTCyvs-fV?lyvRJj=FQvm;Rk)r*Zr$7kn8yV><;wT1Fv|+sw_DHX- zw5$|M9F>fzNMN_Ap!^`@Iqa^t><^ImudUWWLv@UnN%UV^aV0H~dkOOn^%C}260XBov9ib?OX6Wfmy%kM z(&$ONIuG}p!n{+G$@HaEPu2-1+;C46>`TD9IFnjoUXDe)jPHxG&>F>)U1p9fu8vgI z3Nb)y73MxIpdK)M*D8GDVV4@uB2`}kMUU2l>x9CKsKXHR1@uZW()E~5^@YWU_qp=7 zY~0y%h2F1h>jeBdQ{E2`^<0(H{v7GKT)Z>FY`Xw86<01}SHH#D)x$or&U)vS+YjK% z}g+0)5-YEwU+&1%5#B_IdkN>tla^HLc{jD$EUs*{+$}?u<%?L$)Ef7aca_I+0M#pNlhtv4ZOp;O;BW(A$|RR_`S zv!06LRp&@pSKMhrPJBp6!{^@*4DqP=jp(q}I6rZIMXGRyeFC*J_nxRRRRN~I1Q=rQ zlqc4EL6&eX6t)QR9Fs>7^0r(A4DCIHa~kV?Tr&BP*4ZGaytrbI;qFt7H2FoT!MPMT z9a*Ds%|XX*f&T)rVT~7400qn)aV+kSyPOQ)&#itzm?3{%?k)7^1BDwWexOwXjy>Xd zN?7bYr6mAk3GOjkOyz}A-!}4$`sfKn~E<=jpmcm{l1fu+4=dQXn!D=jCAe{56)CQ zTkTNTT~$;p?#n-k@GQL4{)Bv>UlFFrI1K+8eNgC=BRu{KQ_6C!qY58km%H^g2 zE+;3YS&chc_KKX9D)FsA#DU!D<%+QmMIerd%YmclSB9&X$7EwvvNr;hJjKpR`?ZY> zDUlD8#Z`QGDYv?$G!Hce6#dG`R^UnOU25p1?241p(#F>U8s Lx)_vZnwu>}9&L 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%0DMi9zW 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

    j<02EYrQvId7sQ!19euLerL%Z=YyXYMwBEz>^ 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

    Y@X|xh98rsG{$XEW>(>_=I>HiRj&=DhQDPR zn@Aqo#FCw3ZXkC5FQlosCGMpLh|Xg6pC?{z+8-Quk(kZiZ-Ty!UnYKTqn}T{^ydoy z{Pn*d^Yc@!#Je`RBtod#*a9~eM%Njh$nkUNbMF9za#>ky{m%u*w8CMm!kC{fDGV_*KcCD)Ca?2wL~DKf60 zT4SaPE`K>Z3*Z*Gzmbviv0Juj~r6S1xD|VfzrVK;5*c`LAyt)jJ@~+Wy7abBW^DJ!?TU z-jWi!RbcGUWT<9-ptSC+GWRT929>#kbh)U^y=b`Wlu6j*qAqN6hkM>hFcDbWX^gS< z@aFniSiR8|3*|_Z#=<4n@k&ftMUmU5^(W4D+)9fZjcj>HzziHk+_At&$ujh zPV#jkvNaGQB)_yFBJ1BAc}`SHL_Tsj;kj&&$OXAuIA@Ktf;|G_+cEl6Njy#P&9OWV zTuOsP{fBA2K1$bKy02_Y;*Z)Sf3!P^KiVz%BYaU|!5|Ea1}Cev-vxsdc4%y@A(WI~ zO)UBFMv6W4vm|XKNr^cTB*(DsfT2s*%kC#hy zO8lVA641(G-}@OGsM8O6ZlY2~1e9LeY^-pp^$c_#^2}GaPNb<2)Yl7z=WJrR)(eH_ zY~s1r3x(%wV!GA~h39OP6$;PU>68=-&)H5&3WewFiIfxy&)F_Y3WewF>nOQc90^p= zJLzu8=nEehePPw;3zv<)Ff{tYheltxeDsAs9(`f;=nGldMjbGygg>89c0x}){_n&H zaPPQeq-8|)bFIcf0YIwNNdvEgFfH>( z**B!}F$wcWnLDH+a+*ah;Q=k4kY^9BTV%exUoGz=&GOnM^X2_&c^^3@uYEFK-pk%u z6_LFYaf$0{dB0lTN3!HKN9N0WF$Q5LItAvP>i*S`3*p2{PuD??#1%59G(wXWYpUys z97UdHO`1!?_kRbvMW>v=yUvBpcWaB>d-= zDM@>wCrc{Na&hg6{VpDl&3x*L_9aR7Yt!Cmdjlf}Ml{Ye#_&h`V|$)v`dI=@|12O9 z#NK=wmXrFRKF19ul-j;_gIyTSwa?&BH)m(+k2JvKZFYtJ$VP;8Tf50BvK9tU*_?vS z6RfA&XE^h|hzv)XWqdcr;~}wM}O-R`Ke_w z<)_m1dF`tt0%^?`=}Dk0md@Gsve=|Vh1E+EmA$c1Ru;-nRJ3j56)TEzwgbO z+eTm+yi=|H-t}xarC%tE{nJMI9=mEje1n`yJ$cuQ51g`7+mRyn{(rM^r|I=f5jZER zn4{5l?XE>54T?}-Nx zcT8L9ptm*Hc+sr4+xc3D38P>?ah*ulzlP5 zwz1XE18}uRkoj}KB}aV1XT|BsVPgHxAyje~{AOAp>`j)<$FxtX;#~I?OXqU0VX?4I z7}dru!RAYgo^G8V8})&YFT{@z=@)u9jshF461Aky4J=H5nC5}+aV8Ev`;WiG%S_~P zd9KlAF!3ApSj0J4`lxldOIQU?wzM@7W(=!e80RacEH z+WAivQ?HLizwQmp(Dz)uXl|9(W{ZrFe4i5jIdMY8<}x?j^_bAm0BLk=)=w4&f-rg_ ziV1mKmZ=8`PIm-i{f{xT+Fs<~y>X?RnK9!w$CSO=(1cY^<4Iyl<7$(8rm`2de(Kix zl^}(!Ppsz4OKUJf{%+O$o*RjNb;A!haax^oTJP_f=N8BI`Y?EF9NZHxS!cIfXSj7~ zcIymRSPJtQZe6TjyO>u@>H_>jL;nunA)){Wyb$06vR}e3s;nsm*FPTp`s-KLN zd_3$pwly#hIS*5(8L^0`UQ(RH4oyY3m@}b&bm9O%aT4RXm5r{Ph70Vqzu860rYARmT*$fk>kDx)Y}h;8yREc(4IB1Th| z(y{{zUPP<$?$=JF0``Z`h|qBG(SI#Onass@`cQp$?9E5`z3_Wd)s67}*4Ob~a9aFE z8N&OZ;{~7zNM9SYRbNZx+E=GY2LUrvgi+T{J>KGBD2MP$bF$fJFQ_ z8Kf}M3Y}o|0w7mu#xx#`zMn4hda2R_u(9O`7Z}U^P{W(KIgxhg=*#+>r5EW4sJp$|zQLNo+_G;_c8`1>bTF2g7#O-EzDy*lteEB>Uxc(~X{6(lJaiqNvN80o8 zCm_zXzj6)p9(XQ;5}Nka#*@7EJ_2U5cBkpfR|`+;Lf)2ATVX*S|IOjQS^U=pTO~@_ z3k<=XKgmaNhM3wrmfXti3Z|DKo=moz{gmeQWl6m z>|j+v*lE^yXf7}$C}2_o;?yr*h1eDt6`Z=?A!9F;QFE*3#y9d&1~g!-)>xR5@|;S4 z=J%DwezHOAT3c&8fDs!Fpxw0514}i52NJB^tIlarD0hPgE(v;P_wc}>AncO5Sz(+( zQx!IwLH{7VBX{Kt)BSm4g9q3G-cEi0z!9~$UmHnX_SdHSyGa@LWnVDe95^jGQ(gP- z)~2a$eP>Z87zkP+{m~TxS*L!~yT`jnAHgpMQ153<-))pRkjSz>2<2~f?*pb!ELX`7 z$KlbkJFy>kwBALP52^m{m9wtW<{K|<^XIP6=7&{oFMYo{yIpVBcd^kSgM5AJlO5KL zGdb!OBbW#ENXQF4lQx(d4p{4ZDmJnYb>RzOpC%3nz5xFY@m#_4J)_UX2>|AJNNsFW z8$+Ju(UFNt8?7MOtm;paU7&2`T3{Lp#TMAj?06MyDss!?fox(M7H5~0m&azWAJyrP zX*cWit38=&&8xx?suZUSe~e=P@>h&$<8rHwWukp-!SIJwzou@fe*ZQlG;K}As>byf=iRkGF1t=rSIy+clBQST{d{2ow#N7#&ovnTfat%c6q(4co{%?I?g zpf=;lv*XxKC6MH0rZzdJfrnnS)At*!`Uru@5$>DdAiF632C){(p6G`zHfVQ7^dk^& zB-7ql;qgR!RpkO0S@%yl8$rBF0q3WB_fa1f>1SZ_9@L*ieflG4g4*;X91Rxj4F+-J zD_V5^=^G?OB*a4a=WvOj*F7yy@1$Krt-TXUqlxE8nCrm)3n`3N*4`WZ#8@JOW{{Ps z);I*lQ9wD^d&3y^vO(!XZRSbpq1xu%wsRZuq*aTL}if^z|c3b5eu`4k`AFcF#}u7t$2gK-)z8DeH>^UkGD7uk(3-R zd07#{1zy+`3XA}-lcNjlp9QtB4iz zO04FOv11#*Vod*H>na9dy{A|L6al+o(b;7DhO2`VRrXQFIhk<@KoQ+6+dmrTioqIa zeOYpr)>B_G{mHzY_Nd4ZI#jI<36!+S)QL{oiYJ?E?LyA-m}@gcD#v6cIqZC=G_#~x z?Wq{`e#w9>Z(wmcqJ-lVaz2}hZjOi}OOg{{w!b*Nyezgna&?2TNJ2xDDU#3- zeL*ClA<7a-Xo$WnlF$%&L=qaJzZ6Mmh_Xcz8lu0l^Cs_tl<8#Ivi2=3IMW^4HU}RI z(>4_P21hE@w%b{o`_pFAJ{@!ziI&=>E;!*v>k0&7UtcA9lBnE@`IX1mCR!D1P>p4F zFreF3viPAk_4!Y$vpa%1=c`SRhwylBgH1`HWg|tw%H!`>XLSX2eiZa~R$o)Va4HSU zgg&_#?s8#ALaUZPs#VJ$)vD!>YSr>bwQBjJTDAO9ty=!5RxN*2tCnwR)!K{fEVm1l zAzWW{;j`aF%%Rq7i^kA`YF4GyU|!Ev^e2!A>NT^WJk*9PqJK zmvqQj$5^V302c-(6!bGL<*?rAJt*VOCG^YjQT?($!p#nG$nC+@fdfh(QzEU5-$u5R z>LRLC`Vfb1TI2$HCAs0FzFGR#A*9X0Q9HYEtO0%Prej4`6Fy&*ga^-$z;pIa8HHZF`%#k>`@B3Rq z6~s3jPYr!ek4kwKGtD=fcG5YN%%2(lfU1YF_r-^&2z`y69@^)qTMJ<;QaHzklpQHM zQfWw~X;HhsGrX^M@5+gKH!btGg+8NhEw}Yfvc9S3epp$&yl#QFCFpOVRWH&qncYc; zr<~uGLM#Ev3UT%cy33^vE1|`3xN#Q)IKiy&l$pzIyX4>8VPG~qSWaH$GZES4Y$;P&Adt=+SH@0otwte$`b^qL3r%rdBnVz1RQ+3eO zJx@z{Z1bx6_AzP|T=cJ3dRKtZu(l3#^^j>~w04#Py^!t&oyvO7&I zX+9vN6STgWQ?aXnDKDwED5`o#lN_$QR5)tDH12lXlw~uK65h;b|Mh!n^=itR@`vmoZ7%MPb6VXat%G-T2DK-x#EVy5x*Tyq`v=*A%{| z&>tLZ(QiAhiR>|8K)o>%40iGM-LCu~!Plt%zF)JP6(9VWDyq)gqz^lRE)c4soHVH*P?I?vw_#+h;oA3}+$B8vubf#U&ZzS8< zqh4%WFQosRSq}$(8X5$5WxH%KNgnvi^qF%raVIO`CjMUCNw-rCjJ!U%ktW-c2qLC3 z`m#H7iS0>jxpl+HAB+*X@cYKps(i3mqjz1&A07gh?pYsV4?f=QsI=(|tBATpPu&6P z0b5_d1Ba1BB|o%Q%h(GmVjn4@E^Ugcic83U2sYDFSxW+@qPLtDzO=S5Y}g_LLO#V< zwb;kP60Tt7ET!^3i#9HsMcwaF_L#O)&bvpiCUn-hDq0D#9)s%ap~FrLhG^Ocza-jH zgN{-$M0>;!^C@*O4BdGwg6qqP6Ru3Q|3VO_Ucj;#C4f+=;*l*dU(0ym7Z;l@R?mnRaxLms4s+Uws*pq2wxIq9BDa`a$t2tF&HX=nmFT#sTf1?m-N{ zm5J#hA98KR-JajeRgApJW+d*=++B?L+HfQI4@l!ax_+$bnn*_dOfipJKfVIx^vDo` zveyjup@3Vyot{vP19kzfid4Q`mV#8sT@xYpp=*NJYt{D>g{ju>v9uhg#Q9_i=*l%m zM0mZn)H@Mb%du$;)ONFc*1Bs{)bbzEqAqo_-yBbY-4nsimH`rc6H;sgVXM!krOTaD0ZhX8Sg^ zB1nY{B{MqWA^Qz#xl%$A9L?wNV@l&<%W2F;G$M0s&r+KlS@`W@o+Ch6v;Z4Q?eAX( z$P~&3ionx#5~k6VXO2jaMU;n+V3ERh2H$B3W3rK;4o#dtd_wp6mBOvPco%5_|D78| z0qPT<_zn+$y_TH#K94{Cb|*V5*-EnHH~0!=*ecsQOz0$;&B5o|!WfQxW)VqrDma)q zaZAzbC|K$MNd6RMP~`aEt#A*n!g+H?xn!`OyBa!(mN{9>$;LKL-{>{}u|AcXcwMjI zJa%v7YSqBMbg7GPqEN_w4bzV;X(F3`pEub0M!)C1CuN+w*7-e0ZB}SnNIukxgbOogADR_i3avZ0j*4=tWVt1PN!+jkhzf2&0Uj$~a16=fQZ9EH4pu6}%CSsvKXL3L+PCK+sUpUoYwSJ;V&Te1 zoND~=7%CfzirAQ~&gyLj4VG`^i+F^ZFCbLov?_1_;!4D;T$57ZV{>j_4}5z#=gYpY z!15G}#P1n?f;*#!L??uEH)!*DMxGHxCrAxPQr2={y-a5Edr{p+;)<~947B9x>>!Xm*j1ieYbQykxM zYUKP9Je7wVJuL+PE&G(Fq&ee?34;6Snhdx;BzOo*&1@Fay>5;NQX5VVfyK7;zP{L5 z0W=v3`R<-ZP~&bts*9t5>EIfb$q0WuhHdRQax9iD9xjbUe9pR_MWT6>$hZM1#+LBb z4NVZK0Qddtlg3zg_Um<93De(+^T3ke;e#pIPy~)Mna9eXm-**0cWP#nXnW@(+*JAM zkJr_qnpUuh3EB+4Q!91i^J^3o+z{(rP)itZL=WW-^PJqq^O@g99$Vrn7_JF#EM%vP z(5-AYJ&u4}=q%BfO;*L{>K4bpsQDeqHn5k7D5=aV^^i6#T$bpMG1?r?(h~cj949NZ zMmL90I2_KB-0;B?Hzb(Mr&Cz>;~5vmj!^po)9470oOP~ejEKi(9)nyLF+_`_X}~0I zw}Sga)NTgHER;y+FapCIPlapTswhS+EmO|GTGqEv2&0rhWu?omvFm?;e#fRM<3z@4 zZ=i^(Eg{p`#gVDJ-##iMy^xx&Y-phvRVKljAGI9Cdea@xd&1V;mCid7Hv`%F6*CLT z@x}Epi1;q) zxb}}OZ+pt-Y7GJwy`oUUIaeNbSvWu@^hXvS^&~?=2?9cegVsQk4|yI&D|?jbInDF^#Cv?vInC>dfy9h8P5ea3wN#A6va?9GpFih;9MxKSAEsxm!>i9? z%FjUh4(Be zZ~zMO1#W*s-v#%TJq`YSsutXRc;EX2y$b640>o%XfbcbC^y-Jo0&L=jTf_+VVIcZ? zV?kF#WM5e&hKH*%z?-4x8FZ ztYBh0`YWvyD^Mxu8|SrC{xfhEb+PxQv+et3d+NG~?gcP#UG@65ow_ac{k9#+(_ZiU zuNW~8%CZGYtF}10oM-(3FH;j)+%n&=}HhUAf`J7(Y#DG^C2j=&J zz{k8gBaaOdE}BeqS}G)SX$j^ZOTiD&hD6a zXKuFIKZ&zejxA;5_0A)kRs*g&+1|gIxl9!mloW2+%=X-f|2|MxZQp;kdvjMbYa}f1 zO~~K3Q8hwNY(igRDKL5z@8q;4staWKo-vKJ$>$ZnaZL;ePKMvf{q_xdEh#g3WWSZ; z)qpEQ*UxZ+-G;%hrNFT}R^wTIz<)7=V1?kmX`u&nirG z$M7+Dk!1NtZ?2ZckZ6NXyUEXoE`Nq#JpAOIW#@{FZZk44-M{kE_}LO-m~?F$F}ao1 zMeZnD>e-Q&{_(jpnh{38Rzy0bt999+V)O@)`ZuNDQTFAl(zEKZ?LhUY3hHv{8s*}b zF8Zhs*L$uAq;IYxqv~DbVJ>(;u+_5w3(7oMXi;qSp)Tzc(xl8bWD!k>es-rUn`XYIVP%1C_dMdns5k%= zL9J)|*M7KXo$g7y5RmI$y(e@Sk1+HvRLl}nG1qWR-wygAfE=?}iucU?D*Gguh_ycY z>mw#N28<;+_yw?P$T9?MZg}hMEFhfum+bB-y~4(jmmj$sNG)s57tJ(e;-PW?t#^Gs z%P;xjEbBvw2m#4QpcZWijz?hm0=-f7K~?!$tx>(;*g;R|g?7WQ#foWu$E4VbH>JP0 z0)=Q%JDrHj3}1~6g6Tr+GLVZUnk02?bBho}{e5?Jh z1VqTJN(I}+qpuQBOBlaC|BmQ0el__jI=%n7?oY96pEE#Fli10(U)CMaM%k=shY{ZR z$=f6b^^4kR_-2vCY%A11SzuCo=lV}yv$=OrZZLl{g%3(Meg751W!9BuKuM_4PGl@Q z-LsISrEcPBFQY1VsJ-x6X8F@G?)@iB&o2s^K((>e60~bjHvjQ;$y@auuo+kse_bz0 zOqUR3_n02Gf3cJU!19)AzrEEqO6o&~kx&M?PbyV4M8|IL{Ij0JWmLA2{O>jJ=1=%G ze-+HS9`S45FmUk}W}V6CdMEO4lb-0C68V2M7fzSu2S^SrJke= zTF(pRzWofhE(Q`=^hXEgZKwZtsmC!=3N)+Rk4&|M6H<@wxeYwt1RO zdbJ<6aBWIj{RU4L(HQeJmM<3DOwHxc4RuEO!prE~+OSA)$<2`^obwtdw3d?+R>LJp z%l%FIZk7HWKueQF+tH-qP2Q@r0qY@bJG`%|wC~ZBpaBC)rpyd)$p?S_?DnS}C1i1^ zV8Kf5P}Oh_V;9qq+86hJd(7a??Cj%KkV5=Z*pNYE!1cI2aS_Wm z?A70!s|gNNQE31@$8Q}6r;evr*6F|U?f=T@o&b9KE8<~tr00jS;bxO69mc53@ugWgj1TfzSObOUj@A>`(*8S3b9ng z~IEM0YgV}&p6E^-vUc#Fyimlg9 z=D1jmZC{2nLN(W`=7CO zMoNz&eO<|b72juqnzGaq;zHMpjMlCM>~ zwJTMQ*m2F9+!29d(zaKEN(&OT$LM<$?HVtYPvYYq%Xq!Qm?GR|#KHMG@w5xEcQ*Q8 z8-JO$;%{#*QXP}z)SNw1zTKAQjmQBh4?)qr?g!!Ww&iWQ;}z{LJ-70!!C$qUGbNrw zGkR+#z#Rs~!J&atR*e~98b+*@B<|)rr?k>>>p2rS-0NF2sJxa#;FSJ*!I3@tCtQL$ zysR84o3LaEzwz*MS*IM{{dSnDR^u)~eA^A!VX6`P(vNJJAlOLt8M=WXEFVp*9D}-# z!{-{`e8P#~1_O*4i-pGp_R_+YSMIDodk92UUutLQ+XoI}6u17Vw060V zdxeb?vSP`z}`VTllf>OeUQ%YYi|{5XskaJTr+GiK>p~`Ja0EkBWA#8vg*) zZ)4(Y<;-jw>SLc>GdGwr8LOtWfzNYX92MI?Oug84V<$iQbYOq4{Q7F3q%()k36n!G zTn0&`slg6z17uh07VRxM!?uA^`*WkzaszFGV8X!lVIus5tD{ zOSzk2VpuH#)_GJ4#)NXvM7UrOYHkIdBKp6yE@`dL|G8p_t-F(BKcDBf_|G1g+of%J zi}}5;drPs{lHKuQjIZ;M$fuSeM7bWOA4xH*w(dyWJ09cEqI8mTpZjn%+NO^R52RN# zq`~j*0z}9M^D7?Bbe9Rvt9ofX%_u9Scmhk#Ih(L(5`= z2Eo0=uY*k@H`710Lf>TUw1;oO5Mkl3=_n`ResUF1x|`%>xwFMW)QE2WlpmSfh~w0w z#b893F;bY#;VaEvUN7mX|Hl)e$v$H3)K@p2^R!GV;rJ$BSZ1*bN`*FjVQgUX6XSWl zysHZ46k#of8f`ZT_FjFkl{92|Rxe z1exrrfBjwh?Qe~C#H7;<_=#;tH=KTg9ZNlBbUa}52V&sOn0t8RiEnx0=&!S z3425m`EST_rq<<@NpFBcCMHRwK!R$gB^Aia~`D6+HdF~Q1BXbJn+fh4SJOy~p6StfO04aF2~ zhtzTXrFd9~fOAwhrMyW@{Dur^A1!**{ZITVhx{swLaM?MMl7(eaQD2u-usD#8*mjf zj%*BIafjv~MiZu_vVcgi1+KkSC*h|G{gRFynLQM-S6noUk5Vb{ zjJF2{7I6)WVtn(BL^1eIuy;J))KPY%E5b~j0;$yqAlyz8hZRCd2hYa}^h#pW_I|Ai zSY1qJM&$)k7!?F!nKLknAjKc(PIlT%BunLcZkEwtJ$?qn5^I}};_98Mb3U9flFq1O zW2uT$-SVI3S=pSNzqScVP|^O9{E5PvV7?M(2hBH^{?p(d=+DYzDB~BNPEUpNl={cF z51E^mnd#ujyfrG{GeOZTFITT9HXM<~wX3^7)3N#rA};;!0+w;VsCVm(1<7OOH=;Mo zol7Kdur!Q04Sh4#R#?Jozs0*?Ll3!c(# zIonQ~)ASdHz51tAV(t`nF$_u!>%d#jenzP)8uZle6uQ9R&n&-YL(GJkdvhM(OVr{@-CA&2yP)Y}u61=-ebl(wxr z5V?g7T=0oJle;&QH>KRr5x0-9Kp3!dr%4et(z$DHr+Y# zXf@H~4-DLbrlto@cjahXx9Zxo%*D^^Xf$))9186WFLAoa8YHh+-Ph(XO2<MQKTI4JY<3NHm1*}}{eOPq{ zPMNE)&Y?Gc2x73Q>Nh9(0C**mbVS4)99^Uf6KtLJb%y$CAWAb8PB$Q?(nVBt4HGKl zlClV_RdKB_8nuN9fz25R)~{mQZ!5{fw4fl?1PWNC4-(wq_@A*5@Ok}XX%A=(hHC0R zOFGoj_N#EFTsnQ%Dd0Y}5%Kir@TB?tY?XEb4mz^Lg^hU;A%oH-Am#IUhY}o2n$B@c zgA9MUXrffgu>#}o%!S}tz0G>&0j_1Wf={d)mBQ5$D6a$WnNG>G!+=fDbS<-;S|^sj zy^NKl8K%llxi4N^oX?jkKfc1hW`=Y_E|T z^A4=lE>t{=No^Cq9rTHU3i#J!Y}*z3^nR2K?`1~ovjywNdfHtGD~a?D6`o4eet&0% zz&7kLz~ynwJSIc$gmlbD1m>VN7Ps}$XpIp3!;Dilhj&o?f?ms>bP9rHw5J(#mar9g zoRT_<*NaSvy;QF(-EwI_ql0<0ed9M*VY6=N1I<2#F2nUKZpBJN#XjNu*ChDM6+~n5 z5&CYRlk6I0la#6XKN%2fks#)2CySUh8@i$)x7C$6B~E&;iS$7V$=Kr-0sB;hYFjqy znx=Vbs&dq^af<8`TP{sjD?6FJixr`|*A_zW-xYn;RZa>Cw#d?gz~aJV?#TQg1N(F?`G-;nOUvN;KEPTT&am}<5)!JqD}ROcT-MPSdz)x})mrhZ;)LUmBkPCK z(6_c7uU!1Q8Uu3&8abDCTM@F)7VngC4N``aA|V_X)-ixb^!mfkAr@!@Uj7q6Ed)u9 z`4o}q9wgGZZD}c5dy!TJ4tBJ{A1Psll)s=tl@+W2HOVW)BQ1?L$b%y8LF8?M^V*SV z$}s_Hc7^pofT!xzMSeSGeIFM50bUo&CXW^nGXrH)DP&XuX`VpIR6kyFf;eR@cr81_ zrFzY9u{}r4u%4&(niQ2}bdGnCC-(9T>unpLzj=mmYa)=&IzhPXDqQn9T6IZso0J6TmUU^UDOzyk1)G5WbX~ zTR(M@dnoxAAm$3Y(V_Ffsx8;n=Y#Qu&0suUz?LnBmjps0Rp+hAH}P zM$;3$*WR>V(f{N1(5x92@XAJU(PIZ#AP!@y=3tTIHs^*~&zVW-!AU(woI;hu=5m}; zXzp+YM&{4y;(P>PP>VpjcgETR5>4?*-q4H9%iNZX3(zmz&m(>nTrj@j``6l2aFxa&c-(ube z_ke|Y4z>+uzEJp?wn77KS=4&D``N~ZSf5BwwjY@L2+ZeQJ5ksWP1vC$zta8YlykY^ff*wB7-u}y7 z5bC}Yl{HU|PKBP-VBj74tbvx&6|@ZQ@Xrns1-t#pS;3kzZo?|-<6yeXDz@2T<_1l& zbob$D1695lV*>PyJyTm&;XBlXXPv*U{zw<@`UZFbod~&R_~^WoXTZm#+TxTZs5A1i z_ZD{{`V8)=8Eye(Rt+`PnAW;Vd22Wk4s>t40nF5=BW7z+AD=0SGVHZTmBhd>7)h+p z*wx5hM)Yc+pUqmdt{vUXN#JC6)RcA`UGv=uVGzce0@}WVCjlNr*f6S3#O~0eQsO#- z4nPv6e$^y*0@W3mw9BZ04Jaf7>bjb4jfp3SYA=LrCJ=Iw%poFTWnw%Z8?UhZ?>#a8 zPb#v4ugCSlFyiYjWXFV4-9LDKVN(I~XXS+4pk17*dia26|CVji$Tg1e&Mf?`kRXPO za+j!Lg;12M5DVV2qQcOG2!d$>dDhNpu^|Sg$%S4~_P>jNCwmIG%fuWr!Wos{8wgX? zV+U*lb+q~)h#GAqaXg*i5i{`{0>pYPj%d~PCt9|f0S3Q|4j)QF1QgRdxgnEMf*s`v z2;9=@XIzI)K;v6PPi5I$!?l?)1P$xr zlJwmRI{)$jeh>V~KWy{l+yzmn^uMaFpXok7T-H?`w4la)2;+!F*rxwdqEwVmfPaeC z2zLyOpZQs#CHlLQX*%vKy%eG8jpTiay!l*u`Ie^_lNzV5rU*NZR#_3`R_LXiKs{HcT&w0iB z$lDlyr6R>)c!{FBkpQZmtCz=IeK@#yj#lGhjxObz^_aZ6ZGJ8lb9Uo0F^j&2+-~@N zY2=%yhH9D)Eq99or8k3O3^OSV{Zc&h7_F4NZNQOsYjzP zm&{)=!?1=j6mx7IzvxsR1)om5iFP0ZrN|k;c9H(gJrDGorH*$?powULB+msFJ3)v!yXqJi=<^m!;Fo*m@pA z6W+s^lI_5%HbkcA*&N%x1-PztmQlXz_$uA=8n16jFllp*%n;qVQ5>Wt*b6=kpSffg zo&S4k(H~HjcWj!7Il{HfhJ^;Kn>(!n^=3$i3>Q7nq$$|&O=SiP0FZEx%veRrRaS09 z@0b%(P!}_2o5;B)&(jgPE;4K*Dw7j7>sxtbcx2n`o9OggoBXA!E^S|HWA%{T_OO*5 z>CA_b6Cdr~CspP><2vWRQ(6TL3uyRu-NNM^GR?ImZ2&Jl8M&?MCe1Wb$;f~lpDR2{ zR3x!X7J62U&X-y^^VfH>Y3@<7PHMt$BWzGG|76P~Gowr~V1+n{BqhyAO6Ol`qIYg7 zICqhEZtg=mMX$7|8Pdmb*(I&VUb2j$xZd&XTignJAsr#J!{tM=b(PF4mb?nQPiVLd z_~R$Jk|7&JCKA|QvxP$dv$Eqv>#d&z8a7vA7bpfPNH{Z>9PLti-D&K_E*!_Pwz{Tq zsHJ0F?1oSJ_E)KTH~r}4ov1LZfv3|8RV)v7x!m)V?a6vR+p&Zv2)xbd#tO9lnN!@n z$p{oPilPC(-18bg{7a*&D&6bo+at&2{v!~zs;iU-$>R~?q1vbK= zj!r1~$oP3$|LGDqUq4z_>;X*d9*IX-Vet~C(;d>^Scp(i(f4w#SCp;&ueLe|B8q;iBS;oQY`j?L^^?mw(iejX;f0=E zUH_V^ou<9sb6GdOL;Xxge@jr83@w9Or0$no9VvyI^UKyWM*&N}FL_2-tzbLsdWFiW zNmZ46)no9yH=rM(&x}e(?I3`DrE192Sku0$RgkutjhO7`NdEhWYDqU16tWM)eXcOn zH!#nV1|gK=5-#uw0Zs)rt8%~?)lfeaWEqF7^F1cS*FtzWM3*JMzi)e)PaDlA8@9n| z`7XnFmy9?8nmFCNJ-3XaqGEk~`f@V@G%*!qF&vz(qWaWumKmKBv;hMQihs#gsMj>A zkosi-+Bm=ORV$?8ecZqA^=#&9o!$Htf3!Pov+To@Skh~<2yitZ16o9?_BU<#sH1Z1 zo;L2eCu{TlGs?VpNE1rHklU>U?$wWyk00l$Hxm>dFslVMF{QZ9{Pgl}tMYr-`olwe zmNF$(`X_x!H#n)u*KJr1ihALIoJ6^Jk%3{n{d`&HyL}!f+pn`MVGjs=su3Hh8C&^j z$PKyOzNGi{v!2A!!>16wKvDRZfGb*MKa#^YFTvjl4&L~m2V9g!3Yk9lCv1E%0ik;j z8Dib95flsZKSdT)oUmo9ySk`3m^BeWcfO+2g3lYt9bNvVzRVko$l9gowuBKv8|pZA|Kx<^FLpPfsm z>8g;hQ2ESyX@bHZbgVS5E7jCtgm3#Gc93jvO-`>kp_X5;-#(Z%8Ck&*3{b1qo0R^2 zB5%Wz_c13`Yo=}>6AKqY=K8Y-wu29U=Yn;RJ!xp2X>&B2z&0i8p7UnB{Z}J$F=e@_S#I<02P~|Yh*HmgInD^|U*w%x z9PK6T0v7cZzc|E;=BVCkE~j|T-%0a-q6RuKQaPZnK&+b(%icxV4lH^-^`nL@3=^fg0#vm%@~g|){y@Y#T9-9FkjALqF8q&M~55y%hH9AKia;8Kqd ziuU;VFLCl1?@T|*VijRF_b%kdy8QZ*dQkcFW>+SJiJ&%J#9Z>1kjWnochFdm0YL(0 zY#NHh3@c-8^lMutia)zfbbRv)U2s8H?_~}{Qzlf2ms&9NuUXgJGD+ywq=O5Fa4Ykf zii_UI%a;u(nMmi-`wmf^s!}U(UT)Tc7io`9U%L&w8Q`L%JTWSPP>y4G6_;p7(A_qB#SbDq|E#!O3<8opY-0cgr zR0CngSfCnk;1wex4oN585(Ls4HRo|1HD|-_GuZU1FG;=UaAS0K8EbtT=gcUhud3F* zwH-n|M|YDmHa=K=O4(v56AtEJK+T0`MN0=DSWurtH?}^@u)6?Ttb{l`r1Q>Qg?T88 z3+kt9dMT5VtZ3pUbTyDY12yK2F{Dkg*py_30qri?UEgFQsTxk4|DH5x5$&6!*yt{} z-e!W!o*^5nA& zKNL|dN^&Era3HSO$YYz=D;CI1X>7clIk&H;ETjD%;wr`AI*|`{uBmf=3WwF2m0hj3 zXTC8vgmlYR`)c`gguKq-f=ED>WvZKblF{a+sPT8J-EPJ)crnxL(2d5Se^oCFeLOmd zr>sPMKkk`e;osS&7bfz5cW4NotzYz5_$)A|et6z&vq*1K+a!-hs&_TykiqnAw0#2t zOP?3Z2WGDAo!&Xme)sZ~pFV)2n4B*&dX#YnDiKBn8>~k+Fj`SwK3IgN(rptqH`f1I zlH~Zu6`e+4dT}pSdr6%^H+N#3IeXF6xLm-RjZ%kFN+dC7BqHrIo%PHl#m>s8#lT-u2`Gv$~yl@A|m&n{PgEJ9byTtDDq_1>@&8 z>u-pYeT`E+Jk(u?sTzx@8ml6sYaS)L9fe$V&JG}G@qZz&HXk~1ud0<_;lWeh_%%Fj zfz?m{*i2;Y=K?-bppk=|yev#zzsa;)t{^*0SZvV;vPCYH5#P5l2DYNkb}>Kjz z7sE8hAw;SdqS$5`i%UH$87A7BDCQlE9@1L*SMXVYBHeUT7e5h&J*X8j^oTnY4H`*7 z`jyAPMS zLy+J`*&=uDq?^iL9tMfBdT1VI1yzQ7Af*PeULN+SKMpBQ zbmw>FJai0ryQ9GGR=qiljJ087x4#QQYEw3}C{pg}*X~`>CwSytN9Aj)oAYscL0w(B zI!L;@VLnmsx)cJm)W*~q>}M1vl4F0He9{+*cYdq&&^iSIvmI%{=lbhM^;LEZDB=IL z8=?v6m+zWUXrUP0L)l58@OeKtucSm63;V;|8)q&>KR7y!a{sMypk}N#upepMVetA* za|ihn5#LxyPdDAoLfC!Ie7|jd06BJ_geO-x5H}NE@$WhT65^HjbFKUPRd3rj`pw(d z_|p~f$@}Q|Q(LIU(6hdrV;9A_b zolEcPUl)Ru)TR)73YBrQLM!6nKJnQ8(8e}eWd|S#+F#Ar;&rnG z+1&QPsU!^}sZo~sqr-K*vAB(mFUqOJLMG!>jg>r!w}mG-@jbZb)t;J}*ElafZcnw# z+dKC~z16sRxq5WkfZf7Oc4dqR(k{scTUYeqISq%Yc^tQ4!N<5g*s$Mn{@*&gNfjRS zxHGZ2D?&E*9wKMiUz9z7*cnKl5vW<^0x;Uhdf ztaD_Cq&J(lsP_}~BDuR@14LO2`|!M4#Wi1*#3QNda{*KFUOALAO&(uNSf92uS%J6c z)f>~0$PYST*hTusIblbrCbw}OPaRQy33~WF2Ud5t2D)pusEB5G;sX(q&Is zv@E6BKx*Q?Pmk`vc#GPhN}6#VaUgeB1I>bq_8x`0SN~%6$?9|nGW4;x48WrcJ7Kvx zC)7r>pl|KeZTQgJZ1#hdCB+=JF~~)DcDk2-=`Y68Ow2$;&ou8)emvD%%VPQSZN!^t zR+c>W{JS_DV(&k07VfpQ0kyQlyqLK(XdvHgQ51v#`hptXb%9No=eclwEr@(~D?4ns^Qs(a`A>NbeJn%P+U3E#d6b!fVoZGjKNZyI#FPD@TpOsl(#+rZ6ZP z(GG)yWKb765TcGb`?nhz7RwY&7K3yPop*tJJ1?@+Ve4ik1?6tWjQm8V^G!#>!@$Y7 z@{!G%vK+aBruL7=<~p&pA=YNkFxeOW5QaLP$h~=K8*FOMH{MU-2pSI^O8NE;w)iSz z@SrsYpnh`bvTvNNoM+>e4x^-ldNi9a1x~B7AFAC&wlXFdEr34{!xRM2ga#XEn3=l` z7ui`ET1=I|kz$PdO}BiEjND`TE911%;d5vbiSR&uAxbmL{vRg|bid2|Oz)?&=&+?S zbWBRUH6&+&b>jvRHiUUC`5pFI825dUQYDtal2Pf%>TyQdCB{O ziI>+osOAx|nw=R#reG1(x=V^M?B<#D))TlFJadU9_rvEKZ>2s!^wioTXY=VMJO(pEmqeC$7 zoKkIxMlGUG%C2s#2mOots)S9Pf#c3VAN;`>sV_w4&zSv-1Xf^xD>%Lr#1ThWZ?rn@ zS83Egip=C``LyRyR~HO!_*&@&U_4}#ZCBTAMgLgf z{p1+jo)WWLfesASnGYEN*I=$F@GPhs-lmsTDgV8aH-eX}-RqP@TKYX~S8F3CYl3qh zx$S3GTxh;1)6t_KNdIf|o@W6yy=-QNN&ke4&c2#m9rjpJE{b+`sZ3^2^3sPJ}x3Pu7V7&xjkbV}^U-RUU2A=ll zX~^DHx9n+nI=i{3J$TRp&klumn`$t|l?yPvlE%xPPfFn`ivGytQE*!uXmK8X{phjY zW`XJkSz1QalJ}efs9asfwt+O@a=Zhr0r@{sEcEElGi(AF*`x_;UANJbzR&F7xvyCG z;gQ&U6f%(0w4`@<7+ZJBx^di53uc{a*DgO&+<%nN$pqk z{=Q?pVkKvfAoi5xb&MbajY@XF7&1qG0C{PW>V!T!Vk6ZI^xNT-%*PJp+)!tRj8xBs zKfq>+HljFbxlxzY!K;}Nc4p7Jsc=A_V#}HmWraQv$r4+VA{h1K%eKco5o8UUdueb0 zPHAM1i8`Uo$x9TJNjcH}*|!0mVi(F}iq0lg#9jIG_9EtXsI+FkPn#>Nw5GjJpDGqM zJA89Ql~Z?*e;z-86zcALIbzF{eK0=f%*$_87=Z^rW{S>J=^lSMdT`8HZ}`dCa=9bRGA^A2kq#a{p0NtsK(wojIgcak2ZOM!Vu-|I7F911g*^^pA4Y5Wcy0 zj!%|;!0B&U-;~{$2kK0p>>mc3>Er*yRAs+of0&2tcj|786MTuT!3%L`*nADcN8FR$ zo61jX_e`IxpDOf{ZM_%b%L6Bzetn^&jh@o~hQ*0I0{v%qwbOabmdWRXLh8c7lPO%K9p2iHErg~P?YDEv|e|C^AcHJAp zEKf0?%_$;F%5VsFkm|;wc17NkO@KDJ-ydi4;geIRSMd6vNHA|reA(3dl(GSZ^W@j^ z+Z=XdCZYRo+LJ=JW1Ti465L!U*thnmd->A1F}mO1%lI&ujkX-Ddhw-C9nk8>jIQa& z?H53VcO;%^K^96+OmYcZPKCk@>ijo-?@LdPh`z`i>$YhPMMy3xvnXy1e_T+k$OL? zZ)0!MN{*QkZj6sJWpJw-1fkE`!UsR1$ts@o-di=8Q-5~x@BZQ@uHQ>K%$E%tzG+wd z)-L(BnezqYZpS^eC;8aUzWXww`2R+`hIemHmu8!}Ho;C*czgHp;EVR@7S9_e)pJ!u zdAWJ;V4l|Wg3YX*9Y=my9DWR@hjRO*UVI+qFVyIg?WvAn@(TB%UY^Y^Se_Sp><|Q? zo+^HdQP&VJ=9t#BB>S?S8>Bu@Y?{nRpy?FM2<$Qh&!c%z7RoYY*c94921BF4P$nvr z=GOl06NQp5}()a%~gi>Y}&qxunhIWTFp*YP1Dpm3k#}hwBEubnTT=|*$+3w5!7Tn@mb8LaT5DfL#9#VF74!W zqxLbbrpEMm-7mZC03C;BuKq-N7}sLcnCW!8=a%p*>EnLoD-_d5m=Ybeko&m92sQ3A zt)|9*Hds8rHMCA2mlnLOD`&eHV^r1n+PuuQn`)loiVX3~e5@*)+dN*0lOj^8#Lae( z%{KI^aJ28uSUHGM|WNr9SIuDrS_T#!y|L^RYVZ8zW8h za0<4K+^P#s)T&=;PwlR#Lf@WOgH99Xu*642q{diQap+n*3ucW-p;7<0H~R7j1jUle#HWm;&vkzb$5X;mi+6??s#@UA zUkW?#xZfl6rjMa61gjBZsauAuE`$)U#0u7oR!|c8`KF)n=N@UeEp&&4G&$6Fr_1Bf zt0P)*B?*OkdgM7kqOx-&)ERxZG&2xKV00;ydUM=GM#I z%#!ciy$oZxH%RXa`Gx)KS7AyN&tVdmu$#42rB23|rp4b`m?UE&FI=%m?9WektuAoH zu$|W3JFn%_*IK#G{Oh##HUq9mh3mu?uXA&H{uO{)O`L5}HeyXrhPzpmj8xPi^O*>~ z$aLsu^&P%r;!yOot9%XWPuoTLu7qMt9-GMR(iac&@5ck=oZsT^uEU4@yq_$G?JVM5 z7>^Edww9=~=&#DpBY$`egZ}Q_gNyLK6C4(uZX2DNA8=tc$KgE8t``e$<(rhRKA0*Q z@P54e%1eQQp#cFwK>+~)3H+QuKp_96KtQlR+``73-i`gY?r#=)P@w-S>LuV4p!Op( zg9`-o>;H;cnHv}yIGfNp+D@u_Tj37=%6YH#eyyh`D^wNFP)S14wVN^>j`jwgtDLrf z>Pt)Y5+rQB@LqA9^me|!U?)JC2OD`Xup^c%D9;zzL*(}(N}5v@Ng_;~i47r;45A>- zJKV1nUyvu@pVQjD=(@e6bIrX`8`k)K`h4n4W3x5cnz*`D{~xxlF}RXwTgSF5z6hS&Dr{9aO9LDY~q=W7uP(r zz7pacQ5qT_Eup9MMP8#*N^B-`{e{U>ztC8{eQ9lF=RNtb%*O#CBPOt-)eh6`Fy@M< zT0otfO-PZX3pCjQeD864->I%QeJ=&{^D}@V7(kbZ9cq5M26R(CstZ8zpC*5AuHs_I zgu`x!0^XSSgET07I>q;1O)S{ZKnW#Rr3}v#qrlj$=r@9WS#lqk=Ye6MIGFJ!BeuHF zc_=I06X^!(rq9x_9plDuxHxtz^1iwcEEM_nKK!Wf0~7_P4D-(bTZeuAor(Hfl69y# z`6S!Ctr^W6!Z7TpE0?{iXhRv@RtKBi+L8357aveFguaGBbT&NJ$_XLYCBqc6Z{YvP zfc}1OgF5+qkrG|};1%DRJ0}0MZZ}7oo!Z<8emZ6%71}4J*YY|!lps(Masq8GN3@45 zGU}+m&h>_^trExzH0|8)v@WYQ;i#9W$)pB!fjZn^4YWtJ8v*sdpB1DU9ObONlNkh% z=R%WdEZa=ycKS*yoW01JXh>6QqqOvr)z*|NT3)-cy%G9L)^` zo6E_Fh`E0+@7}M|5lb3lfWe$m;+@PPkric$Bvz)o*XKb`!qy2obpJ+z*j{gr0djdx zjs1njf_`blWSpP)Ba#vnywGPY^@_c<)FNQHtGbgDo$3ntd_l-rF*AXtV!*dqw#Uw= zBkU9G)S*2TbL{&5tk)gAbQP0aZn;z^m{=M0`7cANLb|6}P1`rY*U;D5_JKtYdYdN) z-WG;n6ZP0Hco3o)S_p*)?g>thlw;p_(g@-Rngs3+LI%nj2CF9xZyRAdRT*iKXWO|4 zY8!7`yhqU&%Gn$B@%7>UAKQ6@7H9n+YcQ{w(9G%1S-2hJ6=_Yf!9P-HuP zmDl2|YppW|Biy3Gq)}m**{fi^>SK;b2*)Z=Li-TeMlE2u0Au63l* z#@BL%c1E~nKQO&VX?Nrp1a3{r1J&Y#(6)i|I+EL(pvMAVNUo&GBki$(9^fJZX~#YJ z*>Fg3#>n(SenaKm8}|;;c|D;A81o~10 zI~y9=?vKm>D?DF}$_@!*@-x$kC{ z-t^LSkIpC2c-lIzXgR^2mB7totj@LcMAC>V%)CdYxNLRpC8rr#;iO}?ux#XM0FrRv zOMB-Bfd<)Zvr0QN+;KN_a-MLvfXIis+LtLP$GQ}6+sRAP3p+T=tWL+~dKX6*OY@&fYzE(GAS6sY}Qk7b$@tdxn zrxx)yLZW$HaC$b_%~n~UW@S3(h> z-S5GbDoup^$#sZY@Fd>PuT$A1H$=9Wm7f&1SZY$Tjj#KCH|wg(viod*;vcT z^V=H^-Xtf=o$`_cR+9GA5fRHQdumf+bsVigm01UGtv<aI5Wd_yiAj&;vIIng}r-n$Ln|L%&PriGe>LoElKqmA8O?(F>@ne z_SRB73T}i zbFHsWFj+D{rPK#KBG(7}7R9JFrv-Sn+FBn%9aADLG_)XS6j&sm$&zW&8@<9hrW;}V z;^#8WL}Gqb+mt*-eX5*aU6sO}=ClciG%$jLtI0CU3e=#}WDdDv_e%tNQTjfVlP_lcAU}#j=h(DgOJ|Fgs;yV>bScKSc5eRNAYT zlGLc>H;-xK(e#*xWGc%hMU&j*5%Myr`)krSvVU%ekTw5T+ z`5WnuvCHuf?jK+}8)*J~#EM36!w$mxwv+%O8k_R#(Dp7OmJkCA4Xo{%b_ywFM&AT1 z`^f^#!3ZV8Q}ZEm*&%GgS5p#c-LEnRJ~FvGREdB*EslXPttS@_hqMSlhAIWK#JT8t zmV8qE*iyF6W7MII4e|?LCmMqrV%|7BNoM8M zHNIpg*A#kr*MQNV8FK-=7otCZf6x*v!jR9;%;F=Cy=U#<*qFA|H~OR%Hx^b_QtVo3 zE*_ePF(-&!nzxGpO z|FB$s4BNN}cbvi*i^sX3b9X!+B9!ysOZQ6CXGOHRtzCzXq7*n>{(-XAQ!~C`u471= zp!-K01^fD`?1M7+jSQs#Zetlp7J6=#vC@*nvFe{DDphW#sz70A&s9WEf1>v@`p0gm zp90b`!Q7mQos04`9_Lcl?ne`sKZ(EAk!Xcy9hl^=ZQ&*WnGwv0_?;e~n%)zeV(+UT{Z<#_-mT zIn*pIOu!rUP2Ic>__93W)6II4abajSwa0(&A?xMlGr?A0`B}=lb4!<}hsoO>7%IbYaEcVLUFg4C{nsIc6J?!>JpjlW`AMD^6 zh1ZRF6W<%d_rY>~`0xS$-;;Rb(6Z@Q00e}YEd&JK|Cz-9TI$-^Iyrm};}>3E)D)K< zuD%3{ieqA7pM5&X^N^#{WJ*%b1fV4GEv)?||NfO;k-Szzyw>-CV{mpYZ1b;_#r?#HFVTGo^yaN0xtYR`S3Ze%{F4&<2adQ$zY8B~fZ|Hl+A5OP&hCgy z#8nOwKhbo}no@s);p?^~2s!!q+}D?nKWcd50D=8(<92f->HX5Kvdgxk57Uraukk3} zA4H2MFZ>qOTa^M2#pSqK8>l2Rs6vIrZ*k6`=Zo!;i*4D40Duj=PZM!R1+k|X|I?r| z$m(KSVzqawF@U={z(v7z=(Qmrq&Yy=242=2{v(I@&5ZvkiJw3Rw(zRb^Sqcj`tfd{RV2*QM`|nRZ7s6MUx=`a!oFcl$#q zbl1zgn`Zf0G* zWbPB#CHHSjYw7dvkEUPP%>{`|NWPJRDohau3`;gC-)*SgFa9lTG2B26v@5!`@O~5a zjg~_G&$G>mfVH)b`U(lHxAMgBg#}#UuNl+=IxP9N|eHAo7Gg2XPQmfg-82cxym%` z@hI_hV5>(9Y134fjJOJt+v+~IivhPxT^Lfm%ik@oh?jTy*i6)c>cY7X?IL`T z-md!#9ApIqtcJQ(NKSH={bmgzt4_~`9Z+FWN#LtS>KZf^cT(Uix=qmqm zFTHkF+GzdK$VH6sysEc(Q6lR8I0@_3_Q3;%1@u6xJO0Z7jueZDV=FT71kwcR@A?pG(NThP@A*_{%J@q;uhSJvJ%=_citu z=GdMbpC0@H%yHR{mThiyTlifq?=krTr#$he_(umy)BtU;(MPx+KxS(PMBKaID`-j9 zx!CIh;VIFd!i*AaZEl2YCscbqu^}XRnBLZ3)BpC2Hpy?UdnJ7$sB?3__k$mMFpO1R zf*Pfn;jY}Sok+6wtGXL9O7OJBT|4|IAdL>igr&ilK&Ii7c+{WP5H^1QJ}YiuaHF7T z@Yg+=j^f>%-B=pucGeLo#Xx3KhLy(TNbMFw@ed9rjSe~oH!JTF^0PoKmP0lbw+DVo zx&@a*T2W-=xe>EEWwtRBJ|o1$q57YZ2+V{z9Gar^S@sqs2WR?41IZl);HY$na|ui7 z3BCz4C;w39f(GP3JbT^kcqZ@IClT+Iu0HTLn}M@yAN&svO$^*#?y1DI1bsM! zo1(*Xwzb1y4O;4byR^e`fqL=yNQ5yZCaAEL2plT^B9XrRlDM!;+LGm{KZ?obal9!~ zI%d++f2REXMmSAVrQPcG1sqH)bnO|^CIoROvJxgG>EfwJJ`jU?WW?UX=*sKU3e4VZBFTHprpk zYfVfuQ&aZ8WerATo(o7>9IeXqEV30*nV{*HUfy7gXi0J~1)`$=%G6Zjyt4_T>p$fU z(D0BEjj1|PuXDgyS#MwheC6M)h|G@V;N!toz_m-(QdG%S!~Qhm7BpOK!2jZ4LUoe^ zgXfZ}Gr~Kj;ZSVK^vxil)(kp4-3EG2UQ&9Z=CII>(r~U)Ll5%@vFo?-?a?#6Msn2e zYk5RjKC_J3U^%VbNJc}hKar7lK_lpHE19g;l8pL#pxBTx0i@DQd;FD%?9LyJ3-w26q%$TQH+fi1<3PC`yIME^=DoVw4@egJR}v9htklZ0n-eg=`&R1kX_T zRYgB*egsP>bbQwlzQ<%Rb|oGtH*%eI)Q}1G5q-bbz^^KXT)@W{3g5kF^TZvNb$cdH4?Wmf8ZSlQBoA_mdfjO>`$SM1>u{-!m!nZ~(ls@Um zAHlo47sI+_{a3m7DGd}pX~??Lw<%*Fa}J+EWS_9Z>c0|T4SX=dgf7h$Ze_Pl@Ky11 z7TcRxxNgdh&75;9(&hnO;qwf<<-N_egGv6cnJI(!Vm3I^zU>2Kp~T)ZJWsp;{#54> z_Lf^3?#Y)1>_38W$U39@rq^t6rd~JnMI;Y@9*`+yu^4C)e6RwR*q_YcbVujeqkA*( zG6lyg4fl<_q@zcK!~87LoN3ryVG}+e;au3B4ek z3~A1LcpaH1fCw7ads)*nABh6UOw-If6I)U^G-D6~+!9L*ARvUDhj8l$8;^&s2~j}( zBd!6SfbZu~Xht9yxT~J+TfEFDkND49>nxr?Xs-ia2YUd(803en)3dFO*AdvO{Mo{` zC6V^v_Fp)FsV&z-nV#)~XG47NN@{C(?@G&5@Ad|MM{=(Riz{&engNI%c{8E+4BwM6 zfFEBV@w2rK+yGru+n8mS1i~YHp4eUw_LlVCR~A>=fJb9nlz>M=kS*NR)OH+ll|IN0 z*&7fr#KPkt)Aw1nDhsvRd<^3PuMwT<~(@NdRe~JobY=eK5t&+$RN{`pEvrh(QOWy~Quc4i?kG zeer3C#A6Xl6ObdbU<~(xehq9{bcDs4Qwaxu#4-J1VqEQXVn=F*@g&DVf$>o~Cth38 z-b(lv?CRfC8Zl<7h;)K)Q^#+@c#k}CMBp$FOT@l_W9ZZwhw$peyagB4Kkg9s zQ+T3wWswnSE)tJqF6}fMwSN7YL$kR(bD6oljm74>J@rIdRv~wDmS6qG4&6l}wMQ5) zw||tSzV!^Bh_y4pR6P7ptOI>iURtV7@ zh<=-$I{oZDq{kliMh&uzdXPb8o`_@BFX!lqXqjPM=0~s+D}MbH7t^TN?O%>hDFR~R zTw=o9D55ZR+pEGOg9I{Gd<==d$_vLLwJiitkpStFXfrR>eFWb|JO94N9Y7bkQ44>P=+FhpbQXIIJT}z6h zuT($sb~gCgok%TDvX|R$<_%wU+?=WwxLiywQgvi22;8YH9FCR}0v2MJ5yMW}E%*GZ zZ-F{slgaD(A*TB^xJ1DlaO?fqxlQL+?ex2wA4Bh4Yurabpqs@UyKt7QI{Q^^u7`KF zyWLP~flG7sr0lEd3iLh8u7vRh~ae{ z`&b3Pa%jqS2#mN>cIdrT+gP_O2soK8RO)g89NrfNo?n-LF3I}vca8COFn`P{@>=}l zWh)kcaF^pf>2HP4-+J^p1PgT-{p{rDClO7Gis<^WP?)rKSoI5V0p4$ZP+Y9v>S*EVXJvm)xa~#qvR){>%z8Q8 z^NSG#A0It?k^Yu`aaXb5Kf5jt7A9C~RRp|8lb9OlRIc3l9a>5bMDah`xR-$6G|^PL z8Xq5$3$mcCpqt+BlN?B6@Y)Vq>say698b}MCbFspJe+#lt4|DPxh#JXYw#I852P^P zZD;^{)P1nvqRdkTc2*C*L@-oWY2WNO@_wPP$&Tn=jbCmkA9UmO0X`h>Pc3FT6EkTk z-gp1(>^i?4YajA?H+!yRS3M1w{jfiBHR)Y(`fX@oQV{7$1v)WU+ex}@7D!oAd)`)b z;Ab%PtQLCOZeg`35)yV?O*~3=xTPsnnR;bCOyjM``6u)?7}y03kM{DiNqsq?KldU; zB1F}-l)8mE8A;T%e-E#t`*s{CTHOBHWqYRt;q$x@T~*YoZJi8XzUQ(O)N%I`(Fs?P zw$ePdj^6G0vYoV*z}Bqw!A7(9@Oxs~;qW)^)Ya*`wIR(%1F-p`<3noT@K!gIZu!pY zAvm!*n@4E7hui6hX!Ni-`lFup1SI*ozY!v(4J>QESbceXyk*$x;sp0!9@%udjLY9Y z9&9nLsU>h6-qYMxcdB16FLplw|9Tx{lTb)CIkvSevUS+sgNJD%bQ!#?MFZb^yR|%o zZJo!P`o9F~3+O!jR*nBfh?vBT5|2wF}{P<$HAa&`ek-*hC)DVeF zkyKQ(13!U+)=l1iq<--u{#jkz(-gvq*0f-jtb}*TT zCiJIC!J3b1Ug{*^0zYX@phazjOj!$Yx~}B7pk=4Vs%+F>O&cDp53EXm(UftCn&NSD zybirmVQjT;dO6k${_nw>$rl!i|ZdnOm#ODTdoWSnXEkbOJ?6)li|r4io>O_ z^V>gDPFe@ZeIL3&Aye`N-pXm8%2a;2vSz58%8O5Pv1>L7&t`g!q^Rd95wO0X@qL}c zX|#tWh!DY{TDIg!>k3-UtmM@b;%vZ+nle$yW=A@j#G1Fp=XYv1u2)yF^oM7>e6uT9 z1?u_mF>fktc*q)ZpG_o5C}yTDCARlp3nmai3=8?_-lUbOHmM4lAE6r7ocRR%z?@w| zd+4Z>zEngszrn9Ngokttc4fb0py6#TRSH+I!?Sqlt`KES&YL?`SKr4r=UoBkI47qJ zQ@csM;3PCi6%oC>3~Waiz6x@SW-GoQJN&U1oXU0AImk8n=E7rX&7ju}02LkmkrkW@ z?r=&Mu0HxUeASX7gtRYBb6}mN^j+uO>yDQP;1PZG$JpYa#dKZ_oF{sROj&i29q{mF z?zQH`QC2kTui48o*XwsKbVTD>vFsbsyEFzHHYbo((wA4uYh0@g4prN%M2IGD7B8Xl z3qChk!hVM%$zfcsK%tLAX;o_T%8CN37bQdW`CBSlO;#n(#mWgh>pP-;xf%(~kdIc@ zAJ1;X!i@ex%?HV(k5kPBnMPo-E@<>SH+DK8 z;t0+61zk`7y4;xqdsYG6^SY-N=5w2aAe@q20i)fE4d75Zvp?35hOmR{4$THdTWcF{ z+e-aYpOn;I;FRmm0TJz28jjoV-sXrlim1pm%J`>uO}$OVlAA)sDgZ6J%b>D5E{Dp8 zirgm&sU}g6l^iGhh6&HwevDzHkWA9=!B4wm$s+ln`diXKJMumIF0oV-qbgqAD3RVN zgS4y6Dabu*2mf3dxSnB)W%-hKiJo(@7{j$PeybP(kx1YRHXZJ zQ?{y}JQc>yFz78r)ov}(n14a`7=Dje?*z$1Bt!;BFsz0_Gs(J${?|h+#L*p1{+`ln z!c$a+jeJI^JpOcM_1lkc;X$#3soNjRRPr?cEUIZ^8C%&7rl2>cREO={uOcZqI|Lck zQ!Tk!oq?b~^%*A21GQ8xn-{gRf5o&V)I8&!D?kj8$&T2K*AS9L-Y?IJI^El5Dkt&k zSQgX8DAdaK`>6v|#3x%SxKe@}T_LozZ(ljf)qb{AIzLJU8fy7biKuc|>L2Cyx2xvo zgv7_+ck-7|af*zq>1KHc#US~5*CipL7l*T{vI{4c8m=>qO_QNg2(KW&dd9e)BFc9u zIH_dIQJn_ds3P2lJsN;<-Ko+~nL&>8O1YBod@@}60y6}yW#qGb6nG`^_ic^lGcKjI zV~c4pLVz~vy$XR<8aB(2@<6VOR#ot(3yUI>$%{&JImC&%=`f33(vCZFulV8lCwaHh z35zuNC+_(Y-$TSn2vcwBBn@|Y6VP!p*sF|lM(Tx1y{*C9lh zq1=`NqGWizCTJDsnS(xKm(0Gnj>^|M%gpDk>+dL$`a~6r?#sGPyL`_^v@-LWQQD3k zDh4660|nhE0|U!25v(9qJ8}~K#ZB+J7Ys0%Ooj(eQxb#j5@G3oVwOFWTdgW_t+G0UWOhuG zgEI1xd*EY7W#5YrEM>wsP1gS35QmC6P`<>uEbNebbPps3t_dhL@>pww`*U&?3T1!9 z{m`g#jB5~xdKCWF7DZr{i&wyCy#?bAr;_}|CXyl{Yt@Q8CRHpz9P?TYo4Ri9 zpVP03_sEXbEW&)C|b5Vda#FAFw+`zq8caR z!OoTG2muI*8w+sHXk=t=73cV}V-QBa*+ed#dG~Xzh_bFZFc3X|JA$cSCsw8@6Fd-w zcKe-4_rlY+x(mIiBv>%-A4%UA2gE>%vY69AUU6R~ybCFP3x}uM?{-j$eJCm8UatbU z%#-5^4OibUHU!?5Ve+5MmSRIBZjWoH!=Y%}( zPw9VI6EneWf;ogW&mbK7{aR>iaQ}SPa@3}v{C;oLy>R4Evmi8qOMzDkRE!-v&0JZSSx0-Z{#(Mm0sSffpipklV;qmMChgCiPEyDu*^w%y6H6k zZ&&1U#CnM9D{XZ5!v$(yULTJ1rO}A{zZD7Txi(6-Tz-ZOrPG`wKIIy3Wz07e4Ol_w zS3As$5YC((?7Df^9T%kFgt}{@$OnR+F z1&9>SeO`W^`uA36bglCAct4^PMwqIcsPj?s=g@wVxX6wvvB;V|$BcNw*wvNviq>fg*% z^7rXXTJ4($VrT)vQA#tlVAOAxE=*+cXUyk1p4T6pB!`ugPOfc&lq6FRIeb?24v!+y z7U#ul_2H#33_allKes3=-FMRpQM@2stdS|;maJ^HP+!>}oUN8OE-u1rgd3VAd;$-d z^hqAL-r#38nGqb;*0wp)I;9aU8k>V_oG&h7YuZ;dbE{h14(NAM`f!4qw5d$86StizD z6_ufpdab$_v})+LN|fLr8Sq{RZC(}!ujbaswdP@QROrO< zqPzhy;Y%O-&CKKHnp!0FcO=a>m3t=3>qcZ|OZl>Jkm`4o*X=18+TFiB&`}Fa%*f*W zSWQpm;UCfb`^p=+rFb`aSTeDFtyolb7heLY?f9I6+|0#~rwIbhWX?m_rN&Ehc^4-4 z9^*X;(rxaUW>a-{m9DF$?V7Xn4oQFk%Y$n z2eBBAUvVF6K-yBn34MNF>2~^0V9v)Z0YOFi&nw6SU=II~=drV%mmBZsN7MmgvdK^^ zcWTk;0lMJy_H+v{g-VzAbG*@I$RL;vMD>Yt3rLS_A5m(Ra3L-^^&v02q6M;n(>=7C zuXRnQ{1;FV4U1|u#ENRmjtT43FH@#WOg}vA09#>(!8dh^>TZCv(X#FDL~L@A1!sr~ zDFFF}`8Vi1w$>d*86` z!}!6dMtec-M+&|KuCUJUW|<(WNd-*^K;~k$W73Ux9)OyTqP~w-OCN)Npw=0@2&{>A zU8`ZS-B1>9*|-itZZ!DH|GO3cH-t^|;w9(^E0cj-VOjTF4GzaL-a*lVU_LX4&Y3X^ zI$te=B$?F5y;QfWhKIrsk(+DeN4(!e{omfgTsoN8NL-Qy zjgFp>;k0Si(JSD4!VDAN4*4$_T0vP{l1;xPbt2V7`s}ql$GR>ntC845 zM_^t)%_q0*+ow!`AOiWfegaWdkfY?~n8B7#NnAI=)S)a75Gq&ZBwWcAB#4GEXGanjD}%8->qax2 zJ>uk^2gs)s1Y+iA_OBL3c$*ac_VYz^KbA_v7ok;M74@?aq5cI>m~4er?yWjxpGH|t zTm9SQy~5-68}T;Au6+1;RWtXWt@2JmjXJ~3i&wa$ebVj(&H84>8eSRov$tnO$PtWn z{g(ff-Vbi2r$0A@hx2S*(5~t66XPyDI6H%A3RC(LvhH|}kPqBC%SVxCVKlfksqOv0 zCWT=sPAHF$=Xr%YnFrm0E~b%oOs+t-SL6v@tX!^kLqZSC#lg)$ zw_5Hm`e_BS7o}Jtm9x}eFHRD}e8AQfulrd;C!&(2wPBums`v-$2$c=90>zzf3;M2S` zDOQ?mj`_wA8>|#Htk#@HnzYf#^MP=49-FusnY;Q;%CUs`6@= zcHYPt4_R9&;ZIK}CK>!X*>4-MR4}Mf&1g8L$ZsL z1BX16{IiP5K0jR`5u8!3lUTy{NStkq5=6Xw(f#~@UvR0Fs41upSNY{U$hPl*5&8Jp zx*~=}R!W1I$MrMXQ9b?N;f^d<+J3yZBEqE*x~To^)}ckFZsDN>=Nu5zloeqNJ+d;W z($@yxQ%b3Kz;z4qSNgBRut3}qp18ohQmT!9E3o?-232Z4b!T4b^h}0q*X8#u`QND~V9f=0TWjgjKDg7q$O3iHguC(g`Js9t5V%0sl zqXYWts!B*gW_6x#O;K{Q%Y#O5l1MF-c4v;!@;Q=hoI+0>*-0Cx-I1%+8Jk>^hJ{*r z(W%=YP9e`b&NJ|#7ot>&ZNUpsvZ~dZWVnUwao+bPBw+t#o`y>rtzop-8Zwuhei%Kr zPZ&CqiT_q%kILbB`9R$@sG2HN|*qs#AE-T8l#J8&g7 z#;SHxZNY8z58P(NQKgl)5m{O&XQ7nbj^*7n^@?YK0%_dum~zRs>h(Pn5rg{Fms`Tn!sQ2((+b>2eJb@{A*#O!{(vINldxL?;1LYZA)x)NT@|ylbTGT0T}sn2aK+&UY1& zXi{tZUT*t>-6&5qT`(*ky!xJY>z~%+%8q#__aN#(uz<~q{fL4#Oe$TYw*B$bj%<u#jXdcpxm{I$EA}1>Tq8_dH(W0fz{BXW0wav(`9^SA@r=Ac+ z^E$$(->#dU>;-(@B|RPCl8!qteEBw_=xwtpmY!i{$YZqBOfgHez;>w3pDp{ zF~urLrA{`%jr|15<4<7$wO0;9ZESOn(1DMy++oq~4yP9Vb$ z*}8`gSKuyF7+>0JtNfnb_mwU7Q48hWdT?cZY}DgmW7MO4=b#5_+t7n9gH?WXlSf4i ze_YffkxSI$+g00uw~;K){P2`l zJ=75=oeE5}q8{fhToRk8msB6v6J+>Q_I~#iq)F~{7z%Klm6G3G0d`1)?G@14gmD*`M0mbMz?fj(&Zje$W8od3XcxoDXZYDg_!%7H(8%9u-Y|zYLnxk3e9wCt%j6PgWF6@)=?zy0|~nBtF55cR-L>?3L7u?9*%P zmBeT8h`o}1@qvG?8n*cL@nsAB+2i-em#cs`ug_Du@sZvjKJ!1qJ@G#C0pEmUKl6`b zNq>AMR>yV&KUXwyX#Xh!eINd_UT@MHv0oooq3ET}F?al9vWeLpVuQfgYg3~)H+&!Q zK~*^&cZo^PQK>7XIPW31v4c^QvENrP;pc~GfUvF~zue(t?@-eS8#?YLu}-u(3h#{B zW*Ly7%wK2uqtcH(QVO=pB`+ke=-XtFB(J{4uO@gW5d7q)?(`Urp?=%^HmWF&X;!}u zsS$U_OnB1iGs|yYuyw*Q1OA2GEY}rz!<^Oymi=t3`8(LW=&Joe7Rww>5?-D!c(ycg zvXCQbmMXC-_YPZ@^WJ&k?y$kuFQhoW4+sR$J_ZA#Ibd8!RS&EE|^z}&xp{J=Qqtl(ZMcn>m zY$!4V&HBq}Fv zW?4z(V^${=WOiF0o-Y}WTb0irEcy`>PJms?DUpT~t&_^;KCgGutvnNGk^gNdl7!cJ zo@L^D3AcDcR$;Cc$|B`nyEli_c&ppfA=&R;jtD9mn>U2tU7_MDD-mqtZYzuHMh-At zXv6VYS#g;rl2bRapslZy8IEEOIL7^3K`Zz;WcWBnZ0uiOyMJAV&fits{ZO>7GRMc& zX9YCcP9whgt+G$3+r}kXu}b`cDk%PnZ2x25s0TK(@^^ptk^As>& zCQTgDHFq=^m2%e3^P30d19P|6!NEpXBT?UKS}Uu+pC13#&8^s1eHx|(3kxv)aT|%2 zQtEeW0;GJPJaMrfP<4OL6GfbQSaG4*h4=Y59~!23CdZJzxn`_RoN-O3PD*(r36u1W zxr=+HDmT+sa2u&kDA`{cIY_w^PD@VN?4)~k!Z3bs`wBP!$fXsyq$WS!7dWv6jrafr zLy}YWZLU5R;)QOm*O(bd2`+yx!Mh%&#ORt%zWc=PNVual{P2hYjRR7IRaCmbcArS@ z>u~@8C^aUvH}~=pm)0@Ha8z3nP6v4I#HPNw7#RcR)kz&_evoo%bm{D<%j79^ZW!H2 zT^%q<;hb~`oB1?UbYO9F%-rzSU8kf}=cFnLg>^N{zkKYE^1C0VR0Dy=7tP=ghX&8W z3STW?M}-%s7h0uzoQvCmC+*ThRxGo^bWSh#;%_*`VCDF&-sO?tE+g2IN*OtR?SRB7n zK9x);2#3ZP{B?VSQkpi1R7w>*5@K`l9&udreu#NLN25M+>3Upum=b*4bhx~_*9-D0 z)hSMRilg56M#b}+?KUCkn+kPQV0S`6m;xLKPENVO_E@P@PdPmx6GlN()YcJ=~;;YAWB&76)l+rfMLo>I*uh3 z)Y09KBOrMQpalRl)xa()G!F-oJTX+}E0qfht3XoEBVkjp(c)x>?uTcf-C-^<5ZHYL zSQr9F62ndX_WQhi_+h;z5IdM*Hj#wa)BWX#R?std-@n&NXRC<#Hi*^23EM>vtCIl*q$d~B zV3RF)L&NXF^)@W{5R|<*{Pe&l& zM*R;s1$nC%wDRWzGWc=4Kme(DRzCQV?aeLtH8o4nf%b!?zquCdEMxG%`c^y+hKhQR zVR<((i3!+2V|nLvp!?wH7yIJ_xHXMQtclpJ*w9A))au};R_sa`*l{FYW%p*$Jz#&% zaGIYqh{{TBz56-=A{CT+JNR%dW&SdUmgjRJEH1l8K~i<@IQx)4ZkOWBm+^c=ui{w26rYYZKm{NDRmoK$wXtF8MEC&oZ^6T*@Oq_Q>pjI|WZyw@9 zL_W#D+wVV*YP$5ZIldemmz}|a`F=j*zhMBT|7y&#%QBybjMmKS-I;A$u@dutF&=Yk2{=svrd-qPEl>IW41x0LMF3u4DXg8djZ*Q8yKrn7yfJ^)y|blR~F9PzoQzAb@o9| z1uwp9oib%Eucx)j%}Q4{FHqKD4-)h0XmB;@%Psv^T|;w9Df3>(!Hc9$mm{gmP(ym1yf@__%Md6mp?!ztf%EHe|ZzkFAkgM1veUfASNz_y)Lc?Nq=XeGQF< z`wLC6#NSEY{0}b6Lf5-|!cH?`1HRMW{D2<$iL%}58l%q_ZhwcP2QH>oXN$o3upmQ! zkdsa0$e6~WGtAu;bOzbw&7+&s`LTLX9F6x3k9igdKb}eTpw( zUJQ3-{B((RBf;_X{>LW{;cn&4%-R3(4^NbIb{j2~5q}v{XeO8CE#@IJSat$B@9M*XDX7fUA zG7sZ^ajo}_Xe|=!8OT&&JjQj9OY@z=1uYEYC7Dfw^($)?TN4L$5;2P~K`xWE~IfZMR z;46y-f`SB6ZVx~SsrQV*c!v9Cz`l^0;;(*j;+NOroMf5;($)Y=E*T3P` z#{v9%Gw(HO^53t+rbOEM9{{~TLcdchxqOX9YdvnSrwFnL&2}`g7Lk-vv;zNDI-8tp z+EmSAOLMb9%u*9b_hK00yRV6FhG#2fk06T)C3E(4rnpZ4M5w0?f@Et1=<6&2k1$l zEwC{2q3s>8fMlRCSQs0CR>4Aef(1vu7IpXCA?Y^c=q6{4$ZXfq7=ZJ6K%a;=h+&Ms zq))_R#H`pyeIkA$wiXX+q=DapmpWHLBjLvyjza%?jS*u}7n*Il1 zjwLH2hsfczZv*q~pxn~d^eu)*()6!`B(oW>h0(|4I;)Dw-45l4DRv3hl7U@2b3Hbu z=P8h@z!qu#!*P1az^PrxWeTPK*iqOFZQ0x5`&ZQr~n~(8g(I z>0muM3XRj+NO|A6cswrfayTB;$Il!b$Rc0$S2lrL@2naBt^x?t&#Bq`p0 z9O4qObN_b9EU8X3G8{9uZ6b{xl%u;%fXm8Zx%Kdrz4%B(tppicB*U(JX;-es+dGJ4 znt%)tAI5^qY)Uu4;Iy8v(0AFLK)6t*4y!H6qylA^iNJ}W0y*dOB_GoMWkUZfj=K-a zk#r}FryR+Y!*2w~WA|j`OXDj?_v6A=yI{KAeXt~Ua7h1Qgz{zOPvRi;)+^N~?2!fe zpwJS@2**N}?uz3(&vrd;k9=#?D(tCIj&3}@ALsjCGN6s)IC_FKNYPc}hyvp))hFVy zk~dlh(v1~IEXrsw_RvaVtT<-9p-E^j8(QdFKJ8my4yaMz8k;nN+7>csyx{H*SvesT#B7mi*koH6ZR@Te57gwy zw?6zABrxehNA_kwx`|g6Ta-iL*u4{=aDSo@R8^V6u?;pkvhfJ6LbENh3I_RTaE3iH zLGCt8r_1rg9umQKqx&jK-YAJ#;*Y7oRD@67n++=0QI+wkf|8i}&KP1_CrtT=zl2#9 z*|=MM6EJ&Z?&NCdq*6g={ALm<7Hsb)GBNcztQW~1QH|AgIG-DT)p85gb9Y+exzlCW zfcn@Nx=JDo)H}&4-;1Gt4~kjhcTR`Ny%7vNNmg{pQ8h3I1CWHMdJmk#Y+Fc}wUn8S zb0JdE8L!GY_W_L9AHKj5dmwqlkUf_NlByMIk%E%`lB05@GKK^Bpd^t7(0$oz35eLb zD`*&tOuZScj4Z;kEHw{}?4QM2^LylDMj<;8(3s?tWF^Y*(xFVNaa#mN1}D{ZC<_zIu6%R4UFo+febz|D zAuBj?a7Lk$9HNpb+6`8PKP0m(eH));$!;-%PYOhzd_6ijASe6hq4VnXqG!QH`^c(9 zKYfqb{fZ1wGY#mdT) z#h}F4_Kh^MwqN}mHGZtVMFqb(C}?bp>(S6%`5&RGXA21FGJV7xtB%a3NqkH;nMc%n zSkfJ~GC$g*&k}FYr2dK8pM|Ad4(w)m{S);gO-WY{X)ew({3q(ZEHpQ4!mO!;FGAez z4rc*_n|a75!73zWYH2n`4P*F+dUR_)1R5w@#R?~|!u6zM&t^gs5FQ~0+ynx~whm(P z3#i+!Tu>*1bfxRG92rG(QnbrNQt>CM4{WgLTnfy=$-?o-a8mMV^Lb3YZD7aWLzPt+=YwM<6-yt>M>kd~9u8*l{w^1Vo zmlqg;#p)esL}VA9TOPXlC=?ktZ-Md`cUoRN+t~${gi+t7@N%?kQ`Ck`6rl>`uvIw+ zIp}A%(s-@bkD>$i44GAS*19cHDbdcecCkSH;2P5T7m%?kXROL);JTI*9}xA}GB(yH z#Svpx+)1qHswU)F;;T)>RehgbA^R`?1)OR)qe;?9#1v%hbcw4|VXtl1}M=--V8RIT3GfA))#8!5sN%$x-F~N~IS{ zGO%_9Ea<8nbXh)4w?@dLD4}8+c$6mDKMG0qQ<`Ke!u=#SJRMD)W%I>BdVB;QBw_16 zm~U59;n3U8B9Z`$DGhdCR!u`7_f z`>useB*=~EUXo8QE{O_tSti=KuN|+Jz&&G2`c*4J-L38HJu+G!5*8JT=H-(00jq;) z#(M|D?GHM%`vcqyw7r8lP5E>8vV4+7A`8i8>QS=u7L)t;HV>cNyIFF-C8?Z`vt3B; zW;wEAn5^EvCoM%*kCTx*72zY72&Ocf?n|fH^dL>^LN1JU2|4t#`iu1BOnHXo(97!f zAtg7kk_XaLC(qV@%7@%H9aCcTKXBOW(S5k|DzLdOGs%(t!EaY8XCTsIRTUo?SR>D( z{q_8Q0BQ-=Sj(Rw%h`hYb7TgYJg3gtV1G}_C${dxVuLaIV`A6^^;9~M`8|YV5_1n@ z4xDC1Cy}OM1kh+JB;7YLJQ1fI=uK0?n0PCduQ9SkcafA0`R57-xXY@KXx#uoW)l0T z1m+sLx12*I>?EhGqWPDm;V9;Umd|%8@0eTq&zdujK?&vsS*y{67AIaWgVusoPqv`o{tUSBo*La#r1m@XI>+p@S_X)P>&0ZQ#EF zT2X$%Ke~0}+tP!X2#vuY23O=5fFhwzywW7s_VIRHczC(iKjfP8qW1dg3EeQ)S1VUw z#PZ^`4(gs4Z|}s7Rr1iirXMnNkLZUTx(+=ZIxt8Wy`Spo1{c@GxeUvGylw*n$$dCY z{=vL0j?+a*!YU2Xm|#^Nn8~$YI2M8cyiuih>YjL*->FMrmrj9LRWaU{z{^gy!@b;x5h>Dxj0Nyy`^k-m1qk3}9I#rCxj{u<=b z=)Pvc>ygJ>&V4n6zq|x^eB|0!MflH=$49MwX2PFE9v`vx6%+mw6nT7>3BMQlF{J;5{{`}6N&gAI6ZvtZ|AhZ3 z^0}n{g#Qupd8Ge@e+>EYr2mBfXXGc4{uBNO$mf&(6aGHrCzAdX-jDnw(tpCQMSe2r zKjCYUpF;Xi`0pY=mGqzRcOZW)=|AC@BR`Gw{|fSpkvEb4U!k#cxPw18dR(8(|3t%g zYWTex{)mR}(eMi6`Lpl$8PA`&^5;^2j>mmsJ^oC;B|BM8W%c^r{)}xaduf2bJHYMx;wrVLh8fP^ zbvVy>{!E*{C&Zsw^XDHIS$~c*gRU~(%<9KA{`z|l{vCw&7|&jMoXO}-j4ow#C8PC> zu4nXNMjvPNIYwV*^f05x7(LBsKcgA!rIzuG&Sdl^Mwc?WlF@ob*E9MsqmMKC9HTEY zdYI8;jGkt+pV18V^2~VgC7S;Shuzb(&h5Y1WVc&Q^NI`MpQ)gzu_;(o=LxI}6xD`2 z-nzMXUKH@Vir7nVMPBdv#<~6lr3>dTET8Lld)>}}yT}&`(t>8cZ>`%!8B^`TlCoNt zv%bt#QeNgNn^!--cwR|q>B745()p#uCGPpo+6A?B3rc74&rM7PCG(4KS}<>6=^Rrn z=&dvPnoO*%Stb|0v0z&7_6P7a1a5IdFxVWpxrp0WxW?yO<8=poA-~IAh_6=!eLkVR9*6e?sr za{!C)WnjfZzmF+^YDrR5(Y=inY;Xn|h@ju+Xm-}&yB&!Zn0+Wm=c1>M)OI#{TvB16 z0n7U%?eXvgmrP5mQz$uu`DSzbGS z*;3-$Og#Ae%$!ckOP1HZ-?2nffAC`qMopUW)7JO1`rORH?=*{tw^z$!U87K+@5c&? zhi^|?|6OJ~uRr*GDXXvju2lP-#W;=q{Jr0NHX-;oDyiCEpsCOIvu0M`q?te3`W+18 z^=sIBj{G~@9a+ip+Vyim)BfOhxvc&Xjr@@Jdo=Cy@3jg_Db>bLt^N7Z(0882`n~_d z`uUxd{C@IaJ2z4Kg$CF5|GxoHO9KQH000OG0000%06t^fYVAb?0I;J602lxO0BUY& zFL*dMUp6!^Y-wV1bYE|9b#pFrZ|uE!d{adiH$F+zl`Um$S;OA4mX@VJ+eui9Y-N!h z(zH$6K-+{Sg%%1;*>{S70sT z_xZjwqDZNKJYx( zmd^249VwdsAMq)&OThQO8!8d+^N)3y$615#jf406YLUsm9U6s)nz=Jf&c~SNS&2{N za~Y=Fkd=~cwWP2naGf35zk8C|VT$}eNQ*dnMfK_5Ke}K4|HIc^>1)h{J~B3Z%){*S z{DL9o4m)1@xa3IEjLH4Yk4ByC`)}`y{4}fq*Vck>tp$Iw7W{rKxUZ^aykRZ)*R|kD znws&pzBS|5n%0cBXjU_Bs0F{-oUz~<@SmF3O#fRg_?=pCRg0SOfLibdwcw3w!CTaV zx2Xm1Pz&Cr7Cf>R+~6nh`mCanw5%v}1U*#}*PfGxI2Y-YeI#BKCGlv)k0PJqCw*M^ zE6@=I2a0m3!*tCg<90~DJW$e?t(Eakk*~3?T8Wozl6YI>^J_DtZ!f(}8%*bd<`UN> zNjwsr(A_#R-ngccQ1MNZn_zxlm0r9t^7*}$#91ThAAd&w^IMSQQ`AHfh9I8|f0R$9 zOt|82)V>lw?h%Q&pFf|+Y(71kMZj2 zp?uCuT=5A{g-SV`Z!a0FLjH|0J-P(Re{?;Ghai0-3cfD#d8Q@m&udZ+kD=ZkLfm;= z;%_4TMwEl?w2bh1#G9ZU*+L|PLs(#Wm|y4HlKu;%--qc;ST6H56Y+7F&ZA!GY>4Gj z*+iC0CzRWDOi$ts3Cu_OJgwvtp^1U(jn=#rrZ|#ZEtM zAnElxB>fIdXKx+K;bBSN6X}n(!~AATycOc#HNyOMkSX7QxS=un!#NU9!i;@}<)~B3 z^8Fa;Ur^*@ko->~UZBQwelHW)y^X{-V!7K^OFlC({Xd1E{Z5c_dl=;wiTYqeeNgP; zIn;;ZHZtChi0=$SKG9M>C$N&xqMwY|g84%FNaUkyCi!f@{2I~!4EHLpjcAYhXC&cn zt;Dk$qTCisdgnC$+FY|i?@x~#NzRW9~tU3CP zPM&__M@;8=)c;e6w?@4!LqFdZ)1yZ{DMmdRiT<-Q##`YPZx58eEnVV@A83X4qK}gK zU4n8OtN25IiLXb+>ZsVoOo?|zMkladhzOH}dgMPwv1^Ps75Ps^IqTn(e3Gy~)LgNP zvyu;1+bk0Ch;tHekNihtJ!`{ywk6_+P!Ee1$^<4Np9H18cv0%ZYlt7gcEVI_FQFPT zRgjct5y}(Ih{d3N+3YesAK?q1pg%13@`q2LA0E=wlmA^z51S+Dv3tjsqujKJEBP9R z^_TXT6y!NHqzi~AT$Fee(r?E0SZA+zzr}i_a=&EI0r93f^v@GK{qt{FUPaHz{H7rO z9r_cSkCbyW6p#bk6$vdReUZXnX?J0LYC-%-MW5f0eClIDv(WFc_A))+DElIx9xY^z=ep_(hjIx66bdL^*dx`RHy+ zydTnkgLV<|G1>*@Hv;uJ;;>}k*I43Dqn^xY=2@S9gmNfu=-J*5X^(c1Bk_sI{}8t8 ziqL^6?cs%}59jyFa#8Fk2J0978PEDK34jtS0v#OG=M*`y;t$LOlJ?o|3>{}%{}%1FhhMy zmGbY4@m@kdJi3`@`kSD=M|ibkZ`8qh*2~|{K)--vXX&Z{WsW@?$9;%DT&4(WeFebWy_y~6zF*O&BVYR~>`754ioK9z*&NdFMp zZ&{j5;AGVE1BxI1L=q0g^lV1`uRJ9IrN1y%sW0kE27e;`Ux@2EOMEQWBR#QQ$1t7U zFr9CszEw2w^tZcFe@cR-Jcl7Z&|lK`#qv_dN58`KS2UFA{}u6>7;gl|y9)95u^*)O zlL;Jw>Hk6L9|cQ%4(8Vn{f6yfiN~YdVzJ%zR%cJW3di&(X2|p-J2nQ}8|NRA>C|Gp z6c=@-cZI{ zEHZ%~px%CmxYjE1XBtR+MqA{Q7g{5w;%|DSCzDONp;YK`Q;G zM3hhBTQZ$bqGMW$dcv^WLyAV(FYtvt)SrZ4PdV>L z`4=Tiya&>^L%XXuDFwV8<=jib4@+Ph;;T{4&WSSKd5G^(`W@?KjLLV2WVDN-04cZ0 zh>QN5tK5qupp56VNB@@aiDWzv(|;bzJ>q$p@SidLdzE%wYndK&uj~&@Pr_!Io(srl zBi48NCnY`;)BgmPuMOK*O1t`&4(-k;<@O@t$>{%o_4O>T^C;)CR7t-dag8$0(?C+r z!gOv|>eG!f{iP^}g;uaQ+i{qjZD$n|KDax5`!SO)M?>@w}k4k(g zW?(I*v+STG3`R$pkNGM=`~3>}|B3bEAk=fEzc35snYaNnis>AvQv867a2TfNurfY& zK>}YRpE=mS(=NtwLgbT*`k>ze99HOXy{olEJXjGfaA9~498k1 z{rBf(`uiaL_e%S}v*a@#8&Xr0@xu@qZyC~u`QdnPQ&0Q7p!hxPUn}@84CQ>vv)}Zi z(w^*(=|s7`rnI;3;kXtW{Cmo{UnfsL5Q}`YsE5k9N*^Wtb0q#9c1Vt)d`f&g$Juhw z4^)hj_&$`|=V)IgCuEGdi0c|F{3V`@e7;2d{38-q#vzBIK2)Y)J3;xbiT&I}?00WN z`UJ!i=3_chZ`&*5HrSqDg7n8Qot1B)9bq{R!+JqC#?M1;>#<%-#P(`9;(w!E*wUn& zMhs`AY`-q=b#(y*tUxf{Y!C1dks65+mZO}ik zqcY_SQBSrao_JB>eLG3K4CPa{O7g#l^aWV&lzG)VzbO5<0?8*2%QsvZCp;kjX0MpY~;g93qHss@nexu}hPyHFiP(E1S z%|U#DGM--`^V=dw;u%=KvkNleg^GVu#*tb}&W~XPTa|G${Jx-+S0c76ZmT@|kF)T* zPsMhLDE-Hl=uZ+ZOZ;I4$M)*^PM+^ZuUoX(P;UVn<1+Zie#5es1MF&++-G%6H6NlEVaK^c3Q{5(&hjAPkC~HkZIZ z$Y(FM(<=8#ycqeI6!|wozk&D$rQIIv=?69_;|zL5BaAC};g6lEJIUCr25d!SA#9S&Kb~e*SzP$)FF~S9i>B1=dqad#pE>dkL1i(jE@N z^i(!PJHmeSC8ZvTmi!g}c2KE*S4cj)Fy0EZqoP)lPfu(Xe64(^=;ZmGVy+J9XL$B& zK0y76STEz9jpf(?>scGtvoT2j6@EV|!G5CRx0_(UKpQG?r5;(X$Op#>t(dR-IDS&% zML$)s3oKaWd*WZ1&Wf!vojSx{!f}b>rzC&HKU_!t3C+FIgMKCg+oc@w7g0V5GdUnKdL zBfbXh?ydHo;}3e2e+A0_5aNSSe?l~#^sgxM9M;Hqji~3jn6Kiev7SQuVCB1;S9|Gc zl$#FA@m0i=m3G~WGW{y7__Ogl<0P-~oaN}BwYk{-LHW$Wc3lLv>)u8BZOV79!xC4< z$4)8pThOm|MtXxU>I2F_t@OXJJ$Bw}JpUAyqqCJ~zcLT`pU3$#KO?R}|5L0*eL#6$ zQRX?|xa&3?S5afTXco#r8UIhk`n1Ta{r0}{J#?;=PZ!MZALwUv2V^=|qTK$(`YQp) zqm=f8QPFd3Z(wthtwcRcd`IF+|L%RnwVz2qi;lQJDff@m9`Y<#>a_|9Bq(xI>Z3rJ z&XS!_os--{ce|BT3y z^88$hSD7c%KvLdE`nMGSc|ziUVftTG<{f$UPg51UZX(mU1^It~_4RqRr+tYOi$5Pc zi!1r3e&Eli`mFlj-~ayp_xE4?#X!G9$D|(@_apFL9Le>BE&8w_?diR``d%EO{Q$0( zIhkwf1ylJjZBU`hZsEUXhdexLnnaO1F-k)CLMvqbr67x^_c!uC(a2n}J^< z;0blv5IL9t6?NMp;qUdVo#+hf0yGab?;w4jQxEaUCRKzhE6(Z+igAh*F(o*Obygdy|!G)BOpP;{Pe3-B46qNCtGLG2lym1R}$6LiDbUx z^-QhCa&+M;J-lwv=(Vh!7E&bZY1I%zz_Pk5K`TY67xhqpAP<04>hvNtI+w6DT5W)k zqmYL{AzRKuO@K>Ds^^}#5~We@H33dNBrrfgBD*5TYK=~#ce=`4tx-dtDggdEglSZw z`w-yD(`lX1fo1A=cU=sPD7e)6q9Q`_MIv;rG6@=_TB1*Eq5;vd#(LrIoh*21uq%7& zB6qP>K+WhEfFGTqW(fc`k2RL!_7jZNnjw&XokHIjhqYXxo?63vMT1Bc&=abu_z0%c zR=7mQGd$F;z=_Apq(S=Ffj!kh&mscEXBC-JSCN3$h%$4koF_%0F){Hz zpl%@3Lb3uh;@j8~VJ`Xzy;cZqnJbp}#LGihX+e`4CMwx>YP8S}8^Zh_1qIM)m`PO> zq4TM3)IpX(a|+c`HXYQcacYqm4RaZjOp!={JPBVQ=F$km5^YS;P7_Td@tsqAn-lW} zM3+hQ`-Vao4&fmt$CJiF7|-OG>?8=&nHi{7fLKl_%-Y>qr4}icS42BR7VI&&< z7&t$ylH(R)4rCI9RF#~sk_Ev5VLpU>2umQW#P?Rgc`bzX5H>;BtdifH#IRK{gr^`p zFVgYItQP}5-gq-*#@&x?EoKyEv=8}uqS^4r-FJ6K?HqM%(+B<5_CN8BVX=Do<;9lK2AB|Neu|x2R*{@_QiM05> z@WTT0w$~ai8T98*4PMIH@c7W8kE{)z82RcW-_4|MK>-PdXy!Nj())mh`*6pM!=;dV@kHjV(HZ-wk z`xKm6aDK*HNmqwi?}zvMZDHB;B=zRZ*BZ|58ukA5qovNjjO!wP>zBWNOq-=owHUYj z^1Yr7GOV`>M?sw3kAJYNezV+1XB~e3pZ2bG`$_**8$tw|-RgSMj0aKb-8`V0rrmO|wVDg=xPVbW~gQjZM|@tFu9CRxE81 zvNC^MLB!GMn9BX<&Zis;tn=L~S66jhv}tgD`BwEOzxK;t;}$IDHer5mWEk)rMXJ*9&UwJnD#G1>i zPF+ZE+0GYfcBZUH$dRQOio~LuR~`JH7AzsM9CfW}MQdcIe|jOl9BqNYH>4Srf8X`K?cX zW5e7Qv+G=LdwI4w|J<+dz4pOBSN8pQbjQ>Ib*~+`-{G(MLw3G?xanIPzIw6G1n23? zyI-4gLDOuHX4{MPSN+!M{INfKz52={Kff{>(%fd~@xV35H{aaAOY(jl-iG^)9J72w zaMX>?A-VQ2-Gt+;yWyb#z|Q3T&~3(&eistL5897qmG@b_@}q_w9-n-D`}12`d~|+e zgJ*Vy>lU0Dm;cV3Q`)n~E>z7f*pa{D(%xPzGOy0)ba(ff*;DqIQ(ssZ{Onu#dBc8R zGWztfPh)+SRIrG-lh<7uS>A6xJZ)Afg%?szGv^XWg% z{rIZti<_?x?rn&w|Ig#kg#HYj@zk0gPYfHMU8}Q`CvF(rfr(K=9x2inE z{=|+eaW~&-S|=c2$-GBPl6r4wF)yRr$#>^J)pd94dCitw|8U2!4}Li6zt|WYlX7Zj zpUgRjDkc^^q3g4MP07UQkJ!fx`|D1>aHz>wl`qeG;qj;!(vshP?kml*?}H9Dd3ArY z%C7#i)`cvN?yE`v`uBKh;M1R|7Sxq zFW+PpGdk^=aO#)eojL0QuKT=tdHI>1XA+mb^GT;~7gYJg-3Y2QtV(S%_UPG3`+5)S zvOV$9Pd5#k{&$<8vOAL(O$_>RK+Lxpe9Jcz>Rjmk-2PrId`%eF< zIhMjBlYacJ{er?@`=5O@ykYb8ovywA&&r-zodY&z&3`;_y@mOG- z58GGyco?MXk2i9Tj#}`}pJ)Dhx#PXzZ$H0dS#a|guKB$B&yUNr+92!Dgni*(?=Q2z zm*U@ZZw#d8`3`4;bB1<#ciE%;cI@fsxZU^2$T~ZYwSMnn!Py=kz4QGqd%rvuJZDF@ z^uK33^5)+W&!?%&zg~CEU>@v~pYZl2=Nm6amxot6e*dj$;iSTjmv5cz;)L@byT*>Z zKjm=#vlSbSOW^*`*k`XyR_*HA{JCMTiTnF6ZawqUZ*33FdLyg!xwb#d-}y+>k4_yM z^M2boQS}?I`QyF18#dqUu>WXO{vSQJ*SnUMzdN_xovi%>C#HTD6Fk1s|Hj#uI%^gV zsE8jkJh=OsRt0aLp3!*MudMl#1v`4YGHwWaw6*W>r^YwFwCw%PmY;{E98q82$uDVk zys|as-FMeUAAS0Q$!GiJ-iPB- zZT*4rXG6O@w&K?AMycyB?!2GzUBgHEje72l+{S~)zp#DmUyUv;djaYb5njXQf#}&Q z#!~_L^@IDi}C-*37=2#`~;J82Sr`G9Wl|C$VS@7@GR;IOBKaS6C;<m=4N zIlt{PbGH-4F<0fDxA6mW&&3gK!R^>iROXrJ@jWq5CD~-yx z=X+lGT71V(;YkR=`0OvFO@#Y8h>7t5(KlEDVU|jc9Vm;2#dp~TfYpOw zQ@OvNDD&Zz@o?okd3Oj!UV0-A(Y6+AdLAXu`r)-`TZ?&XuS2-@;Hw_TW8`{~O7>Mo zspR*k7zjVY{WEYLu5!=G5q%i(T~Dlm++9Q3#L*w{Ddb-1n<$5vk0`#??1t;JOwKQQ z51+*``MqBG)?NbPC-SZ`hg*D7&4VDuCKf|Dh-nd1rbNF<^e;reL#}0XAGt^;)=bvN z>#1Jp7wiA@c#WUFs@II2VX~hj=1Pcuix>+R{iJX4xw5uWOif?S+-oqEIU{1tYO@;T zwjMFDmg;G|?&3w4?scEWWM4};rqog%|F=gVxmMRo_3uaf|4j0KeTeUlx*G6${|CI9 z)q-nl!D~9iGC*aHk22Sx2c8=fy773Pj^~H)EXEaL@j8Lkeq@vDdE{F8o}TGNpZhPo z#4(z==WHuSPA#;{YrukPz?8Z3BWidij;F|VSFdLcYI%nqVy=DLTApQ)Yca=Cd@dj2 zMI**&Hvfk>IJp*U#<$@0BX}0;W4>f^e%_uM-ak+SCiZ%Gfyr;AdDn5V$4doiOEdP7k`yKc!;s4xAEGC;El+&fw||Q zE5{5b$FdrsA38{$DRZsN5C$>#TIzp0#2D9acs&Wv*C9kBfAM{;bqzkR4`92P919ai zUAVsm=U!g4rFd56_lS){#;W9g3Cf-&Vh;^5p4S0sy5m{-CMni-h;h3ih>Nv5V!TeQ z5eg#liD5)vulSXDjmlOy%KSkw?d1fM-)re1<_(E)JMo3$A@WR&19nrn*TFgPEWTF0 zL*BnjuJ4fRWwq2B%6f8VjPbdjmfztW2z;~lwnz5##@uaoNIBg^gO%6oNcyS?8X#xvc+z(X0c>*hQpYV zoMy~6Wu>qr3s9Pp9mZs<-F=f`O^bAxfYzF6%W;^ENkD2eXPL9pEM_~)vSw#OWOj+$ zS&G>k$uwu$%?@TBZ8l}unK{?SOj!;K#F3FGrKL@Y)Y|YrMXj@ zZZc)}VsmrgU85-@19_X1jSe&1rDmIwMMTNAJjk@XFeazZfg8KSlsrdV8&ge|4Dp6J zE7=ISbG;4x<+Ujz&FJFpy2{Ct*Di@!jTT3`*NUGsglnk|UzXTL{foO=U}g zB48H#+-yfwB#E_}O1$Ob1;t~^v}K5#rmcu|X}Jt&n2$&L(4X`Bx-Gn$i?+`8UVu9H(z8A=$`z?2OI@6x9{mYg9F zsb^5i)?|}I6uv?Xid{{rVDc7q9@NQZaU`c3Q>^o|T%uF}yV;bToX(7{G@5c8)?{mT zHr{5~l4aUcEszm;X|ZIxQj?hiVoL%Qns3TZQ69?jCbIKDACQsqpjt+t-sVhOx(RHN zzDk8AR6-G&xVEQ5QO>D;B18z;iyQ^U$!e11g958rT*xrRnk6hzs25!4BncIC=Reh& zjoAl{#Voj_nk%FTFs9#@lVLXtA3*P_tp-#+%WW;>&S*0UW+J;5Q--uhm;6#wp&+0T z-FM0I(ju?nb?_k>rerfQK?PAvHP>d$wmPID3CCu&8Ey0CqCx-@yCq|e(GWRi)P!O3 z8ZHJ~?GsH&#$h9-7{~RBlyK5~U7EqNGus?^(fuxvwxpQTlG|uD3INxkZOLy2o>e*I{ zi9AE`yMdf68<-IRz%wUVT=f)vE5qd@(EV29SiCO4l4Un%yV7m9W`lxc2}flY3j6Pm zUG>I1Q$~&%6e!Elt1t4OFiw%-I1#I5flF76V^dS1WbL2;5pF^yefUDcsKHbL;MQAL z+;nLWv8bpr!6sawJtxx$bpn{SB^4P=G7K?}$g+^T;bzAKD4S>{M$i(Mf`|%mjuA`7 zZb{38boRm5#u+D%HaUz#2P-c^qF*U=KCgo$aIq*en%_!~}JV;G=rX|H_ z&C1C0C~KGAdQgv^V02iGqsDuXS+Y_r$%=k^Rs$4*TEpx~V-oyX# zpt_ggFiFSf2EA@wr8soTL~jU;-kfc-W=Q*%4Xg+BL`L9^f;0&>YGQ)X<|^A1bFLR& zofaW?do^xPHi2VylhPb!S-mnTc!*Xy@q&t=L~7iTK|Lm7F^=n3jZF{{*=0))d_r%m zng$tN3IRSh4Xoc~JvBY40gI}YItuj~T8YLC^E`8ghe5iE-6f_ZXvobWkL>B9&F-Po z8TKSXAWUOuR!yccG|SsGJef!xNqFG7CL?Ukd9y^Vv%4jneZ`#~f;s_{WDcvQ=S zTHmN8j$X#T^ntssuw`53i9*h?MH=IwZIx}Z*_0A}kZQz;@z6pYU)y8R1*++}z$0q} zjZw8q!#HYOZSfe!Kfq0N$3@h}UJpF-%p%oamzhYnCHmaYLgy_e(AIh32*^!;$#zD`3n_cu4=R-EF^AT41$+8uhlbt3;1}v!-bBfZm zbUTk;eXHF^-`)>CRvs!%!0hZvuE8F6A_NBQ)@)0z8}o9iUNAaU1(hYF)Yo?RgDk5U zT(YN|pw6w9jn`{J(#ckHDtVwdC+RLd=|x}63jx_yBlhI<92DD}NE2;IRLAdb1YwC- zZFx8Vrf8lRtxL%|k?X56P%Yoh+cts;2 zC;9V=70NZ)3SCQ@IgwtO)Tl`4IQE75TXbTKy|4>eLx0v?_to5ubw932f$2l4zzY6= z0+UDHa#0?*bu%Z~;f6icoYdp82&&24!vWP8)oO|K#+B`ej+vU9SkiCBgy&zv1mJy4%whO|qtS2dWHog<-0mI<06GLk8^ zvwASI20X>&FnQyylzL&AIT;R%u*Bqa6IhoRR)C(U)%2eUtvQbWk`h?j)1fT>E0#4< zrdmxB z-~=oDL{;smu$fCFhYD4Lm~Bv^uKR4WM1UVnjt87C_Dl#Z!z|207PFMNKS~+|=-_w#=9^lPq>?mL<71ulet^HD<8}x>|Be z3-JNCmfZfQ;(4HKAJ92><~%(vNu$RDkC$eO` zQf3dkm5mxPXt20R7B_H)jssqq68 zFUCa+310XNoMbk|3vY~zEE2u&Q*X#~x)+{%LY{42c!zR)?uD~-1`cOtI! z!l&SUh!4pD+69QLx;jTpoB41v( z7V%>5coE<4oqohiyzm6X%e-(K;-|duV#LpT;gyJ2c;Q93AKVRZK8RO(;U$Q(GhTiM z@c=Je7btP97ha5uFhacfAg=SmT@yp$2=nHHc!U>TiFmXZuCFKYcrQE=@dR(YfyAeH z<2Z3P(K|her+eXLh}*nz{9&wIFId=dE6-ely~{!MBVe=_yE*5@AAckKsUV03-QDcyzB)Rx21dGTEzE%C~+}=7Jp*E zbvVzt@iqhxIq$|h5d0LO?@aJSf_EYKHiGvc_$k7_C&6tbodXCSf%y`-5o-XHJtyM5 zaOH?6d?p2>n9@KT6~?ir__r&lrLy5I%H` zPch*$i_#N5$pnuee9{TNitwRpETRd$mEZwHK4Lz+A~&5EUd^BIp}3yl4uY4E^wV_# z=_H+nl%C*A2)>H&Sw?UziPvM@2ccg>;2VTLU0ab(=(iGl3c>06fC_@YNbn-UpRO+p zBk6pZ;Ec#wtdUdXAL4~8M=s$*aT|&E7{Mg7{9!==qBzP{Nf1BW81V2M? z9ZBa01fN3qpQG^-J|7ZXOXMKdqbTwT^TL&58;O_V0R;b!;E4pMYZK#1dTtWjN$?*C zzMtSf61pwF!z`EJ0R&GVcw>S`ka)Wjyo}&_Rm1V2yWT}kl$B;NG|FCl#BI$b@Xe}v+M|7IF5 zq2EUEqlA7OU3FMgUlW$@?p%;qRJuDwkVd4WLArZsS-O#KP!y2v?vw_Rt|b@Ar59M> z+u!%+o_p_^ciwmA?DO1v&WuDK8uk-@pEvrZ5==Eqpvt;JK!e z2OLwilL&|x*Pb@`j1@%Fw@4~oiftOTLeh7u^$3J(i%h)6W|W1|p#xN4D#$1DT5wM5 zpo1eWfztCKU?7PHS09i>G8}S7Vypx+E`LTZRCxsU`I1P_q8Gk@q(B zr20yUkscx?3$XY>7Kl@Yg$%eEC@Dg)FCX1i5vGg2sRE}WSxY*U=~J5VPk<02r`vpc zT?nao@1IDDFa&3kAh^JVFX0bJo?*f0J#XFGr8~h8eeB>ch-T`Q{)Zs;s}>xMTU>Lh zHPQ7jmM;)Y`D!-Nb)%Qz5U(CIB!T|dyx=)0@|V~ex4tp#@V6opSLkLcNE2J((xDJE z9BCvM&VetHun}J1mZE!2*gN!THjv5TGfs8sQz68Q!JVWJn0n>J0%rnKkp)uxM}qbv z5CjmJNF-g8KNv5g$9{YTPWpzt`MT^G2r>if6C>zECR~UEIgvNF{D<&~s&5(SA{mEt z6iSR_u#g3W-A6@=WRMpeMPjUq#N!oGXa}C_BE3SFQG|iOEv$j0*c%BD9qix&*n3vE zKe&`E@6_Fj1ulprN%KFTZvU~Ryv_9z^!oZ4z4QT#f($v~BYG*pDJdB6@=6J(KncbV zRw6@?_W_EL5731_k@j8Un99Kv(R<`zn#kaWx#9%pLaLPgmlQ>w-h`rIOXovAfsse( z!y(he$k|X=o|xgHNJGyBFeH7H;3FJl7M&v%wPYjiWI+0N3k)R1=}v?ELwD6gx^YzY zL>4H79H+y1)$#mYsXwZAqVH}6gJZ@ z2{^d`B7t290#6qqV?_!XD{vH~-$|^9IUMOch%oqB17-|9`-!~hfE+)2j|BK%sKJ$7 zvM9(p3Hxrz5LVz!;?86wq$ARMez45@{{qLgOkOb+O zGuVtt^kE`FMg{T$e8$q60m=W5F&5-%)N>eVNZkhjg3$Zoa2VCy+FgqaAYRYiAh2)A zJyJC20XaD2J(A5isf|C#8CM%fB(orGNadx(kw3YO6y4g1%t-oji392Sa!EV0kO|lO zh)%=|LM}E1{Yp3+c@S&#q@PG!Ao-92$m6dkogie`?)`_^4++xrFml-$Y=k`}?YrZS zPKZ=p$v_qMUnCT&!Vj>!!N`9FiBSEzoO7=~Y-lKzMl#t}mA%$~m z*EbdKlOS1p2g^n#E}RW4f%AloT>Cnikx7yC@@mztjrc!sW+TYVco5GZM%aS^Y;cX0 zCm@LC8#33V{}U$!Y4$l=3dHH4i*D&d2sTie{I-c7epFveW7US6xGqC=04@B7JKr#= zMY$+>8i#rIZ<^j{G2Iz2VGe$OH*PlT9=o*we=SOF#EXCG=ipEd9}Jzd&zs?LBg|Yb z<9LQlWRp!?SFo3kp`2ak?$;Ix!Ta5b5?_c47a(@vDUZ)DHZfdLCchMEt^yv^=b~Kk z)D8j=F(`TKHMBmxS(4T&&l@AFoM*Uandc0v3U>!_C~iSxu&-7vZV|J9Jer}s8i;7$ zXH5dkH{5_AcZ$KY_|>`~iB18E0ttf2#CSRw6)ft=4#G0=u(YPvG33S2+ql5O*USdI|lo-`fYk*M*Lfg@`{8rax|`pmjdH{R8KGmkg4yM9ZC&)xJ^`s-?c@yas%^w%>iFo&0 zRO2z#zbLPR2xHCOh8?DHQRP&Q#l9V-M(~9{|2wRa#LmBed1&KjN=u|!K1<+_2}QDX ziqT)tUiM|TOTI2Ir~kY-vf}m&{!xt6Nn;_)mJx9Au7L#*%UtN2xnU3dgEF0m#$3=^ zR>l$Rb)MK+>J%q>z`naKM*bPqqal?0QHuO`uzn1Jg-PF5|D;2?3`&yU908<&I{prTJACe3fgBH!(phDtBkE@2Eno<#wnd zHwQI+(|^hbyOBH)G5%`NIg?j43~cvi?0#hUtxaEe9uK@k9dRN2(=nc|`}ZFSeWpLl z^~m3`k4|HHuM5$~Kk)oC)u>WV*HO~z`gPm5upqyW_ET+8J5*O2V{_I?DUQL}r?KXB zatLW*Z2Q@+?(J^jjKsH6VqjYFlp7$)*^nSJBcGcGBiHBgSNU$?#5`dL1tan>-@{W8 z<6>)L0>3+@Jw)R0*sivCHql0n=Q+9hia#@iU;mfLcjJ+fz^I#P zdCAUEGh-Ft_02k3PoSXqtI(+F7biFm`Ne@dIU4QV4~m6JGb2~-wX@<97 zABvc3z*Jd9UBeV*Il{D!vP`a!r z?zai8mV!AN;sY;-zu_>xi?$JRZ z6N1;Ky% z&x;2=Xp7z&vyl@U^n`K%fL^9?%xQ9EUm)T99Fba>-}S*b9+7or zUhyQT$Go8sj`OKd_G;DVxWxbPWT_Doz^H_wKeg!6c#ZAiH}3X*GuDnaO_@UD`%{W{ zmZYF*=;)S*_~)rKLFPA71i=~!@b#`k87M=t%zs{6zCa&)izWBmQXk1gGG>k${)(7g zUb(lPLVC%NdxflCT{}pvNN!fMr6ATnJd(c=!-DL=#Jd<9x8dKN=Xl~u*cg3GfJz^8 zCcpZN6X5{*i-1bIxM&|);FY1UvL|M^7xpXQPsV@c*Ob@a!~;#vHS@5?E*qv9Us3ci z#0ex~vOT+qu~OT)fe9%UkfDGg*XU@M>=lDKyHDy&MuH|-g`3u}GBPbwrtye*O@}`3 zzD(4}8~Y!-ZjW{qWaIK_@8$1;$BeFhYoSJ(Eo!uc<-A* zglWJ9Xq{UcmJAHfAeLtmlpgI}_$w_ft&;3a>iYsfi}oaliFlP4WNDOHlk`|}{YgBc zaf0Q5cwScq@IGwYth^AMv?%vqjLHVC`;lbP+msWI8S@VL9Qk4V4E`{sg*sIvu2G6? zllkig<(U&x*Zzy}PurRAXQa|<@OoY1SSF`8RL&B!&4V{TKS*N;a$NKLilW#ACSjsH z<%oL+{k;YUKBAhKFu&t-g4V=QMUPM{K0?RM#VFCLUv?hwLuEV~p|*t4Rv^zJR8U5Ef5 zPiwgf`f@W2yD0OH*n?U0Z4fFmro3SIhNTL1sa*Ud=(LaL(C}N__Y*Ee5Z`~%L0lf9 zH-fBaQGILWY4q1>Ch&8F)hAx2t{`NA;6YF_u@^7qV(ZtZs~O7T@?oCr8;?I5VliSB zF9P3aRo0*qalNL8;k0*nbQydfmmx3arfT!*7+VI%br+`qkcIXwl<@4#6&$8H=_65s(=S`uc9R?ZrML9i^8y zEdO2^5Ph~Va0||YB20WOJQRwuA_-H2B zYAu%IHX%;`5KW0kixR!;Kv~q3|6bDD)?wV`VrafF^!DZ;braa5|2FvZ+u*ZkL^sLZ zEQuQ)Gyh8s5n1TV`8GQ)!*;a?S(8|?`Lxt6>Y}*#8H~Eq49+@#k@>tzPXLhofo z&tC#8mtPCVpBt9u`@(YZ=G)8{`yEvEK8fV}6^N-y&W^70uqH7@@_08MG1>EtQJ#y} zG5_G^43-1salczueoM7bVaxhru1a{3`>lXCA2++K<)nLbToF^-w%->Bl-*N1=COG~ z{QW^Yp=Ac=qBNzrj?X8J60z|I&%8IDT-|u&~0IU!z~dRB2ahU|CJUe|a$bMt8BUn0umW z(mLan0#Kw;xb!#TAE%GW$VU#vy1pz$e)^(Z+omP2yR4sqCd~%O&s~w3?Y0Kc(kuZr zKe>s5RL+|c%jNwxF_>|twfDi`!b= z%I-LI9l{1ed)5Z(=6vt{2pdc|n2Rzd^3?J#-n!FpgIxrtes8$Uw@3`$U|4z5i#wKU z9O@HI3uZGjzg5_NQe{aXFyzCMLv z;wXj8&kKr5Z{K>&$xTAUuf6kM26Sk^TwKo$?4V)7yVh^ndHb*kMpjF{=tj=FEDp&A zr|e+Ak>JQ#64VFY=FH%fyT~u1f8#x?jXu?in4M5)FPbk}u!G4+?0u};m8Tdt;#Qdc zd9n7@{Mnfr?Tj$=mJnrn)NP%l)(=ED)PQZYB~A;bh1fVWi-rX6@50`N(hAMloys-* zEbM6)H`b|;It>bvJd*&ezu7YJo@^z3GCw_HIqrk{eKDy-xg}ivVnZALGx-!%<{E7) z@>7At-S5i=huelGx5t9}KHs<0u~)A^rm?mmqQz@z%`F@2X6KA&9__<>MG?3sL)h5o z#H7r-@&+j1u=`!G*@(H3?|qJ!doLVn{L#<4X$i*Q*y^c{xNV{2!j0#?HtMv(yyXD~ zBhAjAgo&IN8sG1{BM56+!6Z0btk9=Q6!$(v$H2g7CtRE<={bU3iU*E<`*i|4mp=Fy z_ssAt@l+_;Il}Qw^+fe#hHIH88;=t0j>ad=G>kt8|3a;!HS9e|58!iEif@X(X0uGl zT3RgTO1;b_Qjl-%`S3B&Ov`HKH!Jr z5vChc;vN*v{qR^nOH1{ltj+eQ(aoRao`N@gZ426!N+g~i(>5iU8zGBKo%W@Jy?KdXL!lk_H*bOGo zNLDdoyuVwjCufDQ?2E@->sYSR)m3XFPFoCY*%r8?H(!xES`T#w@F{(qa3yn88uX{B z+c%rYBzHs%N$T_6zO6i>@~j#}nDHs8BA%MFI=B{o#fWWNnsgfoa4c-aZR%1T#SG@4 zpOYw8G7ukqAEH&~(=@8=BEKX!`lb3CHg?WM3fg1=St zDgIoc(;U5n34tOPXd8qhPsSUoNbDk?b%=D%e+!#K4sVXGGN|n2HwJS2@~~^HI`tWS||49 z`+*X_fd4LlIR1u^`Xn04T*V`sqsnw*Udt1DAt;9H=g)BAgMWcNe2+qXG}FR#cJ>9o zS8Eeffgy3e-8IC{ zdJ}ZJMPZmTUFk)>-->K?sv2=Z=d#u55aC@cI(0@L2JF43-1N>WgWg?{WSZ=dubXVK?Rl3qE zl;};!R8$pWEGk0yPc4{)R?oD|VsUe(RwBgSKo?+Ek^>z_B;D7~baclPU06jpB9^Z4 zf;nrdWOjr@Y+ej}S{D5hF-2&uyN5C{!=G$2P4L-Qn&30WW0=8KHPe30uK{_1quM{J z-dPSH6Q4(AlZ?eC#y9IaN$Dar(|XdM3L}K=dvO_^U)+B32};UX|3^Dfo`@+hA00XI z?4dxJvmSjeo?y7uJ$!s$%g%XbLpCfTY#n)EgE1tS7{oMspfbe^euE>wC;) zAa|Bg3G-3iH(Q8}sCJ%IDQ&hWcMGTq?WI*uru_R;?CE~EtJJD)RX*8k3^1%nPA5HIn7A9aJ+4~Fj@BV8={68zEqkXp$H0>oZ)JsER?&# zQ8+h?fEs@&BGvHVC%K+(-WdLZmru})lTWE4<{P-g++GFImjX<%O!&41Cd@Gu(m?oy z=QY;mcT^b?!W%|xVIyo|eQXLNrjO!VUAk)NHI{qOkH@m%IT(}`+205b#^a@m9yd9cS5ymTU*MfKz&O1XH7l0HTj!7ib>P~E5p`cVx1GZsWCF82DE0J zUeDJ8j4jkPX(lFrA>8nVvud=oxKl@+q`| z`<^b?nNdr6kepcbQnoi#t_OqqbO?je#gK}+x{IDVYgJ0wyN~9_p${L2o9?anR{EPm za+^S%5lBT%0A!cU8;z*^Fujm(QNT+~hX>|#c8@hyP-e#Xu_pA;EjSFOBO`=OpI^yE zf8Ev$TMLKu)=XnW@zBj_c^?`NTx0?T#gk!v#l4TC2?VZBYpsa3A$3%!T`nWGfBP>B zRkenGh-Pj?jb}Lef-7nHil^X5p@k)6WoYv_7-3B_hhRywU?9cHVQ&M}2&2Ggt{t%j z7mg$eZSU2Wo?+nFSZS90`&VP0?Rya1+B>Q@w9uP_{?=n;@_RQXgYye%0cymqm!0gw z>WdRu*yQ84(652&L(SclHqK)~2CI7A$8DOAOPPnV`_CQ>Hye-loaN=r9sbocM{RW! zM?w{755PJ)3D9?<0H>q^^%IetyH3IUYnr@1P${Yn$P16|?)=h8dlk9iGGK9OvLv)T zsr2?ESC5dcQ1hI_yy&I-{Lc0BhXm7A{h-fpyT#AoudY*vkrhunc{fLvF7W_!d0RPv zV5t&&A(mlZIK(#)O-bXnp{4U^k?-V7&$Wt2?;6}C6pbth?DFU%a4@I&`q1h5{@r?C zPx%oPvD_fnXPw*Z29@Y@e0V1;9(<5KZ0|fGOb}ws@aMOi^wmk$;wL9giCKr#XA$Ra zP^&`6b&I|i+|e$7s^&zT{K@%#+eJNR)n9rx$a}W{cKhJ9zFzY@kqEWYW0xrhZ1uSO z9zSD<6|tRXc-+pF818CZ6t&-)7AW!E>}IQ5PWW>J~lob8_3F42B096ZE z__whgmJ?X~#uP{)u&CRRdl|HJ)74!`wB+e)B;~W$@vibDmutC-#ex(1)lOI_SP<`7 zBEh?}D`%+t0hey^U%<_WVwZ9rnK6B?=j%7~0XqP_o`q{8@6_R=z;{?`f77P-x+59M z5Yiy0xaS4$(9lOHfPj|IFT`YXj^f%@IEP`a;__e^z__w@3@U|FYG&>=iq#5L4%kWv z3_OFJ1E(i+7VnS!NH<&UqM-4lr( ztXlv(Cr9-cOYBZN6(`F)z7GxM?sM6PXHAuIQ&CPX_CI3JM9W|IrBi9dJ>f z?>CT{uOLGeKJBEtinCV<>UK35xJYyrS+zS|zt8L+&M$vjUesyw?Cr&TfAVg^L|Zt$ z#xTWDdT-Ub{GPV?mopRI=|VJ3@ZoQKtEdL`uzXH<4Nta1mdQ{5&pF}--hXX%3az>$ z6CP0UgKdv60sfs^C72R96*<#KWwd*n0duFL17W^Wg?1nt9g7Ed_2pHqOu+zW!qq1z zo3O2SUHOL5?Y#3xoNi|sM+)k6l37S+cOnP_1+a}hND`cqUa zKrYU|w;5NbG~6%in)quSYSl&4fpSum((O?pc?NCi_zk6y+Pyqxcc5G~o4itI4j-%8 zb2ylFC{jAYl3IB|Ie1uquQpEGK)jIBWM#pLENIEcSV7a3Xph4Zb#Y*7aq>yX#h?pE zQE#jf*}0hx^R4$;rQ-%Q@ayEbO0L)|i(TG%9Owo1~y|f)M}P=7fq&?i}0Pe169Z z(XR)m37T8y8h3RpQG}-<9=A7K5&wjssWEq`7n(=+j;JBbGcB(AT_VuqjHBX_o>_m{ zFMAhX*jm_$jPnESiZrPwJKxd!LC%ik?Bqr##_>Tl35%Yhcun zMr#HsU8n#1<1wJ{NO#D=za=6~`UD0oe|DekIDJdxxvMwL!Aj4q)4{IPw#W1A+>dkB z?vC9FiE-32oGz-lG44MHQn!UWt>WJJRWi9CuF~CO2!w~7JMQg_H>f&xoMfcPe18qa zxj(Zq#^41P3%VFUb=;M13X;Z8E)3T)M5+hp^I<`2`Ypi;mqw48YdBRoE!q1c5|Si? zWZmV{9m39o1PZz5i48_>4}XLa$HO;>mHVl+ubb(Y_ELU!r5wSJ72Sake^4aLwdq3w z@dzaFi`^d`E;$pnSx6$eCSiVJd?f(QQ{gCXgtjPm@t{L7i0esAGlN?6Ys*M3L0 z*~Frm@x&&N*#uCkC+imE#aF0Ro7I!roK-{Jk>&cYBkOt1gKx9MbV9_`Y)~dRghVsQ zodj_6`{k({=XQ@|XEflbBRb@d7fI5cK;g4LbK$ceM`04j7*yu-?j_ImrC%K39isO! zB5?0#1R_0?@*-LY@6KdSexvePzWYa?JY#@2N>WG=%4UZJ3LqbSF^Cez^@Il+wDs$p zZ}1CT0ZRsZzGal5NEtaZ9^b<=8{M>;0NIyd$fzswrfx%JJP&t%K zzlAox&Yj{*&@{~@yuJAnG|@y~9U>ehlaW1urMT-hP(#?gXN?6oaDxSRF$C_p`fQve z=lj+#-*uk)Y#<(-HsIM#1(sv`a$F5ub_2*GZOReJL!;j)ipg*>H z8=u3{k1!4I>SgLhHw%Y=YUA71-u3WUt8B!-c(sjkYxd_mFHG03nSHljW00vNC`<%* z9~NHi+4{8Y)IBAZtK^eC`4H%|l}q&resDus;31xGR{!(zQGMgi zK@`th-EDdgUwpr|f@j=Yfgr=P2))PIwOR}E+bWS*B4ddZGaTyCGtQ7`9$lG1Jjx`9 z%z6wIN`KxtFg7fMh9QaY@>7*j)j^4!V)L=bo^Wmq+ZQ(+$D|gcNkcGCS zr4=jSN{J`f{u9rWsy+JV3^TZ=wHz1%A7Sitb0T8&E0bsuJe`u5J{pDwuB6|_osKQK z9QDt+L|qD`4%{8pM4ta{z!!H` z&poGG_KF`@F8;k^=n48ka8~9#(DUALl10VH-FZ&0led`7%%>j1mGKXlXv%47c~ncf z+MeS~LL$7!tCNOjQFzCGRQ)bBDi`MWqz+Gim2l^aMSBkJp}WWgIQ-6+0N-8behOy! zG@e=I)Vm4)A-y^C^vrT1RrKAUy?NGQn^)j7t1{_ZSz<+56pQi~Uw>e;zeWGbRD`WE zh5!0H7zgdO`7q~x*h{rc%eO;$WLXWCl<504U}-3*`>f|^qsirV)koded%j_y!Ey0l zrUS)aAtRb|LuT;_s_!>UC=MI_O4@*Qt*;HF7hJpFx-uzdgH03#r9YtP9)DH3>>g#- z{YpW1|1ICd*w z?6|r?n|a*Au5IB@y+8x;T^3{02gYunAgu9oy4LYWp=0kuHYdg>)C&!nu%82k{cDkw zpp7aToBAY(WoDGt)>07s&l4kk>ocL#X$W1ja~IAfzH9Gby(27TOt-b`T%mm z@<+VjMp0C(bOx~5XCis%k4LRjcOm?XRXi-@QO=`Q-P2#8S&DXic67nPtXzVL?wn=03Mk)_ObNC{RbXs3*09CL_lJi z=y=DGeL*}?{k0wet%IAC{x9gVe9L66qJ^V=cHQ?6HVnh$+EF@PtFg#jZo!UF$KFxG zau{3(MY25)Yed1X^^gJ0$XP-WcSz>#drg^`j1RQdk?27;c-O=XsSKC|Hgbc%FU3o#d-deOngoFq8-`*Dvb5>YZX0*+Z4_zzI3~0{1j@xSk_e^`1^&+Fg6o`()UZ_qbV)o7^IJviYY`i zs;u8AD_FH##@X^Ca@qE8B-n00-Ms&s`NY=7TWR{|x?Zk6K(TnY^Pv1Q;%Jq$ha!np zmwTGcVtP|0g(@UNvi_OvRnI0HO@Msl$&8kHSLv%dAEk(6_X64nyA<{gpBCYVW|O3@ zL)+i}nYR{7J^#j~-5bs2zEX5NMiK-RPn-Lgg3%uIt5_t|HzG`Eu$L_;PMv<^X*PQo z5v^wMrKn5sB$JEs4od52tARH5Z307R;%O>CDy#R zu|=p^xr*A76hxQ55NP@?KHbj@IBfi4o3OjN&iYw^u;B@NwDH-GNZt0Uot{br-( z&IaA%zcVMtd(#q24T>EGu8&eqkf$!ExDn}_hpNZp`qnTf?+50EkaH5@+1g0mRo}Fx zh^FK8lAYt#8GoNs37n4Ccn!m`z~$}=+NFdbjfS5A)3rVH({&=WQw=3gR7<5TJoPy` zxl28!W;W9k-DCcU_TiOi*V@Zn*SY~!mj)aMU&DXp0ZzH6Xg(kI6c%RSd`C@f*34_N`T`N3&!nD*Fg0ClxoRG5y1K-ddlG%?G{j#lnqa{1USa|+lKUQm$IUYb>fs~! zVvtlstyYx-|psy6XW>tf0H zSA_-X9a#c=r4X<##tfrXxe${RNhe}3bl`3v$5=J;5EPCZ8(<%L%exxbe%Ge@QB!;uDXqi-X=+WI2 z@J8}1oIO`#VCp`NS*QYtZV<%UVtK&YO8eRjKTtK|ED)eJ``a>b z^i)6Ks8A5tc;|-jIQDBaBHi&uYW|3`F-i7=cNe&>WoQEn~5WnIzaOT2{KrywjBIml*x(D z>m_Cf(2{7t91(2DduqwPHIeon^yl(DXdF%~x$lLYSXUTd=u!}``76>g*px&1o}Ij~ zkT`N{l($mC9Jjzg7^CHVskyzeXkrIt;z@q#_HT8abNvWxgryBSqmLA73t_@wXRNaE z3`Go3@$D8x>37(d;MrC)ZNaG!ngoWflC9a*k}ZLHzE;VXm?Cw2px9u}o9XDGy+y0K zQgwR9HYUO|$C!bJywaVNKmQC)C%poRCVN$^1z_dA0&a;$lGSDuNx-he5fpBL^Aa|J z^WXc~qP@Z>`c$kCdwnvu9QT(l8p?yYN2Klyu9S?T7L-p-_LK`DGI%wN;&daF6T{?qUW5OxihWsT|uPMQi;`QyjN%KYX;Ylw8W++0hrV?KH?WInr7vA z?_r@p?KeO6DZru75TOqRCtMUF7IYZG#4!n@NH+Y*N1rb7m4eo75!W!>? z4<9XBTb&3jx8CUpKmE)MV~K=9HUvCt|CQ6{nPi4NNKW>NcN)_>q{bPI>~7Q9w;2SR zaC4WB50NqMZKqWmuQ{bfjax8o&}KxfdS1YWz5PIn>Wtm)QbbC@69dYx0GemHNrkQZ zI}BtO=jY*pl`=8_5xw1=IjqaNi?+bdJvr%u=U0GdHkM(ZuEEv;<9MO`LF7K)crNhJ z5~p4~+1#K(Z~=9XDf_U0x@D(-6m=AUbmy->U$^JGc&HiWj#$eO9Kpgz7{ERJy~R>; zCzV>};q`DwaICwDgkQBzyr%Eh`j7Feqic)V6G6UBU&Y>JU($ZPbJ@hQ6#Vy{I$x6g zU2040SL+yR!wJgb-7-0%RQ-p0=?8){v-KA zNSMgvFO%ndMcaYyd+v+d3B|${53RWk`YU!pECMLBNJ*()Tis5$efV>6sIK9`1&q_H zjEzicV(M+NAph63M0{3gp4^I>()37aaYNGFOp?7l$BFani@faEk}g%bTPvNJ1>k5I zvnY+22pcn7S~&g9m@-jbk}AoxFVqR>UT@slKe)y#DLo7p4>!{M zzD0QLR!T7Fs>LZZ5DtFd>9N$}j+YzRS5rV11lOjBe#cy`T_(syny^9_n$fJLm?~KE zJ?M0+Owb)S#e^yn1XP$U$xNN4_&I7PwM)5E5vj4 zu#5c>)JxV?MxS`k36mYLX=91PaZ#S5`PR|J{AuYlcH*qPh$SlO+c(v`*(W>TXYmP= zs*jAZySWTnr_up`GpwTa{fXZBA2ye5oyxLqn1_L1Vle>(9LX;_Wj=Kek#^-;hNfIi zX!ji1DBEkB{iVwJ-Z&kUyryU66;w&$Pl*4u>=QsLdx>uSg}<_EDWUjwO@dFzf_(lP zp;=}u0|!QT`;vvx6g~}4IP4( ziB*CcwG|XDOuaq@bAgHWX2z+_kA%5o3%%A;6%h924@(b!NI81ysfV$6zQ|!l)c$iv zsgx7bA}H+_&wRTC*XO)yiTRS+94`B70VHbEEfar#h|$%*UgS|M&}r^Sl$lBJW%T3` zvZiGP>N2G&IdYVBFL}W)vR7nb7sX+Cms%F`NT#i}rao(z;Q4an{L^e5tn0h{a>93t zRTafp(}Ap&^*8+p*JY zs_A7Bw<~GT)g@v07Bd17Vy_;1(d(#2R|rJTFOYTn8yJ3hLB zk3g8S;OFykF%OVzg`^9S?qKoMW$jl6z%(b7xx_n~>3HovajK7XlP|nGed{nypESre zFaAZ$RW!PP$Rv~|`4PoZ5wOF@kJ$0zFfO59-UhTO6981f=sddXZdP7c0IZWQP}ZyvbS$Z!HnEH63JpPC-P%ofIxw^kPed!bS9y$2agM1AEF((rz&;3}+OLBz78@AXz9fW#G)8KAG)78=Cwu$9 zp2G(-K?u;K8od-J3f6{ZZ!SOSxoCo+sLd^BO7X;SNA>h@%ga>Q?gFbMCy)j>S^}Ah zcm=OxqNw1}7_wHe7wzUH;5kwrV9MUbot`^iKBqBgVMYVcCdcSkj}3Y|XD{&!;%!!p z_p7%r8?dv{*_!+w4vY7!HT*zGDE&2zE}~6Tii0YuW_rl8aCe(>PBM?=YgE@FbnDBi zALth^V7zT_2d&}qJeJ@Nq;Dnt^dtr-{TJ>2Y++eR`Eq!H#xl?F^>B|*?72$}HBZOO z;Z?@Cb1Z(wkw7x5>&+i(!;qHGh@f-T_~ssRmfUxIW9zK%Dr{r5GZLr?Kp9Dd+@ngU zxq-;{Y2WIC{eD9Ox$C_~6=hm>nPLR#DB?%oNjo+q&rYiSU@eRhtP)7h)E8f&ON)FL zRr0bl<`u`f1daU)D>K@LQ{$G)%avwkl(vY8w|8I=d5E3(AG~Mi^! z1f$JwbB!LpHEadV#&`zJn5wcgI#x@!`>9ghWjnA`h^jYwMA4QzXulK7LESwbR>t&` zV|8ecE#nu@c%K0ZXiI+@{MxN#n-)~!Atq^Sg726#5eBufR!D`TmobV{+yJBreQwUd zLBXPOmpTiz4&O)`&-J3ne%ABy9fp<3d6;;a?PVRD4_<%YJlc;vyv2+zp}C^0+7W60 zbmHbNmF@S?nqd3kaZqCVP-(I*x$*-+>eYo2D~@(q3hUoG(z(`vlv%KAimes-vYcbn zQlYQflCwkD(#dJH(-pqiLXT*)^9uP8rrxxT(($xa`pBfMNFI+xdcIyQZSG<{Up<|n zq%~c@?F^$&`xTSpiC=t|QE$CoNWKlA(b&9Y)YM9edc$JQiLtKmq_FN^v+ELBN#PP3 z;>>BN!sq54N3VuUJv{UqYBhXnfEkJohvHI5N*gd>9olV`yjcOA(Mtt-e?d77n#2aO z=0K!8Y~Tkfo_Xb)fWnH};Wtf~+wFJFONdkkX0L{OOnCFC4$Vr zmzSdXB%;n`BHc1$R3lOGgAQ9ya5v=_kxy1`cQfwr+8gvd1T%J_7-PIXGzwCo@lEtX zo!aLUW8%8gOSj_~W#fNz2|R^`k()7==MDMppr}9iPJBX)Nt+U6bRlZJ%a1l&kh+HR zg1@Zv9ilXGi$5Xy6u&^%V}f~R$%*4H1(fk82E)kfVUeDO20$6Qn#01dbe?2m8A+;5 z|I|3|Qm6%-gQ&xA&nm1uFz}y}^i7ndaW>bmP{1H)U5JnI$uvHRn9vo7Fu7KXe?2~Q zkdjR;HaV%3A9X=2o)cG@u~$?xP(M4lldWtbg!bsBrqB}kgQ?&H>3twCNz?Mw+wr)P zV!0@*nbu(SD}SCcKpDMl!VOuPzlbx4UNWS&05NYlg4I=Y916 zqt#c?hf^Lm$)5>dDR49qsnpIYkx&IN@4XNr*B@6pNA(kQbEgXQy2L{u_SndKx`|o< zUHDY*gnMVtp1^uLzI}XjZ!DmD*1TIo#-KJn#&<6CXwGO1TM5Vpp?+Ze`1p>OV;Flc z8#bk1K9-9w*3ig#&wii2_6B}ut+S9|{ge_#y1XeVthkox4PvvbZ~_}%&W=__+LLrB z2eTcyS+fMZ@#S&ksk9D&=2-v>JBEdW0tYdJ{Ravp9`a30@oN1tP=y6Zw%M@$y2rN> zN=uKtsFAV<2Xyg{1fNy9&kz_%ws~L|h*(meI)PgUEJUlPFNXanSlqYTUpVFQ6S_8M zMHf5B!widGE%`X0>rEyO`|eoPZ70(#!57pz9d|j8TZM6y7&ZG6u;yxgf-#>g(jwIL ztW(8|2@C!f#I0`bTckhUiO#FW>k`GD?gcXONN_ILB_sA3Afl?vfM%s5;2HO%w}9q^Nk}WWG__&zTT4j^ zYQbIoGCH|q#BG|ybYmmq6Z@lclxWr&$Us@vn+To@2ZF?N%yPjZDz<`uJx9*}>yZ#fk;jO??)z zdC=ZQgRfag#jT>8fQQP0i-dvi{>8}%k4)ncjYMLeq*PTANbznz{~_^jJ+x-S53y}> z`3Un8a{(rQ=s$b!1bz%)Ee`C(5?1?VON|EZ41O_0JVmRZ?DdwqMIOH>r$9 zefGxE8*G!fZ)?{Eoqy<^jHUvU7}t%CZ8pZ7e=MY{W^(6C`hCuhvgT^ZKVG&|_lh{b`6XBv9V+waMqmSLXL$ zP-jrrvee`0O!zXg$MVcUOo?s-DTcodz!4B3n1v6&v^fyM=RV>0j$i^)Ka{H>uMT58 zW*UKvW5(IRL4FZ;t6^ya35chDgwEy4(ofLz4*AMIuDd-MJY3$HsW)?%feXQ5_lWZXz*3~ zY37xBOT#DC@# z9Gp}*+@Cb8Sj|z6LdyF?*Zz71v51F9S&$HkX2P(3VlK;UfF0gpxi^qWUE;{VQm{kS}Fw z^CzA-8b9h}7R?A5UT{W#%-Xb9Fi1RDx~nM`MIJt5K-`Lv3+pQa<$$#nB^SvB9FroA zYt|g%=?*W^cq)gK1o0D0NG1tA^5Q24`n--QqF_Iq8tSItU=z3#SPi1|EZ83=(#8Ac zvINq>8ij08DgFb(*Jx-zTR8^@w5cYfIm(G(B9RD#c{Kk12*tU^h_!fNryZc6GTF!-RPzNSzxqEHYG?nJJ+=D=5H1T?dBj?jM2>(B7)uUVh!DJwXe zGMQsB#y)ul(Kdwx;O+WV*pmdt72)NAF;WILO=E+JxZ#HEEwEOROQ~`P6#4u6GqJg* z&>lUT5!66tzZOEy6t*I<#ZEo^wNr?CIydDwt-FZOb8!A!E1;3hHa=2V506Y*f+*p{ zVLp!q=;f8VI*2AObhA`?PRlXM>C@rX9u(yX9j?Ws+^7N>&|9~A4Df6>BI536WVXvU z>h0|0l{GnHEn8ev2Z@~?Z0vb$eFXBg1QWxKJY?!?DLIq+oC)-;bEBBFT-Vx+h)?#r zC3$I?$Oj(GJgoIdzUQcouOF2atMRSWf6HS0vH^f&7NR94H`E;tKvk5yuk&nz;cZHK zoeljH#f{C;A-8td^f?lZ_#)MtORY|&d{swkZ_fN3_0Qb?X$7{SC6CMgY;@wrh0@ZG zJ01viPNyTudfrW{7^ft+qhRoPJCEp`T?_b#E23ug{pT{9L&G@y9;Z$qjd)HG)Rkm zI<>OdHVC%rudLr=|aaJrMiZL)xNWnn2p(uQac; z#gMeNw8e085wMlx4PaB|GeeC7Q<;Ti`GF|dQt z<^6t58dp()i52~ja47!{Wl(_`75z|gCewa9vLqmkr;BSh4xHy!=4~HP~Cxj}iyNUruwb#e_ujXVvY2C1sPYL4oLDyf>jT8`R=8mYRzdXD;rI;n=fMvew_ou;r? z+&|<6@gDn(dGTk(l`3U%ue5*k3&B0dncd=X#g!UmX|KG0@(caF+FAAD&&n%x%CcTX z|Lhmud%)TG;&J7b28JrDsa^;mDux=XnzEXsTD)4V+O*oG8iqQny0W^X zdc1nA`m{$~!q4jNxL43P(DU#tru|sPz?Y`hAA6-B^4xf67aQrpv>xTz~4HPGf77E|5%$u58v?;h0EPj8VF6$`02m$sMgV{j z0Z>OUG64J;07e0TQ2}5y02mzr#sEO0fUy8zYycPs0LBG?@c>|a0GI#(4GAU!fQbQM z5&)PK044)~$pK&r05twS%-#snP#j?-sTdYtK9!@N#HLqhJ2b&Q_TC7?P@GOAX&;oJ zcJ!0l^onSQX4t3R8<87|%Yb@d^X1bx`pIm1#q>V|HmotG;zlD$3$Xc&XdT~VH@)K9 zp=tI-_C^AS4&fpvFtIY&s8|EV4Mc<6-(UgHhp9hyf@%s@S$gp`43MEe^C;F)6YMr`OX z6q*A&gN=qYP})E=s{IWI@JzpVqcU_@0?mP)Q3oYt3`Aqv-v|KDta~@cLx+RV98iys zrfK#J6(JqOHY~nvr~wDK#U7hdq2e_=)Tji#P^< zkaayuE}~vPZfOXUGj3)uxDbmBW(Ee6KbXIsAQ!2#AITylZ-=!We!zs9AJGVo+8KdM zg03ANt^Z3R>O)9`$tPrl7zq(COd@t7=^%AyoC-M4M0nm%yMFjs7}tJ@YxuEHY7>$l z=+i+)Ch+ob8~rS^n2Min2aIV-ws?Nj0Y9YpHJxGa9%+c^#}$?WhP%UQ89wc#bMA8I115n z5!U-rOG80ByzEF4g~*FR89|ug!Ni4R2A_I@DaR1I2K-GBB*i)SF}7gwKS|K6eqx2A zWhXfZ_21zjLM{vX9fUv`{IwoQ&4i90fqTF?5wF2f<$&;>Jd{UK3Bk91u==65Ue8Celo}U zG-M)XjTkD<{{zwa5rwLA=&$I|h_IOsgV`FtnT>+kxrv#BfEg*0S)R2S3zu0Pc*sm| z=zIQ1St$J#3O0U?4w^0ER*yuA3NMgb)MXJ;Lk+p3gr>KTr}qyx@}LRFPq_@E94;61qXaIg zDdqwK1SuuSWR?d%B;tQ>UTGbCfs|mp_oR1;y?(g7bv;J{kG=5P3ET0-UE1+~#bcl& zlau2lu8R$Gt~J(snqH~jPyK~^3xx#^whl`XqOp*vA8>Cs%r>Z|Uhpijd0AXTOXE|d zM96hbHcvE9UJ{y$tUGR=fT2a~5TBLqFu>@ysJ9CBio6P3N?e9zMpX@xt})J}ABn}% z!H49U^8(!52*X~|@sT$<2O?{pHz(gDf{Q3j9XVJVY5}!27y@w7uhV?-?2m2OVZS<* z+oG=utW#|BEkrkj+dPXPl2gNa_)Mfryw(@mqGNUYd8(^*%}Lwmi3-J@vtyt4N0lCs zAM#Fc1v%$`zSKVzF=G6SZ^}DbCDcpL`pO%7aqeW9#yv8AiaLbBa@Oh+d|Q zu1EXq&(eCK&X%uloo$Qx_~?`6I9=6xglgmVDtKRVbmVWPs78d!ZQSEG7e&u8S9wo) zR|9shq~RCQ-{IKwkn|5b38DPPY%4Z$;MNObxa^4kOJ-*F0}t2vNuZbAn-uZt{TU}&*|^V6Jup;VMlB~G#lM?Psf++mbLzD*V{}iI|zAP zZfW#f^$!fn*~TwZ)TPujDD&oux?cM24=X6?)EadGZkO}A9w~(V7B_DW)^0QEjQac^ zA^YVs#=QX_;Od&5lG`gCZ^UH`)=Q&+l2OG>A{+vWZ^qJeli$C8Q<|Ba{)bCAna9^> zy<5CDtdP#aX4qkSpWSjk&b;EZD*s8-VWZdlxK_ls`Vl1Nd_B8M8@NO!BI~@kqVPkp zFjvt3=qrrtE={m&)=r1=Ct$A<6_6p5g0HPv*!$(;88ze_9p(WtII2g<=Bl4slIJe^#C5FHa&hUF;EtTp7 zWTX?YaMU+8LfnkYluTD0!{b6wQvE>B*`x0lIKUGem<}#KVe|_rK)wz}5vMDp zQ6=sSOZ2E~d4J3X{`dCQbFfTF`feMqn4y?q&(On`MlELX4|BLSpRUic)3}op?UDUy ze>*=pwVljx`1H!NDK-_NGM-4^Wg%#3AtYoWbZ`(b(e|Njt&w>A{ze#-x@)Tej|cB9 zEn7y4^n}DJWjEKQj-n*Xw6V0b)Po^{q|GEX>*4IjvRNPT(RGShv}Vquz%yp}wM8VJRBGV^$pV_&SV!(4!1m2b*w7-J~-A4XT3wjVVk z%<6nNH6^9kv3e`oCZ;T(I~la7%q0@<_@nv|$Qj(OUIG(EQ%8cx=U3DzN7_P1uSRRA z`gU?F=He{4WYoD{L|&CCiG7=}X@4C=Bg&^gn0_xM6}nq;fjmRL<;LDTQ>vrlT-7N& ze&_l|?^<@QTb|o(z}9v-BiQ@vr%GVje9M(D?xkqbRLz(lPGg%vcE9{NwzLde&uw9A@)(yWiEnk$!vh$7a#F>ZdaVUKG%+>I{$OYcrS= zC7e7)+|}GwHix}2E6CPaCmZ`C(9kDTT1|6gZ|n5smV#UQ8vZ_s2KuDHUtHv};^rhF z(v+?Q-l~(!KQ!IS*_uUY8Xkc%2sw0qi&+q~Iid>IS-&+t=NwI=hw(PXvX=QTSyN&ww#FHkX1r~b*t2OeKeJ(XBw#ZU0gi!L z9|0?wwL-Eh@+If(#KHOQC-@)m^7+QZvd+`=?oFTgQ;~vnA3aIiuVU{`IU%NR%TKj{ zP_XiT*htdIVC?gVXB;oF3`rJ=2)BXbiRsf^;iO6CfZ@A^e{X=LQ}veIUHIwJdFR>k z3EJXXX-!$)_Ib;)bq!p$ZFUsN@lxEP#?o|^ZMAImg$9Jq4VM6A`u5pR?6=vLt_-nE zMIvLM{AJCbO8dx3lp|OePR;DqmY!QcgvaQ~HtGiHdS?LoYZ|2KXf)wUdE2!w?`iy1 z@Im45xUFVTk1){ZW$tu88nfmKZky7l{MGStW0_B5+aM71_;SZQqw~`^-pBh@<}QYE zCPTE%FW_M<_9w~AYI##Zwut|m#b8RM%GZK7Yk`f%BDyBJc7)a+t=4VV`WAZP6T4q_ zKo$;*3i47&{A~{2j(-=yTQt7aqJmYz*U$grT-{%*mswaQzvYYa@XwA;tvLpDL>1Mh zXRCfQLadl(X05HLfE?pS(f(DRKgMTcWM!`YQoY*jztA00Os9~Znx3Anl^EA6w%yy) z*?A;A6@|-?t<2pr#(2Us?paPF844LE7t^Y_)68UD{WwktxFl}`xlly+Eox2*DM1{K zbvGpCzVNs%I63VL=~Z)~C79y%zM?-rPXQMBjLPjZ1LT}g3D=A{`;80l6nw?Iihrv+ z>Hg-ev2k!|?EB*rIzyk&DQFlE*QIIsyJzvG?L6k|L7zUt1czbmB?|fccCYfJ$`oU` zSTOr{iTFoqs~sI(VoP1(-=6#aq5+gdkLl&6j@58_JtIB1V}U9Qkfqv`2PN$Xo%Y~O zOs&9`8TAf>EDYUSaA%!GYE5dKuRA|_>i1Vfza@KDOvtm0g5%`QPd$^;3x?+cILSbc zymi{xjltKzls2N;<$<6F#nA|3B#Vh23R<>AtyGxrZ#%)9)VPE632|@wkYAH-rt07I zWK$=Gbgh>!`rkIYHwAQ`1;fl5_>FT#0v+d??_%fQ3O6TnV}VQH&%!Mqv-rrbo$)!k z?U1p<<0^l$_wvJiHBn_`yH+8(^KwP$II+D+fyj>jNu>VK%b6gED^e?9Ben{@H`(u=(>K9r|0sjN8jX zdU@Vqw614%(e)Zd!LapxD0HjL*+SaqG(OzdhhEtzB_-WK2)H@1xWzzI<2G|TsuvEe zhc%ymC44KtvARqv?0|48wKB9N)?z)Fbj#9%)=fV>X4uoUYvhzd?YKX`tZnR5>~uyU z>Z&~4d#=*#^D0d@#VqCw39f9hcKhKnJWxg`7%oNY64OS}`|x*c(?LS@63{) zNGY?$?fl>9ODm+|mXA!-|F~tBs;^>`2WeXL&BTn31V3G?;c6UrkQ(%nrA``&PHNph$y@u_2bfdF(&aMkJj zEAhdl_VpjW#p4bPkN1*oKJ#{(NB|gn>Tce!$&C7gFdH>!U`a{qgerezL z5LiZs1N7ZB#@|e*ZjZU^JvzxZjQFH!9p9%)rC&i*@1Km~ob#N?x8>_&7cZ$+@^4_h zemog?C4YqW#qM+t3(3`@?Fyi_e)fCD>&u%*aRHGHv?*sY+#TA13vP~Xzg*fH5e+=5 zdHvdX*XLy#Mc(x`{hiGD?4Et+pGs9QBlO45a7;&UvS93LE+I=DTIc#X`o>(Mx-hg& zq9J=>`irHjC2D@=)z?BCkwcKSPo0~%Wy%fWur-88!nIAC>GO0>$Ze@H%!DX*8tgFJQ|^%8sc_G4>l?%)0Oy0GVV*Rg-dV_IMcz5i`oCYodl zf#=Q*LN_CUNUG};|mDVN79R_k+*JW zO;mpT9!b#plf zpWO(wgT2qqfsFOzyH7^MXJX|EIvu7nLPX_^Ym*{k-DzQTJ0;(_LyPOV{qTrF`8rFPqM9 za+7K~m>+!XHhRAR!qKGM#4vkMhyp;ps!NO5aryjQ{##Y#ErgWaItv%(M|kYsRxf{V zM4dO}OMP<0%t1w0?)I+eGBl_5o3F44$a3JGEr6&q+0v<$=Lc(rR9fzN8y z$=OA}Q#_sRM~ZJmlFLRZ7rCeG^W>Cr^NoJAmOT7@f0T3|EZQiS zvtzhHkCvazY_j-Rd!_W#!h!#2^QT+Gg_jLEl(aOZivrYygkRW?4teaU5x!o__49O82*Y#Q4C!z)P`?71QO0iAyT&{t=%KGj|* zju2T=$FUETijkOZG3WgUuuXlzY8ie@dHX)k?BJQRvquTCKhCK%z`NUVC3xFu!|AS* z&qMl)^w%LI;qQBhR}JUX<9ENqrqT%EqSje$@0=i607sOYY{-8Qk*OlRXp)je=Cl!H zBWwYhUuL3l6SA;AfgIb)Ckr)Efh~>6uRx9LqSl~03q^~&)Aah}ok#*zMf4EiqNbgo zahS9oNptnla#Fx=D#PotE+;*)>^jN2HgWlubmf*dw)N`p#NHL}h&%DeK_+R$WyH9fm+IPYqp4-ocepwYbROhjx_ zG|AK%_TT?3ZGUrwjFw_Xr(S9z#XL(bRz-#TX|>)`-Y_S) z^3Q*k=BAh$r+E~m!Bu8jXHntf6Je?NcBx!i$?^|81qViYZxU||86*khz`i0sU`T9g z>Cmr=vFlGNNJ9Fg=b|GoB!0S7@9&p~QzHvJLak1Q8o4<{gq%L9E66`dHGoF22;Ks^ z^?^%ea1m9HTI8Faoktf@Bq}GnXDPhY|M~A`|ISI*e&d>CpX0FT=H<%M!|yA4kazeN zn}b9=1G8{!`wyqPwUx&4EWaJZ!NCy*luF4aK~^LpH`d1y0-a6K(Jh_3n04H*5^dzW zQqCX<5ED1h(ukOIEi9OK)5Z|gifK31mY$qV8`IKFH155k(XYX(W^_Pm|10;HyDJ_; z0PxJoJ>tpudi!{LWlp3%ks`sVjGlzD>Wy%sZXj+#RFHM{p` zc%ID?h;G7m#bP+09d!C(8)9szwlN~Z5p{TqTW+aH%O%6bg*nr!QEgT{6%_j~e$Ss{ zZr{?Pc%O@Fa{440ruQ}{aRO6s68ZsNC-8WHw5=R>!ul5AZ{Kb9n6|tx!2}FbiJ&L_ zw;;W{Hejuk^>Kid=5h$M8E7I}n!Ws61LzdJvMI7p{oJWEZJ*xPV$Qw067IWrQZB zE9-c(Ih>e$T-#=K=N}{ZOkgW}K1}p-53a8unSZz)eROiFGqx5EwA%hb$S_(2*J8sJ z-H&eY70y%HVMc)rmGe~2$jE6YG-Kzy1UFEvQcr^N#wmrl9>M=X@e_l5SlJDcud|)X z0KDC-iE;-hK@*db0zm=?N{+ucD<#oac%$1+kvSS z&iQuq6e#Mxxds7EO=UNFXGJC9`WYbUKg=y?Y7x9um!0dd84E*K38#Mp0x;h1S~Qb%2MhUA9*B+Nu-xExymD}bZS84kC>Z2V$9_Q%r)5dsoiLWbJmxjebGlC$t zGeY<^QX8feN8j4!o-QqYT`Zw#^T@PsS4L;!ezQvtmo8eJAL9Nzy=E%pHa|DYBicF5 zLtMqkmmKFx^ZEk)tb4t@92^hi^snHpj)KDx_sz62wS^MxAVJUvAml zWYI`O?Op?GWA9OsRla-yY62G*7irR3<@Img3}mIhRz8;~yN|*w;mM?6hH^3j8S^vv=Z`71^OGfF~sE+H=y z1Kn}oBWk|`^L1?@;giRt+@}B_YrQ&rU;YKgVhU276*cO**w!k4Yh9JFb)Y-o zr^Gdu2*NBF>3Oq{0^3ExOe zbA>{4Pe*qb)tGXGVst5p@`~J;XJJAzl;-T|P`inH)`qo<1T$PdviAdrdiZka(`FtEF`9nhJA5e7a7)(qYX z4m_LgENyNL^_Ki91U(qoAw)aT3_m{a2{Ygmo3+RZmE9dC5p6!a92j_#qd;|XN%f2E zH7n@@0v$v}XJ(|SHfl;U2zeDfmqD_N&$mF(VAwvAHIvIzwu)1($X~@4Bx8oTMUYu^lpCi0h%<{re=exflTnMdUdBt%j##a_V zTCV;vNvCJ|xz37tt!v|SR{&by@!96fGS7I`f_o&5N~y;;!^JxTV?1nY*3JsfAMmk; zGKL6S#Z4Id(qoJU+&%vpahsWxS-;$L6q9Yp6sH$VNDs4(OA9!h%OHW;q7=0+@e$4_ zu-Et&qhPas33&9J^T(0!5#Mw-z=-lgSp zKe;X;BvV@}2T?*y*N^JTDPe1B&( z#AFOkW9MmD?K)=7ZS_i$hI`^$m@j!#SKmtS@sz=;ho@-nb~6@YK67yV&y2qXalfyx z@3m&TEzvdK)iHkF@QR1_<`gaVpcut%!C?N+Wb9u>=G9LK<>#M?M$xodM6D z9W83PqtH8>#zG4g`;%jZPNR^E-(E21o5);cHugzgFy9%@YvJQmuXWhD{GG-9m%E2N zvGEWX>$PNY3x6jxPHy4|^|Cm*@eKy(%XFw}L;yPpw-wI!EYc8?M-^G7N6ESCT6R2p zvUu4}Wf}5)+S%CCBhyLG7q7H;*1QZr|TTwt1x#BKc=_n zm{i2n?ObEl^9gXvulZg}w9|=7c2CWWo53*Ff@$c)wuXp!6RU)aZTMu5kAq}!t;vYZ zlH+}Z%fC+cV}t4)v5i##ZEP-r><$z6lq|S+){RzO9>wly4v+o=($&?VT z;=dnaqe}sDJ0!=p70nK`_8+RSISE`5bsbdocHAqWid}eGqAn950pXKb4y_yqjX7vvjRd_A)pYOG%Y<5^@ z1w}k408|a|7>4B=)D)qcbrKoYVd^rjPbDURNGuC>cUvDD{_FIhoCmp$5`T5!X0dg` zVQKIzKEVMSw3rZ<5gwQ>OtzS45VX~#gLneAl_`k#E)1xieiE4CMO4w$Fy4GLyE zT2CauIp9%sRtq-&+}qxE^});iWwm3VG0=S+7@U4*JEk?OQRWVoSr54h6+7SZnKumO zf2`H`+$kh83_`{d*iN5O2|83HOUTLE_Woy9~wdXq{%L)OC4b?E7hC~=b%NJt_ zX&D&=nVQlcoeK7-?Wp6mJB`epe~Lp;BlW0y#M$t^I0SDMp1yCDJ#s7MeE8Daq2Pz{wD7H8yv?mN*hFkK_+5#fagFIxv@ESbO5vyb zu&o|%zWI-za2v8whsrJNRMj4E=zfii+auot#o<`SIC(JIyt+X}tP!0_m0V86E!=S9?-NMkDxUsCw8jN`p0gGZ>l z;J&}Rz;BrJqk-)LDc05KGv{v!NBS1^Iva@R<}v&@=<*}t_gv9jk#TN3hQGsr9jc`0 zz*U3hMbL-g8Q($oqLbHg2CyFOBkZ?5oFw{)@x;6St{v+2cA`p^u^e*WXx*_xK^wm3 zhuy04L*1^z1@~J*-0pUQ?Hgv_;QZSb{}0uDctpp5x`qK5sFWAd@;9beNKsuL%<|#U zZ2`+w8>FGmPRmtt3m@W7B6)=Z)!&dDiN;b7dhc`7ne##afTwzC4U8Kki3m&+qXhd6 z^^Jqq+TOPxRPyTv#9|_xr|q<^LO^f+B-0{De|-L&815=~0&K!vO#ZmscNo0k`p9pD zU^RH%kgrCR@1hlv8xpggJGQY&TLVG&O2n+jA^ru(a%1~9QP<=b!q^q7fiNS1Rj)G~ zNXMIVEtkBKAXLC5TKx-K&kFFNJywA$l}opz6Nx zsROf^*MDF$t6v$?Fj49^8!v6ix$5iF}&XeSSdR}an;6Q^vba?;qnEvJT zcbiOsXpiBLal2G$o4?#004!8&tg(g92^*na`=zoXW?Os$1EK6t?GyR8q)d{hN7VR&N7P6H2*>@SQz2ESLE)}%+OXK@Kf?E6E!J_Aq?mM;#F$=PRzjZI^q5RS zD(yf+*7du512+yO))1u-`6uX7;BA;e>T8<8-Ql+m`D?Kb>yvJSyGxmllN#ZUu_jg4 zbv+tF5q%~?0P{gH-{qK$&63O9QI|_n&Ue3rJ}8wtrR?rm5XPfoTvjS%Z!9IU$DoeJuKolR+PKea+m4 zGHFHKF)EE?3T-z|1NLX`k3Hl~#&lS2s;JC72e{5m5B6LyZZp{9!nfOzm#eAb@nhvsV$iPif`%p&I z)8wPA)i6@jV&^*ZSY{Iu7>m~1N7LWfYv-| z?OJ^lJW&ZVRI>gm+u6Sym3S)QpL7tUSF+m{hZh$Izq)bl8{Su$Pq}&KH&NEnu{yBq zFW~u7A!i)6e6-gFV+y1#c@rf~_FdUR@)p2>ANI_@+~T^dXag+XC1~seOJ0_(X40ej z0>q#e;Q5_7|Qz5KdED3255nD$95fb>CfCaCm{pk=NPayKHEorgjp}dgEcGcOn z6Di63slio8*Ltf@!ZY`a3$R(_wvrRG4f0Yb-=B;XFxZ5kHzu&JaCe$@DRYud5|!f@Sh)JAn6#D~jL_p{(I8{ZzV~70t7Sh?` z=-0Tt7J6K|GMmc2Iz>G;@K0csEpewZV&rUe6f2|2S+lMT3El(u{XuYg_Q20=C1L%E zcjXZSUe@{oE-ZnIH2~Majhp;nLrhce*rUtGSz^i$9sA(d(Gy!EajqP~o!s1@EqW2F z{eqy1mosh4VC@3)izEE!leq=Lr-@M&!oUsQYT8*A38$;e3T;nr^tElUQ|0-xHYwthFts?~F3Uzw!O6@Lvpk>=deI+Mem_u`qf4qus~ehmpL!xi5B6(=p~* zs+Wz(kHNAMV$%eMtm!Ir$s>=D}>WT)oGXG=%+cKiGe_KSUe zv%PW)LV^Z1I^~@*8|&v6R+#ZJ>+4t#lTAImD({Amt>n|P6 zzL=BO4vXvB%S)B^AO#)}U6L|+TlpgpTk*CJDAL?tc6M4MxDxRANx5zrK+;fnYZuql zCRIYJ;pt!zss8A7;->a1&gr9Y-9`{IPI>hvxA|5xZ{;-Z*<1&7_H52u9g=(QV2nK8 zp@7ca@(ewVb|><9Vc_t^*mq`*YBWny5;U4uOK$5r>l@K2i++mecX96fMhax;Tx{Mp zS$OhkiEyQDYajXP_jcsvFz5Aj{pWLnoNSbNek!T` z6(x@HfuwHoh}&S_oiAf5OGxY_%R(-KOJGvKOZ~ zd6L)ugHq#pd;K#I5K zhZXIG{Yjf8B9awP+gWwuIr2*Q-s-Pu4@&n7nsmPBtp<$*9;HS2JAM}8vrdYGbfF*o zo~^hHOnb`Aa_sT2EwPWy+MfF5MN|Xu+cg$PL(Zl8GmT|1eLHgSv=0euUzj8O%{NZH zKlbn9rrc)WzzSJ*e=2#waw$&lT~}P55RM-CZ%VBP4pG^4=AQ2^JEUZP21V^pkKF2y-=T8 zlzo0eI0Lo>>=|w253md>=1j;u?)v!8MEch|(vx3GWBp~`z2lMFLK~xNL+-gWI_I}vHO9TanqYE>?AtW7Ij-_ z=SE_XF~e+F35VE_CI-RzVc|2dz4<&C!Ukw?sA#I|SX)!G;ft~Lj7EW~ z#F>}1sBGS;qNBAnpTJYUTRXcg@<`U%YHO@h$YtB!H4s~dcaGa3RF@k%I&TlB{JC(S zz$hbDYkoWD;j)Ks-Mk6WZ+HMn#Q|8xj*@tV612Che=A(Mjdz_fU#;7e7EUdcrFb-+ zTi1i8>^{5cd)0^)tuI-nT_(4BZ|!^~s%)PsclXnOG>K!)B)ba58j$gwXlWU(CFbG5 zT)S61Su~2*BW2-mDxMo`m8f1A`6}mb*hA}(WL_}E)OB>`bC2|XVs4(7@kDpIf>d%m z5m2Awecb5UR&M^j#L@e4<}n}MtP!S+hC^%ZHI_c7nCK%k+VDMNPK}NHTfa&BlJ68z zk)~+}ox~;erNn6aAc!xsGarC=xePi|TdF;rjxN$JWb<=+Sm|_cMO-|P( z@0Q^@QQhy&{$g+G@{$yb0h$A0$?e^E5b2$NDeyANYNZ;z!*s^?jDI@2cjW3{2)KJ> z#JVvOa(@zMx2X+Tz;<`z-&NCC&>SziDf&W!}`o6o1;e{|-N zZ-#fClm?<^t=i=ldi#ve)oqD|$NL6^aLMH&>AaLb}aH z(@87g%*my;S!WGMt4M3bTt{h(8@(QnSwa4>kAMC0&B?KOI8&#yjwN_HQsM|ho_J9O zS31swddHpk4b9E-qjad_OE+&{I6XjRvdhJ-J|-lAbV}i zk%agbpfxGw!{t(}5HFJ8ua{WuR}>lpg4hiVyvfYngx(8x#!^K6&_41f{(8QnISs)} z)tNsyDjOA2zjDso^Yj?pZf4FqSjmpW&Md{0v8|iu1rCfNajz&&AA+ka!}mqrirGrUtY3VEHiJza=&Si4tr|yX?Gl zI#_wAd(=EmqQ(y6*w*DVV^Ki@@w5-|hZsrw2+7EEs8E>_#r|ty&`_kdv$b`FjhemO z7_Ex;LNEH;G|WfhblBjaKd$-1TS0ae@Za)K@<4GAS(`wxt3E+<^H7x=owuXAv7y4# zi>*7thoX-9^GSi#Tg_$DcpqBHEmvJ$k6O))`|_*S9i}ldX)j@=<}(Joi>A5gRLyx< z^7xMg@HDfsRuB0{gD2}Pf$TV8-F?Q&v~2;MlkdBKpOW<;m8Zv$I8BR51=|aD%{ee^ znmR$=P!Qx`XYcop(1WpJc=8}{#!;nY<=J9$Hy_RJZXWb90tCy9)^2yEt&~^x=&U%2 z8OeYYC_Cp)f5=acfl*v398a&hd_BJu?!lfq4iek6FCPqouoUfB7?-VW@~b=B{^sn9 zX;)EhJAPQdPyG*lK!U&5W_Ju01T5vXepx*1a^~LuMb-8+RJ9EHIPwSULr6QS(p8X0 z#)r~OU|8Ob^}~TaL(M&*xmxWKdrim%zISNz2R+(LPbpnph6067F^}8y&HsGl?PIg& zOrPc*Tsg$6eWaXhN6R!L1GD>^DWvK58}VgqtAjPO%jSmg3D=4Jb zyUCTWbf)W?9a;jWkCpZ(&$!tiWR&0Gq3d>A=DJD!<>T6( zlA4jF^%^f{O4#8vG2o4OJ*{+S+oYh}5IT-sq+np?=y1isXtGZ^ zcs`)I+^*PRpt)ALF!%Fy-~3!Pk{ZD1=Buk(Y9phR!!CXU(=@Z=IWbjOw__PzC1$31@QN&qq7Q zKiT?d|C64JOKIC{U9IDeb&lir-1*(rYSK;MRsUE-y})a!(?~~$dxg6@ACNO(7Le`l_g#gy&mKZ_YuvXqHpCBb~A&n z>H zGdPv%YHdf8`f>+%%zp4UcwRm{xcI2WO`;|CmcuMD8#ZYH0pch|IP6o zl=X(OyyY(&@I}CV52=Iq^25sr1^Jh7?195Imqr^d)97+OjlKY^h2s%;Zx2UT9*vp+ z#UN7w?`wEJ2Jd69(C9Dl{w(14z>$M&S*NB`2Xg3S4B^icSmKC`>7t}TB{e!{fy=Cn`kt@$$_Sv}a;ugfM=>KWaCy&QG_~k(Nx7nMssLdXs6lh{ZB2)#=Dh^NxC9I2pbqdL=C45L1YkB`HqIx53LL=KfvrmbBfzi1_ zwJ=`*b$~jY{yH4sC}r;=v<}cmj?iAfh%|)G1IB#}^8lysgZaZCRQnHT4`9US2yFpO z`8PtPfI2|zY$Lt|e~a&{14aO*0LlQ9k0Z1lFcap@aeRIkp$b5s2I#*NU>ibn0Z(=# zv_oT0w?1d0drSwx_1fLoUk=ony$m_Q{seI0>xfDtbc zD8dEGd5J*l0CjH=C=>9=TLd}-81W8)ny?%O!xa{aBXR;s08>&4v=gx8BLW=<5$(I$^n!C*8UmV1sI`#{D3C`v#}ho6p)_*`2piHAwQrj zi$HvLC~qHu0syz}C(tUuF2GIr`~b8E@Fd_7z=*#=KLO*ip`U<07eFr_z`sbKxfm}& ze!yBKfl>gEf-ZZMM9F~J zkC7-7P`8qVvOynD0uP_B1)eXo=ULzZW+szpJ7D&D5*-JWZ6;9#VB9MtqWoaoUnkLA zK-n84S`C<-Mxs4{C4VQ;8Ni5dpj^PMfEvKe??@Ep5AvT$v<|TL7vKYyTmU|nUnEf> zV0JNybbz`N5=8_6UW5Jt#@!&%DZs5aAswHWL3;xs9WVkgxq?IzjJKfOfLm`vyKy>T zHa@R}_5()Lk%&JH@;8tu4p0WT3NRCJ+cZ2rkPonn;-CsZ9hZZs>0nQG95ffO)`5dI z0UmMWpgn*-P8@Uw%bhu>39t)LgVBY9f`XvjnH;nlaBDR10ZU?m4;Z(IgH(VzKz=ar z;(!lW3n&AOTg*Y*0d;_xfLlczbR4h*FdL`GL;Em)DYP#HFag>Zg8d^0Z2;7*fP8=@ zj{zT_KgB^^fXS2fZqw9e^o-djNet=Ae_|*iUj$K0e>aL4$xN z_X8i|5Zx!xU%;)OLONgxU@aj3FbBELfbsz60%o6u_TuxOIOqUiS2olSn0W#833&1v z@FSr7n;ay;SOMh%o~(rS0?O`z-T=D*`7@z?b-)A690nes%$q{T0Z;l-r~ zo@W%A3wRQ6btL|FFbeGfl+B{h8Ni6y6l%ih^C;vx3);PaLScY9z&OB@F%((_D2t`g zCcu(73LV5eDTT7}c_QQo?2=I^0)pp?%Si z{yek~FnI%o)&c6a0UyxkE$9#6k?p|8ydA)w4fX8=KApxLkgt;^5qmN0ZchS zAst}$IcU!u;Qb2i0IUVv4w!k7LgxXKl@#g%JedpSLJY~zr;ra|$z^CipsWDO$MIzm zv<%Q*$(1-z!G~Jg)M+_a-h*xz)XflXBOaj3wr^;I3F7M zz(yx!8jZFCo&?+r7&o0pM*#V-hZ+Qo0CbIod@(eV0G?a~e880DG&&C01z3X5r8LrD zOr%i|#MIeIG+GTP+W>nLz)V0KYaanTiSY&4p8)bVLirFG#R1L*EZGcu7{FSmI1eSk8qijI_cc34@Y(ND2fw&)_eSo#6K%W>-Lpgw5=b%4;y7M&3 z$M`diDgl$TVXr2_^Ap+!xb-URqX1I?w*r;`;wMRbN}zt64tN4E8!#Koufg~M@=KxK zc&Ps-=pC@+Hs~WB+Y8NpqvJ_H{LPTrRlo=A0;I%{|1OQBfc$!>2cH8T1Wf6Nb^_J{ zcH#6v8VQ%e_yWcNc8$Qk46s%Ud_cYq`1l-f5HQ6a_65rT`CKH$=)gr=0Z#&E;&XYbb!-Eixy)MCCk63KDR)GxR zGkaaaXR+x!`>+gOqYy4Oes`o%whj*0#fIx0_=W4)iqk;mZnoFJ+&#wp>&;|`;n;Lv z-6!GL0J3FfG9?^GK=!Ip1}2Cq;Lw2V5N@Q{QLxlexYRLVsbknu$0)I5Oqxw5H;dj! z?dSZN{~De$*$2fmG!wjam*1mJU)P3FY)@IaT6 z4*;#z(`TxOy*9@&d{1B|-wJ=!eu_V7t6TB)qYT6KJp9@OM=Hp&K_;aspRHKe%|#f#kYod5V_KRGYcW)^|7wq?=??f zVXB{0vD1pJyAZfRcM@IJ^&-@aaG!4HFFYLg#wtGA2=>_4=98Xd!QTt~<6#7P%$on>krqDZJn-{p5NNeIKQYa~6Y6sq9y_OwpRGgwwC@6^B*VBf34~MGnySJ>^HUnH)S<}oOo8s9 zZf!mLh_vvL$-rOrDq;9O)+yg6b_`21x-;^s`Le|7@&KR?@+>#6W6D=>7Lo>&XZ=w& zybE}pKy%Fe5ms9T$fY3nBTTY3kga}$Kp|#49muwUZ1RIlcwH30m}i0v#~SEuc3s?w z*G0UxV0#GIMxb-Xv<2;EL}40dA4y)rMHua3FK{-! zNgyd{MOU0S(p*_={~=H6+XQ;^{{Dl!4dkI_vNn)W+X*z?OeZ?rpB)66W+oHZL9G7{ zF?o5Kl?8z;=3UUUQHE_H24tId5~$AHH<^6;|BQ?5vv|008C!tw`W}JaF>6x_$WlQz z`QZ<&s{(r}LF~k|*@ZKlThYz=rON7}BV&^~b*QF3h5N>784TnwT zVDHD-L;t=qj+e1rVIZTvCQu1+f4eyOxISzL+~cZ^1j4Dx#nw~Y1z*0s@KS63$cl?^;K@UpD=*mo>_ei!gp zeGl!kHh-JQsfJ+t@`2bU;50E6m*W@i%UqCe0r_+0e#5lREuZun*seDK_sCBKI%w6d z_gdIBZYREP!25+jKU(ukpIiE+eBd8e5~$r;Oj%|bQw{=uT_J&1T3gHJS(YE%nTwFC(V*9}l!A%@p^i0=KIie73bQ*+x#;{Yl`ix=o;Y*0_F_Yh*3( zH&hacXQdtFuwl3{m*)uUSQUYU*2?CZ+A$ZnF*Sr`Oup*=SlQ$&HUVE)N1!VLEBuoJ zi*_Fb{`p=4B{^8($2d&2Md^sI4G?ICwYAsPGDgGqifReca(yx9?;z3!@ExmwUqX}kyWOYe(I${-U=!GE_Ekx^>=clf+LEZr*-Af)oh`~f3H$>-B${h1 z8^?`GkY)Rl$lK0J8D-?;e&;9ZKEJ4gyvIUGv>uoDxTR0xB;bA-)-e3SdwhJqtTc>7 z&s)sphwnQdB_G^(PS%U>v$Z`!qF1c>tw7w;$EU^s-;WEM1^u>WZ!1h=fc+=FZx=I% zL;>bwoRG%hIFS}(tHW^$Or{EJ@x9r+X8Z31@y|jMJ@p6n-J*x=yC>~oyA!@XL84Hz zo5k$@2jl_~i6YHyZ8E0yHec0@{vY$>N%UuHvE4DhTOnNy>0DPU^>?{g)PDr{rJG3fg0<@q zQg;hJzOO3y0PNYCpIIibm{0irYkUU&1tybD1uoD|kO|FoWVYwx{KL|Q;(?#Mk3@g7 zw^FCSy@hYv=3=;4_rt@+_=u(NIRe~OpOdJ^ny>mBY0;k&;0wMW(Q0FV@cOI5?E~3f zYu72>ueS zkrp4h$8rhC3qij2zWJOCa>4&dG=?6oN3zMJdUpN;Uj=-t>vf-r&&L12SDqo!f&1E1 z0rHr$BznWFJ$RhDKz`sHiB6lxJ?6IkEHSxO+%Ug376kK8c!5NuwS8r$r5&yYzUC5% zCVzAV`-Uwb8w6Rrxh|Hbao&TR?EVMRU6mv{%vhO6r4QO^HsndYL86P+*3aY4mi`a; zf(8-^NGtqd%lYTy0sg9yME|kY?{_Wzr40DnT1gaUjqhaXFY$8^+d4=?oG)hmkj6yMR@~%CI>xt zpT2xRz6#|3_|1Ks+588-GLnNLt?ffSO#9Gyprl~_JPz9T2ktSw|B!o3?ENqNY|;YG z)V(+CODk}HKqj&_&%7q@fjmBSU6Kb5$#o$I9kZ54%CYdjSZ{HVCv`E0y^n#+>R%1= zN|3*1(J%8o|Njx@{b2n+t{)nlYBkSx;(_a5kY__Y2R(7$xK)7s1jq?DE93uyEA(Z> zR9sK_Lj7wv=u6w*ABX+650C3v`-gJW&vB5qHG8o&*mMB4f8aJf&q348eFFH7WTQjC ze&Yx(4>*U-Ii_oDdodO3As={KHgJ&BeR`+``8JT(SR2!P)0pBfAmjT%+ct5~?)&Xg z>=-p!E2!X2;vxOV`DKuQ+lw5OaR2_$GXFgCA^GuRp_!0B^(7A4LOpaG#E$V6L-8c_ zuv=bcey|qGJN7CEtufOJj)Mk4-Ujl;xS!;@KNPUy^*X{I#$zjoEyH}wUNc^QmqEJV zH4b{i*NWY(@wM=mDZpR#F$bktTd%g8)~gf1J(B^x$l6{tou0C-3gCu)!a<*zx6`oR z?8hWL-!#BE4jd0_d!_dt+;8IN0uTHP=DW51;|k05I~n+;$2n-i+IrJ!x!z;~{}=>Y z3TylPS1ilV#+^M0{j_%NC)ITAhmC)MzwI;!9l{+-eAuw>eqf)1^92QhKZAVxaK055 z`7#ZQ(9X$xcpWOi3lZCY$QSh^2a(q9JGEKvUo(L(DB_@Bf~@d;eJuMA{A1l5w87eV zEVtBG7w}8fus&JiyWY3|p9cH^4q9t%FDx}(=VSN(_&fPWIA|jlJcZ-gU6b2?L;MHn zt04UaYvc8p<+Zvaz(1hlphv7-f4E?|{+3`qqENN9c;)DQ*MEQ?&!Nx<=KV9fj%~i; zu`w(->jPX0Z7{nxf`5gv)`miD$Rt}0vhzF&ePbru02Up?6mt3l z*R#g0T+d_ozfkFUAIk7-ftmkJ26=!l#XgUN%=*0@b`orKrV=&4Dm|BL+X9PvRg_fK33&#Nm zK`wj@#tF+4f7>plzj0k?vL8)QUW|-lKezGlb<4@}{(mUMXX`06Kg0^ZXSxMn0{p#c zl;yf*uGhCHi+*nhe%oItlwoF{@o;2|f@Bylp>){%2kX=f4&O;Qg7|KAic(%AjUagYf?HjDfFcF{^d!mT{xse>}Ml~d^DKTyURD`mvbfH0evBM(uC8-X;V8fSy8y*4F2b?%V$WKZ>N$ zRcrThciS1S+ZcRaHt979j}NA~#-{26VKja&0;EvBW3-Twpsd??L6k3JO#vT@prH~_qIeK}Cms6UhY zI6K&z;8zUmS;Q<@KOi6brR~d&`BvkiuNbcXKspMe(O=Acpv-u0?K2pdG$oZZBJbmkPWZYw>ix<$O2-{HWP93jWRY%5*)o z1oP+6Q_rRRR-gMHa(^C|A;6#Enopz8{=l9n?IC5b`#&gS-9j4WnT_ohkhw0R(TLeI zbSZe@TTCP8Sytwcmxtx^AHY|?N~34Z%2k3aZYzy;nav0Md9xEB`>QoQe_(mP4u399 z_y&!>HY-;MvNIq{G281#fGprm8XYyt@Yu(L?BrXt;odK~!St|Ng6Uy3yvA(--k!H% zZ0O(D>yr=q?5P;$6#neoP8!*n?0V`sb`D8ej6*GOqu!?taidW;YLNMSK%?i(bz^=_ zV}(gK0YC}BpTYZ(Mp5QAxHOFeLB-UyISFvq?V{Pwtv@jDe{!(U^)}%0GcD)+9+2@s zmS`qB3^EDGmYc~=f-LnunG$5#W-@mF4>AqNo-?11lg}DHWm?a4z>CVFQLi;!oVMKG zMSw$6@263Swfoa+E${awWBvge?Xz;tl6=F&$GXO!Q(W~ajlME(H~G26JyZM{##-P6 zn{zhsr=DXgh4d~+w|$s>5GMP;W$A!hdiddGVPD__j#vfhfq%dkI6TA`VE?@V$|(7q zMh#ZtV)D%7EqCXs1%e%a$hYU8COcuD{{vYn$ey&ekBu?yW3e670{_&h-?js+GYWsV z3Gf1~U59oy*gDo3{_JPhX&Skim$}MhQ>%>WN^ARoYLiW|&p!e`?C9IVqlz~L3sXqGwW6@yOk^^oI`E`;;~-0#O8yswhQUWDc( zn%|dMYK(I&D9A8|gHYb#s}HXaKga9}zV-y9OO4Ovz`lJk?A!COJx4)WZ3%7lvkqj= zq&@#QWzSd-TOi-+GV61jz5fjyRhh}=C;i!RkXM!&ecS{0!QP*;j}rVqWI6Q9TDuQvF^0t`fi(ai|f$E;?FD5=$?5#^Xs8EO!GPn8hY>! zZT;SX*z#F`b(mjGqZ8KJ|EWnA_;cC98sqvm)qcs8&p8h1Qb?a+p1#?XKOg@N$ZzCt zg7m#s^0V>JBG7jYZE+7>2(p79+icC2WR}-yF~}fK>0Ns2cWkf>e}=pSWRpKTgWH2Y zPhJQzAM^ICHMQrUG2J{yI03SJkl7ez?Db!eDM2=DO~-YX`>8JAAGk*&%FG7vjoVC+ zIazzwUQbWy5r57-wTVW)=Jl;Jjf2dX9%i1t(PYzGA^iZP7n-L}-ESdJnO~kS@PHSkMXXtdfIf8m4IM}rnaJk@IK_kAyE z)?&|vT+~jr(TMSnM|qpsS0v-lezF#|ck1emkje&6_$`Oh6GWK$2#;-H3&f zilL^GsaJa0t`J38v~rR^zzFz%VIm9PwUg?V(%9S`J&MxULOHKf7_E#g+~q9YcoS1T z8(`M)KgB#@Y~hFQ2C3{b-2js;`&7q7=Db5H3iF`+mkS&=dm{Szf!E{kX$kX#K8rt& zQW44-6}+6YZ(J{@aWItA#?Wg55Mq8(GC$;GQ3)v3w;@L(fRoAe@WOdGUtWp%i@9(+ zt>2%yP>`nf&sx!2-jXHlEpH9a&1tY_F5P%Jr)J2k0-fJv1qP9ZAxfD`*I&MgS-W#t zSqHN_*T5@dez<8AYMJm-L%nOnYsG&OuMw|)N>M0PtPax1GS&u26)8dLjI~h`MNxSJ z2;0PI8q%<{2E>dLe-8|!ELOSwKMBmGyKrn>N=g(RlJIJ1tGK+eya5d0s^U(XmU`P2@yBP^PtVm^~}-&o~ofi5ssc32xM6i z`xByTI$g}lPt@ln#@1{6jYLIJQg~lt+9>aBMy43)e8p5=lAo9$fpe-#QjDb6>G@*Z_jL1#X z+Pv)~g9a<=JD;8!KRksLx20(*R{7Jqfkiz&%V<&dhNMrk%J<}$~*A|AiP@0xQ!4UJLX-Z10zaoykm(tX( zp8y}HO#3h!F_MG|>0xO`(g|r+-0qB|^WizLW?&sh4@=YXV@$Tt{&Bte`gFfN?35^= zB*mHoQpMSwcvVw;sLV(ZW~>p!<}z7JA$k-mu1hjrj2EA0=Xklw;D^B>OBh*B;>IGd zrqqn23h)9KaSD^v2SsheMQ!YtvdM>`rHX`OP}8Xg{4)&DUa8`WbWy@FW=}bZ7uX$z zAXG{-;scmV$=gWg617dNs8b}I5Ql>sNQlXxPU)4=W#W+$iK4H(jUB&7lu{TjsAVrG zkN%RtL;H*pkLj-*7}HNxG|a;_Q1Fg0X~vo;JiZcz2Ip7d3?Ct!!71U4A|V^f>xc6C znJ1K(hE3%&P&~|jSOuJt&dU^~;8zk3OF0RkwDc|bXY8*k)}KsJjEi?S!wAX2#RQo8 zw{-rQLKUi$rHb44k)h)MCdYU{#k0d# zJvv$OH@M=j7=H;u>BGz(Aq;xPj%>V+y*56f&sZaEOSoq+xOUw*C@U?0=fhW5%k_18 zP@!z&>Y(_H7F9ruwICt7sB`^h^YyYIfq-~f99zsBRT4GPWwBS8qqzx0O#)2u*sEeV5ff!% zFd?X6-O>zdFZlUV32|FYtFeq5Jt~|7=;q^yWvHYYH>P2jfyy78Yf{HF;2 zQEp~>4gNg&0!m;6DHp(@*Ca4cLxn7kL1_aLCRa@WU3(>g2^%q-i5cyLF|ChHyKPJ} zoLy&ftBK7LM%xG?Ei39ZtPI*6pClGaylX5rce@Nmd+{@46F4LRdmg|Y zJcJA(=hOEfRAC~=#LQyl3d;tJfCIy+(!c(Ym zDT891QLG@c`0?T#m5n4kXLn96fwxTLd3=2&ZFE(Nkj=Pz6yHdF%RXAUdlWy~&iqwr zIdj2|pOXapD)In4tpt4oWg=jc7Fe?g#)o% z&v$oeZ19ZBS~sfKOEO;Df|tuIyph=?%Ww^5FM27~l)}W8DYB$61(MQ>#LS^9iVIsI zLpqr9!CW{mP^7QI=MXv{%z-EuW~L;dP!gaP2NXznrG}M4%(@qeqFr3xkoI91%n8X& ziQ>>|$SBKrJODgCw4+kPPDp;DA}b%iRTLXCK!~8y;1J#tFkN~Lq)J* zNla^0OBIc>@EWP2w$m2Kk7fBBjxNF#q~$QUEryCtv9;zWD(Vx%i)1*$Hnw1!Az|u# z=Y8jUjpq<@!Px;V)?LB|XkIG3%}B?iqDbF>-zUT?(zk1I;E=^?PLZ{RJxkb`!Bb=% z)?!ti-O0kCBLIrC$JrvVLKNwn;C(^*G5E_al3*K0@j!{w#>jZ)Xo2Fw3AV6wY&SUY z`=<*wqoYDrS6OXPhHPUN9vPWpLY8p>D;Bn{Kds1$%VKxQ=d$o%iT9QyDT)(Es-@v$ zVtqa?ojJ;O0{lY;YjD!^BJgQ4Mv?=aRFCM{=mbnonW(sO9*0sAMOC>9On04B(I*Qp zTA9WF3|b&yNAjo|N_aer|2L4t;KFiRNNMZyUVj zVMS!5OIa<%Pk=z;ByEABGtL?Y zdl+BiP~4Gbxqd2TB)6rCLp-)EhlK2a9*O~g#d^~=QXHNyQK-sQ3U(Z&86R$Fi@!x9 zMK08Z756i1XObLo_~nd*Rf-(NA>7XJn^|Ho@4c*fZyKFy9Uh?%6Eb#elY|e0fw5km zNm5G`ZGRt`fL;eIfg@gN#sxgg>3YSen2}r`&LhN3!gWv!Ho(N#VMa0< zTgqN9VR@a5DV|~}G6g$Yz}&G5rX@S$A##@*jEK1;!}}uFZ4eu7qrwXn*TZjPy|+fp~tx-27%~79aoC01D5MZR}f-HTOqJc$;)%r8K7z0>SFG-f|kHU105Hy9ZQ* zrx#>90nAf4{RX7t+F4Wff!IGm3Wei_Wl#qc=_zLJXk#K@8e0sm0gtbYNxFdNwv4rxMocrA663$oo?4r`bDk**V7sCMeqo%eR?Rqs70FKV^fG22{s5<EU zD;Vq`id7wdcVcWIlQzzW*e8phDviy;R>h=sfdH?Z#V3gEp*Y&1_Cjqq4ZXeh3pdW;)cWwDhO7DRd|pf?&T@c53_TdU1=Br zV#3o`X$@&Uiu4m~S{A$XWQYa$5*Q;OnDB&CjDTIufsQZ;*dT!sWNl;NP$~;Y53rP@ zYTVxNLn;=sN?Ax(;@)liiZ$Y#CK9Z~5XC?Y)*w@`w$*n12F;oe>yZpo8`gitS266n zhPLn7^mNQk)9CR9sVvsUvRrqcXSK^n@O2GVD+A1WRtp2nb95v-T25?6Ls)`e=M8Rx z4PK>-vapNT9%QsWqZnq96q}!lt&5EcvZ&p7(M;>&nHJ7TKcvvYXt8mDq8^q`b`j0U zddLb2ac2M?k|*27T-c>FN|Am91PKZiZ1{0JhKFY#{uUx}nhx4<_7ppy>>Ov(R+IQn zK@HfqQCS(RirT*dxuv~9Oawk7FA1i`q=pI~(r=@Y;r)bH_XL<^(+F!NzpIM&Uq>wN zUm5E&4h&`(y{;t36c377hiLT5+wojB)N{yCkLweLMIFZrhXjW4{rEwhE40RVrOFtu zBq%iZXXJB5-J>490hh9%5TYe}x^)K76lm9|i7?%78zz40yXP z58o}?2M*}z)fqg)Ei6IuK0cf$XYdVomDW|`ZK*W2aL3P3QJRXgJrn}94Cvs#=wY++ zPveq*#J5JI`YX~{^^T4-cE3oyZNsc>iZ1GWQG&0i7#a%a=Jt;B@5k-ikG=KN@E-o+ z6J}jLZsC3&7GN+manpW27GN;6vBA*B;vHwj!%7b5>L}LP_L+%UuKS^-`wa`IMSWB4 zRZX@RvVEmvohU^18R94$rn4TBU<8Q*M({)cxW_2gS!M+o+^XPr-D>(agFj6*c-I3g z96dJ5<}*6yQ|vstux-k-wtY~{y40PFEq%!%!fW>^GOlt_(uErk@9T%Wo$|CG*j zT$&d$AF&*fNurOiI)WL+zknIZKT51aJ{0p`D##-!be`bxB`!H|-l`?ZKTVTK4Ho49 zCCO_9sSY_*$HVZ(?rb!R#xe4onWgZkNlKvjFnqCB5~)D?Vb_UXSO@8ajp&6<;G#nz z`k@g0_yB243jK(FA^or;KH3o$#p|?L)bf^e(Yb?8)8FT^v~3pA+uVDE{>O-^jowG zo>vMnrszX>e(fAR&+fo7f|}1h2zj5t?_RX$;Z{An;UIg|DxtNz9-t_{-=sub9uHVR zl*TFOj#E&fOdv>^K$r--77FZICa|kYVAraDja_T|Mqn3FA$=t=CeZG>wj{AjSfxIQ z?cYM;oRCAvLQ?V#(4efhjif}AffB8&Fu(dMeN{`e#aKs_Mi(UJNyq`!2n8u~Mt!_< z$Uvd+W+5?{@@KP((B)g z4gmL@hN|B=h4Zn(H2rbgd{wuSU00ptY4$VKxVJ6EU>*DR=G4vK&)NSilKDGA4Ia%dmA`~ebuL!ij2 z{4~2^l+2#5S9muWhhrP-GA!{QqtMJ7H11iOUs1Y)`(9GHKcwjQI$;s?2XkcouI%7G zHnWaKZRK3>m*f`?&6+Tkx1XTt%LTvuCpH;NOXK> zL6ypzL01SIVC*e>o*X`B^%kE4wLbS|8FVE1qHU@^w@P(?z{>Hm2%3Rs!)l;+EPFO$ zYbv>x9;?GRjOs`^sFryDCwhMINXIMae?y)!JluLz_chBr%ENMNuiVnB8U+$=joD}? ztFPp3PcXFPG!Ti<3?qxn??|-2kXQiIahA8Jt%rR4V;=Ul9zjjm{vr^XZRo**RBZOH z5$%YkXm-n@_}*zMy306VI2 z=_7||O@SZtj|mb=(3N78Ok?#053}-~45Iv_$Xbvak7bCBX80Vyq&gfZT`RFCtIsDA zIZ+lg;MeSd#(gVvA7~-|wQ}o#+aXqfdp?u z*h8?<09nePge(N=WvaUwu)fe&_lV8^iD|w+KsC)8uc4Q&^coPxaPx8`! z(y5YyVB?ZceSE8zC%pVU+)t9lhSD~CxN5{~5AG;2pVZ)$TD^(){M$QT9cB;FJu7^{ z5G#Mi>>D2BRQX=La2IcFIjb1sF5->ALn}aXdJjLCZcKj7LSWF?@udY-wBHqh{)M1_ zjV1Gf`>=lzFK_ko{bv6zPWJEe5&iqh;EtD9I_;JEy@?$o`v)Dk+6#=qzl)!4IopvA z4+=j>UIy|-y8vO5iAB&FTt`?an5)K;oYayU z-vLV*E58Ki(zk*NYrIF*{UN=uS@*r<;W2p@G+q!}$cU>rfH(?*RepCmAmfN;v9>#| zv{*X6i`^Po6}BPIDv0G?P}k+@NTAlj(?$#WMIdeb#zzAreey)dHNw8BT$D&epb443 zC-cOl!nLd%!|k;6N&8w)a^Eh1uMcHFqm}`fY4p_sef$mJq~d%;;FAGw_>(krs=HFu z=S|e1%aRvizbUdVdyq=XIbg1FrZEvkY;3TKR}qCG2_DlV9E~yJMoPH7^v#V9)f3)qViL!dpj$~ zSbe;A3VUm^Wn&I;_5QIX2gQyiZ;@LM=pLYh5xKQrZs}Kzha?=^mxlQZ`rKPfR-;41 z=^4g^m4X(Y5X8lkcX)*Vi^kh|m)hE%q>D8E$CBgE`s2t6cq};q_CI?>+VnfXqR{14(g3ga{U*h3FuZ=woli#Gdj=^Yi*wYbPq{IpY z<%cMBql{wutMO+H}XWCy!5jzp}A5~pV(3^7)-!*)`E+{V&tjPrJb*o6R4W-yb zihU@>4m|^*zstYY+*1s|K*&fhZU7mn z5%N7CDRN&0exT@c#|Z11XS{0o-_#7>Hj-CQ0V($I-h%rj`F8n707*c$zZx%GA^#|* zUMZACz3@%lzxg!#maj(lfd2NaLh?zvXD^w?(H{(YTe=pynZ^stL2a0aYaRR(VpC&J zePR>5JsUPAzXvdp(0$vaK39V*!Dz|`q#P3aH0C?2@-{E?)tlvA)#U2OJyon+he3dJ z2ZLtiM_53iGf9NE{H;6JjnZ*PHSp_WRnlfka+o z{3FQWewdQdZ9svq_l1yDg^!PwrT65P^B(s}57SQOHA1Tjn-TZ8PRhEL>vV--DrF8& zx@Ww{eV%$~yh4Un-wH2}wgMP@ipn#~RB~;ASlph=mE19=;!(I38uOr|R3sKXLq1%f zSKwIDH1E9LQ!&NJnpjr48vWU)7}~@bbZ{-H7oc4P`oGMys3OI29JBWfBxv!vLCZk? z3TW&|_^T3rXTqPOd&U&R7MLwuLM_C7<_wG-{F3?;Pkvl5^r^W3vD{=IJueM)(C*X= zR|j`aF%)NvDUm?mm&Mkm7FMKQ>@QXb$N@lI)!L8q!Dr=;89pasKA`73Ow&O-QiM2VS^Zt|f;S*A}ejZ||FvKCmIp6o|xvRirG~x>I8O!uiy* z8f^~Ktaw;JX5V@b84gIC5~ebE@LG{5fE>E$6kx+B1!fUZ53;`$1;muu<(Hjej`Y*) zO9pv}&W^InFG0i^#hlH6F)cVVR|1bf`)4|4+-4;Q&pAIpYM40#%^_1!$en7%WfiUY zuZg2WEhZ#e6dA1i9ool8q_Og^A>n}qs{DWy@;+GTmSbfHaYr2F&UCcLbi*JQq}pS? zhds!?NqC_rk0>~_?_n?6Fi*J&5k4%e(8&V|^)}Ug%D<^QER{TbK`m5}&LnjFSS;5w z>jW$X0G4_QL{cE>xjiN%Aq)ZN;Ly{_L0Jv%1Zpb@;xh`8)8g9&{D{Sw9g=ZQWnr0V z5w5arrt#>~f56(a8@4{24^!LBZfJV=W}2HP2*iF!B9z_=MR4x#YGQXn$`4!NF#I!6PeWm^LFlK^?Tu`oz=Hn z`1bILv@$$mG*%5Cv7&mFvY*2o<8EcMk1@xDNz8ek%^6^`&jJWczLgc7V6#7End2sb zg5W^E@>X|KiD$uu{59)2bNr6%XPEOKjP(%A{sDxf+~>s663oradWQV^TKr9c(sERE zaWei0dai$o7_V*7FfjrEPQ94SFt|>WuZIvWOq7%0ozTZhYyV&!~JVx6aQ<-=rNL?-zW2C z;IONP@o?VE{oB}@db9%@)@o)dT_DGFs{DxRs#m$cM&)OMjVdo(#>)2+YY(x?h3UA| zQPs#OZy!Jta0Ljzz37In@DT3S>V?~6-Wuq)v1dZ2X{A0tr#`>J5A?k4I$nc1fw9JGz z<*H5;TCW18;OK;9fVGO`c-`;&4?$@Vtm_naTgmesqe=eH+vnwb)d29t5;u+kD+3fz z*_zXuv>VWMHRKPs?~Xb>1^4_xbJV16HPPhnK!>})*>i09t>h_Be(N>f>1AueUgK91{B~=6C!U|MreTGOj;E-s-xbfjbO^-EI7csEqh@u~q*bX2 zgnEBne7mr6u*&DjOazJ%EMFsfK#p9#DzvAY%eO*Uzu{>uo;OjAA_s7d8Xqj*^yETu zKS(3n;Cx!pJYxmTKB7szYGS*;!{^%SrS-WBM&dJmEWu*z z_Zm~u;p+w;KM$|Ryv9Rm@Y;haZ0dUB)PG0RNY7*OS*fz(V%;pB$!lV?c(xPdN;`aj z_jU0T1LPXE3VK%qu(T}p?KK&ee_;as)|xLq|5Em81?gxW#4%W-)w+L-KG!jvIQT%m zze>}W+SKAZ9g02;^vP*CKK)PlsZW0!eNh8v6Ri9+nJ0=R4_osyn=I|}7VnY+=hN8} z$Xb1D)8yM{8o9Sl1J#rMd{YiI*cEpltK2|%km-5J$9Kpr#}P@U8}`2`#_fqf=?ckz z3GWQFONMnJ_l(i>dsldjIk}SDa@N!k#w0ZaplhH#7>F$%9xJ#XB=+6%kK|bzr-LW* z?Xj<3oYWLTdg*3UNvJ`#Hu*i|sNb6Vt{ga*#{LlQX+Uo{f_K|?URut|znX?KbrPn& z!pHmI8{bRuKSP?AZOXdcXJlU~v$wLZOf)LSBIw)+9O75=cESK1nKMU$-}r43+lK7 zE0i1%wKYP)FlukH(5o31wQMtn3y=R8OC`2?`Th~-XC!Rh8#pOFbWO5Ti6+q}jh}sF zKeQga2mRxgqF{%0$?r-@HK+BkoY2rZ_tRbQA_>$Z7X%iKG44_nE zL6mm$8oMJ>$+(|~1`iqM?OXu#@P!E3Ex2Z6bi7AST1ECM($951ZKBN2#1{c4EK3MA zQ2@u{e+OuJJ;S+|?`|D%o$C0tC+~3l$53p<{Lpchk@Ex7g6NfUjP1{V&j42YzA%O! z#t-tkWB>RYC;+3<2{5M^6OQAIy*|phw{<`|#aqICW3O!z@Ony!G_%|Ex!d?&R|{zM za`+_ZkbrOi4QCamoke%NV8X*@oL5|D6@7t|Tv8V}eaQCvea3|KKBjbm_%FNn1Jf-c zy|)&{YK>RZQ6^&zVe;&~noepG+8sa|=)o;E)HL>L0y}-Yi#h8GSq&3ab z@mNM0x+|7voQK~0yqOM!$z>5d5!Q+)!cg-n%Hmi&OaFNY&w^!$lVGiY7r%KON_4!+ zcb6RzEib521n$UXA;caz+?$130w{&eDPPMxByvbyjEdUHawWQ?y0`n^LQK3S1rvEB z+)tmdZ#4~~o_^(SVyh_@dn!<5){I9S7mCNKG2siu zW7n|r*LZ|js`Jt@D-@2U1|c7x`B(DsXj-q)|JU-dO7ltbG4LrNALIHw4^u@ub5f@_ z5xr18PS;?4Kt7gWnd^NZ7{hzFmw$$GGE{VdeEdAg$4-1!o+nymwTKrR<%}0Ec6u2@ zdo5BFCFF4YQz0RPbPO1Y67nT4{Ywei_R>Gmmp!<7x5LYKEBb>@-IJs0Gi_S&-40XG zRCRAYp820`WQBQiOE1#t>M$M;m)Wx+IVWnU=K`TlPkD%r{P?1>ei6tqA-Sh{nqe)F z*_x0Pb+x>p=-*WInOSmAZy?<64l`dAb#5{RkzFUyXI<Ev>zYP@sK4)$h*nxVJK`6Z@ldD*WDOJ;B3Xen#VkYgIlsRPtMm z&s~A1WdWd;p+x{Bd!VH~8ZTXqej)sr!dqhB|1(*kRK7p<#(!p5xCcb~bCUlzWSE=` z9g~I7!=A`PDMY__6`I%-czUn~K?JwwfJ^}}VgRnZP&@}IA|JpC@A*&D!gUT<869P# zs0!~O7(o4+UK+(}eEbXxJ_+Js{O_Ytx;jK{?;;V;vr1R(fW4|ZomVkyqs zGC(9>WvM|9CoaRi@&rh`4KnB#vb&DWi1f6C<71^T2u1O4k}ySWJR1V2 zPX}GV1`Bs@1q<#-6Me+}!0-tWo^V^xV=R!S8I$KvHL`C;U5|ep8v_GV+`Y`W8im>w z>d!-W6cz$th1GEU-_&p{M~cQV=BL8Xih1(GK<^F3t6_Z9(Xs}Nlcn&1@W5p_$iO2i z+F@ag&I~+qjivRuV_4-vq(knHnej7Pildeg5_yI9qVvjWwy8PX?m&}wzAGH=y_Btq zWKg7>UdPXyF;Y%Sh#CQIw#b1~@HYLV z+(8m&j~Wqw@1c46-9b7obKt9B1%|adtfH?M8*IX-vsk@$)DJz37X_gl zTN52jlwDDe6osloG)?&w&IjbMDBvsK?~X3LxMwd&N?MQ1dje-A_T<`h+_GwuTY90d z61HeOEGZz2xDv)R3G^a&1Dn|lSXRL*x03F7Hqem9)-<&A^6jqrcnI~4&~QY#cNHQF zv6vU=I8Wg+B8ZQxLYT!P6`F(sp8at$jguAgnpbL3*ehyf&I4q~WJL#sZFZNiX5JM+ zb1h7>jGLi>zoXc=GoERRmDK>}W7614Jn&M!77{^$MF`XQ(b(KS;W>VPH2#OQVX%pa zgk=H)X{_=ugcoh$*jPnsYw0!Y`Nrh@Ktno@^z4+H0+Dn@>UT9L5@1`yRAa(YHuIFB z+0v&PvzDfVMoXIw`(kO(l*;$Y;iH!k0QYq9x;S1XIJ8UK!s=Tr%-P23+bkCtTgCo6 zsA;*k!k-6j1&PEl;THI__!bA7eTvQQ#{e@$N7(ERR&)|=y-+R_$|X&&o83(%-yr)i zo}6hR^ZawDGYxJ|KW7?XKS0`)w@(iITc*5`q@X9RLIlK}&rJ5jCA25~thrIIu(vve z`$RWwsKi`<1f}gL5;^HGs1geA>S+O8MhbgeJwT%rcc(ESpUvztX62`&#&50#h)ujM zc5WrkQX8%bxd&OLA)185Xo=+E=V75vnqQ%4Tmj!e(g3M?y`@Wx$y;cPAq44(XiLl} zkoaz>oUQ3>IT1$>x3v<%;G*+{oC0c&TM23=W)LU?r-5v2LV+ zE?jAPC?Q7x#qHyG9D57kr5Kaxl9HZgMT#Z6leZGL2u{f^w0J14ei$V`tGIdtA;6kQ zOD_z?RX?mn(4S%*f1n8ow_gSzR`@Otmq81ISzYS0%hmyWpfTfA8uxuP&V1=*P+w5qIvJz>v229jPHOB-{=k?Ml#I( zq>^?UG4hEymK{leLjYg%0zjy%S9XmmJBu8mWL$8czmV_WkRXQvN9)0!od7iBug#Qe zkQI{{lD99fJI*B(N`^|IYZI2+NF@^QU~Ry&Yw$OYu9f_Q@IeRLbXsulmT*7t+uguE zk>B<^etwGhZ9TE66RfOW@Y`;-=9DqPPyBWO`0c5|60Y5B)9$iU#J{&g9`Wz(L;Snz zi|Xr2t*>jSzU^#NcUd>qr|EOo%Hcz!GVz^2v074cpnS+982RTjeGkTB8_I!J z6lo7g84Vx}gypi3$mnHx37MAyaVWIP+;8)6k6q?-LHqK6-n#j|6usb|ZR>qjXTlSL z6Ec}ux0pM6s8QX5)|FcZ8OuI&5Ber~Kxe#LH+(;k;PGkP;KL7}HW^Gjj%QR+A1O;L zz13n_^7XRBGWadYBV_R+HZi=FV$e&%sM*y zT~~wQGVAg}b%m+QjZ@KQ@|ktjsm1$z#T}H?>MP!-@j1CQ@;d`)rgznu32Q<5^tk&J{ys2|4t7IWW}ag* zn|{n4!6&qcRp6n5VvLgD#g@i#XefqHB^;aYgvZ$mw_)^@wWk$CeF~%kxIek_^ld=F z|ADDlf0X%VneXAp0_UNtnOg22G(JB!#scHg$f<3HVW&XA-tf^hjW=qpK2_?X7(d?d z!8D(%#Vd7r17X_?BkPqJM)qH38WaAaxekiZjF%R@iNIp6_cRJ%=V7z=DaaN)Y|bHg zIF2cwLXT#TTMw_@_<9CAxOlx4eu@^aKZi*J@Oja=ci^$;Jmx;I-Uj`j-Ht^bSl=ru zfy|Q%?rX!_A^g*hNuNMc2flT9fO^7LMLqcT8RnhAUgfUGsy@W`ZJ4tapSD9zrhS7q z0a)bRAv0$$d>w&*yEJz{3*Lb1#qsP1Od0wZ+z|DD@3(mfHsuFVLo42*8k3i+#)QXY zSIjGp83CF(Q{%H$gLL9*W0kLy3iyF$*q@_Q&M%-DX53%Jf|2=MEF;QwoQD-25`qaj zdU`;Tc7?F^=|b~cKBfGH2y$tl?t%C3L`*1p&41v zYkYAo?q|_0$zJ}M*Y%#yDD8z|R%7STdD*qqE1g!2?B}K@;f$a0y0&=(n{609s@>~q z1OW3H*5@?$e)hDI+w61iVNU~t+Ap)G>ooUn_-WAGJK)9V-i0MXn!5oK!#>K301TIF zxy>v{la+Ip1uuq&ug!sSI&_Z(vnahq1v0RT1wSV7X_s7f82vOntiI90>Jva_>qGcw zrv+!B{yhtGVWrOPtbRK^hfGgOe0DHr6RU56XL~MlZe{gbDY+9S{6mG+e`pb5Dtq<< zDye`wGT#YwK20X6U@YL370@5#0cR5e=T`wD4+F$!eHFg{_OCY$Bmi?hh3AsoW&~p> zv-lbdq6j>dTyqX!w;!HnU1No(-Bd1w<>22bN(VqL>Y=Y}SQe}P@&cLNVs9D^!+mCf z0*~R*IO1n0xfhZX&j5@(kn4a-K*lb@s8-{05$E7v%tX7A+)l(@cdJn^$ORsD1)^mWu1+GZolPXXyx;3O1&HAVy3HQ_=p3C}eiG8Wfk>}NjEcnPG3XCT zhk#RM?Q{^WzS|;}{kR2J9q@+L_k%Lytb@g?!-s9~u+0KsOGZk~wGV3f9t^{lw=ZS? zj(mI^*5A_vG~)#96cc(3CiM6-3*hNJG2`&m1y9{DKaT7d;KQRZ+n>M$A%WZrzaPMa zwZZR)fSxj&-38c*Tyw&U=pi6b6K3z2*WKo?kTJ~O-8e<;$|xZ00(M1=IXht}iTKX5 zcvK0|6K66$w%>du;HNY~iukAtGvcTE1Yb`6r|5Uu&=>l_Livu{d6WYN-I=55%W)%8 zG3=_GXk>4H1t8h9m!niOg+i->XmXl&D6Y?1Kk*gs^huFv?5(WJ4#WO< zAUq#KdGh^v-Cn*qw$;E>dS53GI6l>;@ZZ_tpF?e!O4mzbx}8tWZkURv-z<3{bV6p& zhWOOnhN(_rJ>S3aU4~h)Up|Q%W>2iAG6R$;J~b=QD}A}KBV&CZ?oLLLj$aV}cZ9!0 z6csRT;BKCmpBwH8t>0~L2$m$o;l2ji`y6b2dEVCL%zTlAGlYkT8nzd5!W02bpNEdE-!qB{d#q zbu*UXO`r{U+>w&6B*KbLX#n~*z+{_yhktLp@)|4~EWjBeXIBw_6`IJwq}KMbTAaOe zAmg%U!oFYv5AEgKcCg?q@#UKX6tk;Mj2`&Qwgqfa zYYu*M5WYFAEby{l#Np|<0vVsBEnvI8{f{@XDM8f5R<4E^iD{+(NJ8(XSmgceL;OBZJ}W8M}>J};8jiewn`uI=7HCGMXXk?X} z5PZ?bW6cY7%o*Gn)Cuh{updE|UY##qpVFiQfTjyo9>M)Oxsy3A0*z)C?__sJP{-oz z?k1KA>6sTnIdMXE4{qAchC1_8>on|T7_N(wvYrCz@@vx(Pk{#DY6K!cF}AC&`n+NO z&DKXT?rs@we?Ce<`|JI2jlbrk39ikdsFqouiA8>`h)9@cjLd3%IiTtrlWl>9cEVNn z9$q~QvP*BuYovn#bUeiTM&l5!>q`r~c^RKroQCr{FZ z)9MLt>n#f$+h+kQj}-W$9P(ye+b3(SWc9UjpM7CFH~wbf=fux3)|(tP|t}o>1@(gU@KI+hZ*d(YAvF}@6q{iqn#Ys-_p!?1tm=?@&BmOHrGhg5wXW(B)-1F0^L zG6mxbjz^jsS`eaz{gL*po!uaCe|Td z#X8_qL|c^~1^R%zN(CAtLSwdfs;&l=pM(A>xj8cE=Qi9*N{23o52oie25Jdt$dmiH z%$r$t#Fzkj-66$YpUkJ&Wvks@T^kG;AYyrJhbm{|dQe4N>6&jDDQ_khn zi|uPvDaX5`Ynp4wH>*l7XPObPOQUV;hDL2``?BMR15bIFW$-h{{)1Ajvis zF>l~Vr{)5|^bAIO;OlzHMSvKFs;_jAV_GbWzHZm}x_tc1)d^LtpsF^T3|bB!8D%yY z`%3@>NpteJVBZFw(;|W)*Wkr19^M6w)KFWsSW|~)wl+K&X1$6KMSOH%gn!PI1}k0n zG$5RLLJWT6U++7OOx(qu!HI}{x z1#t?d1SbF0vgA)5zHNx$!R%mXVsF9tcpZ|tcOT8bW67GB#gH1wK~@5tLZDTvZBTN&Xk;RETs8hgVHOmjuO{E!bOgMUb2dQYqH5ct%l8PyLA;kAfy zGEs<{LzH#}El04I#u>lgg8!S8SDuwvmm?UCgJ>^c6VAu&Fkv|4+d&}0@6e04Ks89y z!C%yU3SzS#B?^s!VhPT%LxWuyqVite1{7W{jJY2*DY{PD3!cISa@f*)JxTu=sLE)? zy=Z!c<_m~)ek`LKaxDsFwo>NFI}RO3ag&Z0l^sIyYAA6SK26sb9}i78CcL8Y{uqPF z$AoAyVPKg^Xig>^Hxt^D2_KpX?a73#WKBO8Gp7-+i@zc4>P#ZqxO>FkLkN6J)2OJ|< zKCTm66Xrn#b#;Xr2r=Y`UjB*KwMFBf>NJ^90&guC;+J0D?sWkl-s}PH*auR%wF;xd zn2xJ@b%_8Ygihu9+GawHl@Ev}ml5xsJxp;6fDOxrD^J5RDQw zY8*UvB0)WheYTj|$?y0%jODf*l45QfqZ9j+L3egVKwJKw3`PD?tYwK9i#Z6r8bK$x zll>{Alc=2&Ue+hU>@TD5g$AStCz4EZg=9uPWdU(~I(}Nr0Er9RUAr}7-19Q62=Fve z8w90Xhn?uiPQk}%Tw4dstzL0=v+^r@P`9v8oJ36J;X@{oRQQ00?}%MniWlbe!AQkW zNJ@Z_wh3lRppzG7wQ`Q`9Ro8)B9-T&Z5_WKj72o43kknUAm!rq4WSM#%=^L?We{#xGxCupSr^(gYoU<2J4d+OMBSdF78D6ILiZ)r&F9xDO@`e5c&WF z?jtZytV30%0HdFgM0fk`G2iv1l;-0Oy3n}vbx0IDt}^Wdc#n#ksGx}D0P4b$S-JTO z*u?K&5&FwO0<`+w@UH2LKU(Y2J>%j17fP;%KU%Kp(Y4%J*u?_sB4`0E3)n4J?v#K) zgNh75d54_vs|B%7ewT!Ep&V3ri#_8(XJ1a=p=6z}2$CYZLZS_-yg=mxy74M$7{4%9 z(=bJyiFK%V#GWuKj(^WoCh}3!R_N4hu8QVd=q>dS(`Sjg@E~M09~ zCVQEQ1(jqa_`Ru&1C%Tj;6IN@*gDix{8z!6Aeu}!CT|4LKzT9t;crj`u?gfhVZM6I zkn+XKFCw?8ou=`DRsI@;3Y|=4KtOB6l_TLm-{nh`FVgTQfF_>x4M=`bH6}l*f-Du7 zlV)n`pqhg~jJ;xkX=y_tgL9#;NGB-XCDeo^PP4ffI5-m$hQ$kJv0;pRPQw=yonPQ$d^86{;!vY z=HH32vBmtO`$$oAXbtdoqD=b*YjBrDYXBrOpJ2=$99#q31l4Oz*jO*jJ@WxBeuAV3 z`ZRHfB+b7tzNd!Ef$<6FYPd$3^iC^9i6ho&d;ERNkl&5D&`62BWC5ac z4F>8+;8Fec=fu-kIw$iV5_!`E&R@coZzHSmZn=AlRFY?cVuSTLll~*n(i>24fndou z4CAt0@*1VEn*FSN2HE|OsjgFlBbZz(7Iebf=3;7wb>k0=$vKepCm0#5=;Zy->G?jX z-5Uta_u%=MsSQfSIic7oPoBe;;GvQprZ!kBOqK6RkzuXGUoR-hn!CxrIo^ov1-G*D z>j|GeRWTr28Re2;4nEJZ%vf_1}xWA%)O zVCj~MrMsW%@BbezoymXyN9)%vxBv|km+_i}8JbcgUE9FQFBbhXS935ckbp&{)4#k& z4IJ&&gro(0gqZ{;X=$HnSSvKacxD<|74Vvc%juI2`V9LA&=*+YtQTQb7UHHPt!He- z(0byb;}y7a))lyjFVG^sfR8WIn{$k1l$VTHVcy1tp^re#MOC=8KNgC9X2#*y6j}_>4rMVjs z9-I93_!a^w5$wekQF%m0ljV1T{Z2GFc7yeCQyLR+_qc$&>durmqX2+zCIDTuos7X^RKxv);eEehNv!kKd`|8-+S3BWMx_A8JhZh1^Nf2k3O}V_+iQT4CroSVH_X$ifk(z$Oew46BK&Z?xZtYu1-WN&Zq~@4BO?w` z)%}7FQH|U$pNMhpd9~vt8hp+V+b`Fb+P(T*i&{Jt;4Qf9B)F*U?vqy-OJ4vmv?GxO$a86SW43SQFHCkq?2k{Lvj-rc@9FiPl^r2`z!wp zD^~ftpJ7uN?ha#ZhP>`m{)22ybp@?+1f#jjdf^yZ_f6!+VqCSp47M`Nt5=*PD@1j~ z)}-;5J5QiG@tW-qCqnT0vy&F&9K{myt|X~227sMn3C|ZHJgD4XC^r| z%-&2SF5x3-nvwNupp4%F9#J{N$ObBz?Evvda}B8O{rT+a2A{i5W>1IYoL*1xa6Vhn zpx7Ef_FB<{nV>c~yG&(DkNS?9&Q`mi?1z%>z9i@RyJeTpIa##*SBf1pQk_TR~u z`R~@jT7VU0{T&O;`UfVqL*gz>ob`9BH?Sqm=l;|mif=`eU=SxpHxQ`YQ8qIQIxRx9 ztb4}sAa)=?@N3~lFV1G=9J>5DjJK|xq|NS}ljhBJjU6`*zwgLc2w8alD3wVM62x+P znPgPZ*eGlOLG&c*2&ZqBY-VeWiS$aeSFBXWJ;V2(bn2eyq>}!!S#lL&cAd9)Gn)KP zije4ogDP|?d;rz^yO$3nVz=Cv0W4$Xebf~nY2-xR6}AZ&m;hLKUR;#saUDfVHrCO4 zSZ+N^vB!Q#qSRS)e2OH(uoLb934uitJsiA0sv$P}XBnXHhWoNaaJn#^68;Bnpt(Ez z`{U!u9E{({+%pb&A3Wl21J1>W-3EkhJbFJ`O) zn6&->;l2z?i>9Pim}$)^X(eV_TT0qIGp#)(O*7MCDQOeUw9b^YTr;gVCCzak?nBgr z_;i{DZznaQ7Ds6*xl!YX028R^Ug-pFMu_qJK%aaBl_dPW87A7B*sQr<#;^Q`0i3fu zu=>LCSxw|0B)*eE)}$W;kx>!&py&kc@{jJf6eQN+vfq!Q8_sY+L2RlB>Y1+xk+>#8 zuvim^o#SS~f=m=EhlwF#HF58jR*UE|ZdMWBShoUxN-p#0b0;8l`ho+Aj(_*y<erZ>1C6toi zX~qMQ*ROn)J{674Di=3CWfMEHUtQK`v05yf@DKCtz{?29=^7P{VtH4v*^8o?19*Fzu<2Z;j zc&`U7j;gAHG^o4{_~WYMsEs$P8S@+{dIi@WM@$U0^qSI|yb-6!#}9a1+Z7k8s)F`( zDu(q+5XC}Jp7$%RCJ^4N=wd~bq#=BAtklqw@KP)d;)_0h4yx>g;$GKgU`5yBrIfH# zJ_$zqaxeDMSRmo8vF=^iFBA#bjDs~2)OtO!VIFKhL7N{dXhN*jLeVnKp0238$qpim z7vvXS+Ya@~n=FIxZS?LKe2*ebZn6%(FQb~*;ex2H?cU<;KG!Za1HZ@nOR@0A1RmUW zZO5xEXccQLolrQ#(oUdAX?itInZlc|8=$BV!5B1K6N3*GSCj|apq`o@@rJj>2W^;e zG9|%rf@YrrNd#-e3teaeuTfe^nhCiKuY_5OZ%DhE%^(!PDw3mK4$)6LfI+X!dqv@Y zgTe(k4w_Jps~NKfNYjG^je4~JB!fUJ zxf0x{IZm@#BvRi%m(dgSxB*=|Pmm*N4Lmx<<63wee}c@XPa|;BxYzB%%woJGapH+# zwXa2};VoWcIyFsLAG{BLBy{L_gi24t#AfV=aT@|&5vATnO9hW~Iq*snQ+X{i&5rAa z-Pe5pXgsMiK=~%UETo@1&n6Nx^fx~t$=8GeS0^dR0n|40LaFDy${$u!s1MB2pH%Q*7J0iptV9zIlg2XjrTt0LaX^20ci`cP$JW9Fb6Eq-r3D<66-s6`QzkK4cPBSxq0b8mC z3e@itT`T`FifUNW;xKD*I7GpDSj$2TJQ7EFE~P)Z$C>mPFxE8_&NRw3V=!wy)>;tr z{#5u2;D!(uz-@z<%LIAFcSznvKgMB_g`1i4nkya~NU+(hZ1xp@*YW9!U5~?05oCqg z>^i96B}&dP$}u_2GOMYI%-~^B;Tch(wNq3`HT?&tn(i+jUek}Msf<4_Pu6rs)I{Gp zf?4!qF zysGG4C&*}PiCCNjd>v2t`m$gH2X*2eVya^q_t4f2usFbEWCXFgyXcmY480WNzqUc& z1GUQt{)E_vmOGMNcfy=Z$=DMH*yOBb=%3 z$fD9=IWIxZc;tTW=0(Niv!_=h^WeJ${W0!HGndZ#98_K&L_yfNLmHmAje0M@;4Mm_ z)&imQK>c|jt6%ZIO%S!93K~ZqfZNB99mX*DkDX!ljaRe!(A9X==SjK(s$Gy}+ZPw? z6a|TI>%?onprE6Z3g(D{y~L#^Kc<>dF-+ox8x_Pq`UN^^2KA&MwsCF-Z2-_@FFPmqp_cr7pZR4lg_#b{|`XS%q+_*zWJ3c+;~lb^I2iQ}1Pu zVXzH$Lmhjpk^;pSeGLEV0k7HwUe%@G63iybgdPkMv~D5sgYQp7(p>olR^r?aihDFr zi}J|mU+1Wkf+%9JfJ+j>{|V7c_jVSzlJ+!I-XdcN?`uy}9BI4~!U@gK!ARiQ81y{5 zimc&{k~*ncCE>u_wOB@Vof;XB8mg%rUd^1xaaWiAn%B~$wt<_?*^i6BeU1K5yn`TM zus*z&_z{dh(l3r1Ooq9L3hcxiG}ob2m>SO748x^g%Ww&7%-00G{;o0Wf0%z&6j zdu6hGgSCvn1kVLT{9SQ7%_7C0fYPF+E;4>6<6UQ8$sF^qbgPntb}F{1!D)Sp>T;a>Ao;xZ4GiLaU2PpNqL$i3UYA0zExqwaNB zXUTUiTrYx83dmgn0IIpei;tz;k7B%)qy)0=%SQ_4QQ4gtn}LaYbYFqVc6k#rZPE|h zq0m-Y^Cw})C8n|sNu+t*E6QZ;^Dw`4IDE_m` zx2rw7)pQcGg>Qh&X4@@v1L<1AEJ2}A1|5Z$mIR^zuLcUmto+NL2~8bm!42m43T9#_Pov_9wT^7AFeJYoAT+~_TC3-qOJ5MDejpj*<}M*fl4CnxJu&>u8_+} z-7!e_MyQ60YTtdr-R3=^6M90am-=Yjw|08cI>HMPhA&;Yh}i2D?nNlsUh}~cjMSO6 z(kxP;@|BO!rlmME>B)mOPBnPwSct;su2#6`RlsW8LF;fU{X+E%iW;rrByTZ2^iabBgT(h@++v9d?xYcF(+N;Sh@-nDYP->iTeQ1 zUJe3XQ5^n#%AD;$i#|b=8uu<{J+RUGIe8DfYfbKAZ|e4}kXUdDa{h*7OB*eNEj>!3 z-kq1AcynXFpglGluN1!F3tGfn5JSg3hK;Jyc5L@;Y}fz2jyv)a_*61uSjC?(XY4e_ zuE4-U?{?fEnx}%-eSSgUB3`xzV|V7h!_Q!g(_2zECJIus+&>j}NAN|8h zLpH?w{EPiNO6+;v9W#>jOt@F2>ch_532gQu_G(}CFU+;_2~BC&KWb(S1Bb^x`dpuenq>v_aO5R6;{04>XW zyjf#w8Z>=wg^z!%VF39=1N`iP(Vo@#Aun$oPLHQRE8n9_S8?d+0LZzM2464tN?SE) zD|CIp|9*TPG=&$iq|u_UCkE;PiVN~;zRknw>XM62voAHufwPhyueSJiK&Sg-0lVee+$sl#w<>BU^>s5wle#>2H(pWuloj*mS6MGCFn_lV zH@sE%9{*c%H%#C+Q68fDvar4@=3l&pTV&wc=*LNfGEbaHtcoSkWK0dihbWYrn?NiTiTM zn3tMi@qu;UDpk(}!ByCYPpcRr^axP0kcao_p8SG@KN{;Uz?KSY&?yEQMBTurSiem>9_+KmjnT_UO3Y<#2sL700u5e!k(C#N#a1n{Rm!-@q zT3mgrF?KIqYt211LakdV5FZx_#pm3mrx9IPO>ypUUm8?5tmH1JgyO%E9B1C&_vGp1MM4wB>3nc(#K%2kelFWQtXwHj$K$SdE9zpL9oP_WJkZFBYp1k8; zUXMU1Fl0Q+F|^JESQ>vBomV<$roa=On|lzJbE``CeTjm)fB=|voc)ot=~N8{eq-g& zk!Ms4s4u!KJSrYH3s+&p*M3|!`F3%5BX-#j#KL=m-FRoHCtpCXe>1CGgnm0bV!HJJ zDA6?)Xls73X4v_ptT=kkB6uSX=%oOLAA9%~5ATwHgt+#&k8rKZ!()nGm@lyD!5V>c zyD$_AUK{#$JPqpSz2;GKpShfNP;|l$-hW~Ay+ue|$AbK-<$-jXl_#R_0UOM|8D-a^ zz3`~rOhNgInq=n6z8U^#L!tKSRd+AQ#pjXPMv3fTI8)tMwd4+>H+X|k@m`pVs2V;5 zMCZ79U#6awt4YVqvDS=0cpUGGnxJB0fFB8D-|nyf7v%qY(}(*b5%q5-GB1vPazh-B z3yPC66j!4>R5Fga$PtFQ=%5aB2`^O_p+0lnyNtOWtYR+zD&{I(o60-dR3k{I34x8M zhQ|TFQMCq-jgL14(VDZ6ZB%+X(b*;I;qR!%74lMy)M9^Jm0A)9bYDmf^f}q`?;#lj ztt{Qs41z!Rh2r1F>m};rZFr3FBDOq$)=A^iJL&7J*Hx)0ad7bK-(T=`H6Bq&W}izN zncbb1lHGqn_8ANYwTU8%q%XArBebhJ(`VZNf5&a02rfUg^1a~$7{FyWK$sc8B@o%= z#f`m&yzba270t=*D0Vy~Q4pK~Z}A>Ed?bCUVgI#S+$HT(i_ZoUY3qgjzr&Lk0ws6x zZ?t6Gte(BzKwr*7_sKk(+~P8+sn5uI%4gX7y;9ipZr$LOB1s2VXU4_97)aS)Kz~Vc zYPF(Mt3pn#R&r|fx!e2)WV{Sv2R6u?d>kq~m|ijsLv`V|GXd+NrPqB%_ImVy3DnJl z*FKOGFmA~_^6#F~VAV}$VEXkY!aj^=;e+0}qTWF7xrY~Nu0}Ib+2cS{&>d+bM!pC{ z9)Q;YRjLP^dLS=h`VVeAW4foBzZ-kack_4SIrCloS~rTH4W{p^60O3$k6Q*>f+*zB4s@!3Ej#QnP=2LH4ZF?4MkaU6`6Z?}BWq4|oRX zfHq(iQIu-&epsq?n(c+2rCD5xPr0C!Ma8fWH(;rm`5hBKFEf)eb33m7yiAO-ug@IE zZSfrLpDYxeV0j*)UEwUR6&apI8uY_tq%@6%a15BLq}z zn_)Mo?ZG%yTVhkEA_==*;l&_J7TZ-b;FPR;?0hPS{lC+I)_fIYB$hdKoJwIYM8Kk{ zj)tk(csRUaYW^5X8H`7D(j($g{Y(+@sB+K}2g6Odn|I`W1Y=~r6J$xpX%BOJ86y~e zJy1Hv;=c*a2mbLWcFA}RC%Yl+|JrYJ@NTBcE$QS;rtqC|pdWPq>2$0q{ulC&ZASA8 zMd9z2N`Bxi-YKqaECm6w$IH*;b%W@^F)U){+-CfH?=BDD9lL1?9>v5cl8$?K;orH< zd@ptjgc1z<3mO7BZ;emK@SL3r@5kE`6u9EpyIdEF?U~K6w$#cGN z4t2G-(Z_e7&-OPpm<_fDH_2!VF5`t_yqGi!@>f0DEgyq&Ueu(GDG9GT%*v5wczKJ@ z)#u~q@S40C#yEeKe;NrXjD^Hub| zY4Cl8CdE_<+6@o*cg5qD)Qfk3O>7~P0>)?0==kXXhTu!y_Z|iDsnv{|JZBpn7^4l% zM={UCoQE*{<;nq_L0Br5x)Ca*i{e-}R83^T&Y-$34+Iwg-mBlsbz?sGQNQ9vkM z7v~heu|*cT^J3b`sie|UCRlS(@UkX7*l9FSn1!#rdH<3Wq49K8Pr+kXlNn*{5pu)aDq5rLIx z2ZhrA5#CJ;WNl0eSyNs8D({0i-H!JUp)?4hAYzR?S6o~5`dx@UogJ4`RPGud?kz_K zSowZ&0N9aL0rN=q#>hdK{}0)$Hdc6m-EoLHKY+BoFz<(0VH>;SLs%r7?}K=oE&P5+ z6;@CkHUAHFu;uq7Bs)Kh|1^P-AV=DlRJ!Sb4rh6J&v4t)-8PV&8$8dVS*gK;Aq?Q> z3GqYF-$23S$wZ)!^@?*5c)HzJTx|73q;yE z9jvA>LE*mijxlDaKq{Tr*g*OmCW+_=8h;H{K95mL?7pn`EdJEuUk}q_3b3$bcoj3~ zl_n0z$aq3V))$T|xivuH`-BDeMH0v;FoI5w$LahOTSF5O?zKwIFZA@OA}&X`cT^nj zY#aKzWFD}dH58&kZJ;u^dg7pXpg&DqSMF<)!pR0E8rJpUqvUH&t1Dac^A{7=#-?OTWJx+c%khMi~m~HC|zOR3TPhCRQR#jP2Z>R zcsOEjaRaoiiv2SLZXRK4T2(XRh%sSVpqBcp=>(-qeq#oD2>fOup2#P-cmT9RI?0KV zKBbTu;K#_m>G~J&l7aBqQSLT2vu)B!9BC;?yEGSyF)rOaqNeZ@GT#$hGa2QtezRJ$ zgDR)bFi6%)5hKbl0T(;18ChlCvIt6tygG(ZUK~lTO}<@rhj|c>iNlJZ_kNUyNuV&r zuzrmPF-V1gLcB+7SSXg{O>!c-1xFu?|3H4gn-y0(^o_1C1?I2+D(8is0Ui#5loiCo z?@O-aQ62$JHPB~W^i}lQNPq;`@f#8Y58zR{$0or_#uaH;fM2uR=iQI}4*s3<4seUR zfp5%>z%THtxpidKd&f4IOt1FjD=M2_cd!3lHMq0n8m}?=9pLp86n=TKh&K_^K&Uvz z?#%eF`AF&SjzSj!y>J|C`G4YYlOwr!a3ptMcR`2$xB9Cl`WwbowiyKrn2F73Fgxp=Ru_i7))lh2m38vSo-VFgDSA1dvZ_BtA`_D5OO+kTO6 z++9o&yEx9f$@OqCGDaNA#1VkmSw!0@0VzT>B7LS?cl}rp%?vOEL15W&`o_#7A4}YL z7fB@MF)R}E30)!e1EOukR+?Z``UY*hxz!>9I{dv19YuvpdH9j%&>@4?Fh?63wPQSc zv)Y{NpA*dFr#~e3#Sa8#LrMic$wqCR{Zo_OEcChsK#T0FPE?)W!AkJmBqQ@Xu_y5! z@iR;`*%townBJ1e!OdJG6y9PoRh55d4(Vt%EH((%68(jFVBQdfi_S2MHm?HDL&}Mp z<_m1oM1=nV25g7*ErUNfqNd_qlc581&Y~}WfCMpDH^DbeTqPKqn>A~fsAauiy0+dv zTy;gbG2miXZ$d+&2Us`8laGAgWNa{aQs5>Q>oGQC&-gAUiY&XZ*c{V}iLGIX89YTW zApC>p1>6cU(Ek2uqwXns8kUZCKwVgvmH_%fMu2SysQRP~+V44MvP*$zWkRY>m8XZB z`g9r zWlgYE(}CPbb~y?8ko_T`ucjATB^$;e>LyMit%VqQxDa%p@`be1!Z!F-eacTg#=-@f zQMAl$*R* zE4xp9t5s(AO?kN$G?nye-LY0sN8#VFRe2b|8myP#CjdXwg0t8{zpq*171=mSht|+v zF}IBmuaF(8{B;_D-x9_Xxow1}K4eYMi{3ION($QqN`DK&R&BWrH5YpgJgV^S&k0fW zC-7ip3z4C-soBUL5v!)#t>&l=FL}_Kz>D6}z02VG=L8u#=Ru6W8NT%;Gy;69uSfEI zMhL2%*;Z4si#}lXxH$D@2xtTtzGI0cEn@JhqfONIPf(z$FHASVY1N9@!e?m!dGk=b zpmJb*;DiQ2xAXc%(CwSi>mv~cj>0yCM3|Gv|73U;yA8Em6DWl`;h6FLQ^Q3+6mi&& z+VLADwc@y2;MJI!flJ29x(8+1{0v{~ol=7@aLmz23B|_J<^+juxfPeu3XMSWM%DYl|62H4KbjEiie$()h8-W|)uw zP_04`!T3WI`Od$>?X33@tcq`HzCsFu*g{!PSi9wql3A6cIZSBDD@yP++-|Bc2h9i} za)z$NV*TOgjtgfXG!L=ml--KcTmvCLL)E9bX_rmkBoqnY?J${4^e?QxA|@505XPY> zKEf#~=ZBM=M0wC)Cb@ed2nolWM-W_#rr&A455^q!h00r|hDSXhLJQzw+{q@z#f?dt zn=!cSeHbyifo)qS2~iI1vD?T8^}Y)HE+y+SI~M~g1968mY899ljUng!<4j($L?|u#lLVvfhlo$R!f)(v!MMv1o{cPq@ zmWo$X;i>>_YxnNaoSh{jQC|49yYKrb@AyF0sRyBi5`Xzj*VdC}D;0F7z=Y-mCo9$q zjW>ng9F2gnr{V~VCf*{(5aB7o5chhKZ4!bBZau<5xP2soj1h+^c^Igwn5u`oK~hm( zpWZf$M4eQ^PzFKY%t~G{$qI733W};n|67ab5yew+wqy)GW2&Qj@cK0i>KTJnTBOrL z2;O)p@|)i4|7zSu)?B#?=gH9sOUT0oJ^8S*(%xbP4e+<{trXfewx_)XKX4{|Y#Y1> z&{{j?iiq49jNLIHAJ+1?pJ?)O6IU-BxkG6F&!e9EnW^W76!fE|T2sElhXo%(8wuNJ zGv}2%4nZ%JdbxccW|B72Za@mDjsrL*c1{*9fIoVY{@yX}*w1EJ=Psbv*!1HUSn#C8^3Je)7)k_}I)QB|d+6l@v#0sjV!xN2k2b+Etu1CquO<@JwTp*EpoUJA0ec>U3EVDE*AY zvc6+^tt>ynrk!J1P(-lR%Cfpy{uR7Dt(|3^LA%vCHtH+31npJuLgbm=u7)u~MuMTD zp<>8jg7sY>PzTVkY#j-dkqsICxg@qWgh`h`mBIh%LCl(DKKb}$vPBnJjVBTOcD?8Y z%QPMcM3@v{TWT932udc5GXnKz)~Z;@ZA?j=<-Fd_cP?lJB3SPe<;=9sn2hBJj9pVg zb4w9~Xv7?fDYt*%61Ktmepdu5S{D0GJundSzw<~!0%Jgyo=Wc0i!*w3;s_aP>Z zJj-UnxH`aQ9%S~BkHCzIJ71jlHaUYOx0?!IK?6Jl?c0xm0!vM9b__lbw;ujd6B`EB z`8n7OR5*Q`aGocW+$gjwTE}o={u~}v#tMU=F5GbRO~?cn{#4;1O#sE%TRxP$!*J?M zbkROa6w@tYm{uVsSR5XqKAPD;R3Bl^NLa^~;#2FyQ!^WAEEUvg#w5vQxClP4VgeT4Zh#=_zH zi^zf$j2c8yp8!M?yqRuf>g$&Y!5w*fICSs8j`b0a5k_^Om{#(5-wX(fG14acwjGn5 zUlWun3S+iF$@fhbstL4JAO(^=vJ~ybCUeC+y8Uz$VfO+F@7;iGtCnp$ONN`zq97y+ zn3O2sPXx=JFRHzlR^!_6l!p@Hl{6cG3k8+1S!lKuLV`<3>@Po%@u%HAl0+}C zY(LQpsqu7p)dK@Xu5SgdTb6jUM@INa2wY(qbJ{jd4R5?BjsV@WtgmPz zv4WN+OwgK=xik*!K_mEj^k>BoCvgpv?6HDlaPj4(hyViIW9A8Z@I$yZvMhcn`r83( z=GEy>XAElvZ68<9Y;*O@j<25CxO!&yI9kE#iP1+htY0eKu~q+Uy{u6ef4Rf`IH5k} z_8pd+1^YEE?&gY5BLoI4Nc2Lmr(v;ZMBIcz8rU!I={pTVWV|s4Yn7>OpbUF$b>pjR z3687Z7s6>4b#Ta(u3|Lc!k^qtHe*vVO_^v|xM3OZBnu0B*KgbF<|oHV-W1T`lp6B9 z!v2ijisz^oVWJq^gnrAP5#klX@P8g56~y##8zafT*X29W;<2B0^RHa|q{n{F%`bv% z?XjN$*;<#!nnb{=Pk3e{x53+-oNBVU&r-}uJ2LtE3&AB<7QR`wqlnQ|HKUO35*}? z-;(|XNB)Y^C`?DbQ}dm~#7K|NG9kzkT*kQ1G+#}MYKG(Y^XFfrEXBN~i!4iBKEKWF ztoELU74e!x^Cp|$jGzkcGLm0c_(}V2jC!NXvsQ7->sl=6-0f|P+)NH1%_MOs{74{@ zB(Mdwz!59D_gm|8kOSE*H^7f_1B&YzD6XGzJDY`59J>?ou3se%67##9TfHrjje?yZ zk;Je7qOleKiv;dHq_308W69`Y9zA3-d!x_0(Yf&o0;vK-s;g_-k6yE6Ak+=`+o#5- zp~hy%mE1ZYzJn*fYS#T?4SkcOdSlX05mrMe*_%u&c1Cy4CY5F?5gIprJe#%qg{CL= zdo;ck`F`Au*>AE96FYFSVfoO^ZqNs;;{7WG;Q61>Z8vFUfi9A7TQwtl5kl2aRnk7u z#9ot!H+$@1f@iiKd0fc7XPkmooX%ikO;K8Qty{W?rCLfr>Dn3)3yC z{#=U8-V4h5bK_KfuF{{~e<&mf(cgxNByTFz^NH7BCJA(-wh0ZC1d13LX3#h~g7_&e zQsMjWqJd=k#y_v>xi-qV6gZL;&%0z~+Z>H1COCU5SaD!HTU}#hPxDnDMWFeeBGpg# z_+{%muF$?~{S`3`D6(F4hTXbgqbvS^gMI@&Cfm;wpuCZmkJ_ih%+?{v|Da*zYqA*0 zDT=dJ;e@n5H`Og9=f=0=0ifWD3%x zx(Sb3j+zV->LZwoAy#)YmjD6Rm%Qxeu>O5?k)*eC!g%`P?br*Zp;2TRiN8zLR;9DF zk4~hG-uJIQV^+^BMj0yv{uOWa@)Z~|Sd3vQQ+QAlJ_`0a?nmF8??IdK+*O?Xh9(8H zq+t{0SQ93&&B;e#s=uKblR>4J8q>zoQzf#d55Ef@=!s8u6=%PO9=FnNcqjX$%(Gu} z6_>nb>30p!Dd0Nf-35az2PE9wc$XY^b}Sm>`;7u!@RK3vb>6z@W<-E{$v-VWKObW>0+A=QKsZAJ z)r*XRT*4h=UGSq`^Aine+(*8MI}m)m*qSxg@NOyJ|z zxnk8Ggz$SIvByp`iEVk|70hE~c}2EwRoU7jCbFvBbbNw|aRSS(Lt*ZdinAI1G=n5~ znl~u+ea=wn`=&d?1mC!I2@BtF^Y_NbdYnuq=;`YmZ8-h^SdC+tpALU?D|&Ip!Y6K_ z7NbM@aA7ne@Jp##;SCsTE1Jcd>{ZSl0Z+8JPgCnBcE^Y&%m|6D!>UXgoB1&_Hqrw4 z5lXU9-%JrPrtlANhWF#wV#~0n#+G%$YrxSaJGYHz?^ee}7#I7f{`UuOHSt##-@GDmW!Y)v8mb-ov!dx zktNi1{abDl#T>73I=FQrIgleT!6ZjyNOwE(~hR>>rCvG1aXY9itET-Yjp>022ev# zd@29A)Z@~l>Ekol#xa_iK*dPYK~h=*OR%FY%4V&?_14!tjFq=g6}Rjm@2I0}%Ys)D z5n-#TS#<_W_j!uz6dq9Q4Q&C*d!3utMxDZ5NdBcdc}cv+vT%{zkI|3&QqX6Q*HPI;w1|`!UbC>Q(@=603p>?S|@B|7}*f%lG|tIMIQ zX!H3cf0O6ODLwqIwBG|j`mF^%=O1&G@j<@el( znO~NK6U2X;t%A)T^AMZo>+$u^DDhX}2r*qthSPL4E8*eMxZgoukM~g4=jeh4?I8E7 z)40%KgBqB-A-<7aUJYeVFk{<|&p3h>1Ku2w@4gsrK)l zG}1p2TlTo0VYZnjB&Or(8Z<^w)0_MTon95mZTasO}R43RB!lfcx z*Kd(|ms)Y4WHLDb;95Xa?U@kn^*3kZ{{E;2cxJ90I+W0_#Z*LB_2DT*3G@e^0kd>u z9Xu!7cX)U^3i2TNEfDiXUEEVKl2KR%q7Px^n7ax%(nPi;T{brWLELCMrr4{*SGHrV z-oe>|d0r64W>~$0Otr(u(ckB`qsFf%F3!VIg`ZXYx-Aw1DGXN*$~hYGw$W;#5syD0v$y>JI2*62NB<-7 zZ|RsUeS?D|)j?K%tkT!!AZfUuu#rbq=?ps3t_~=+fOkH&Yo##A6by4bC^nHk8jZ$# zV&!5RXa}(DBP{<|FX<0JqxP}9IyP+wzHxXovCgJ=2f$3qCQTX|KjZcfMw88cWR=R4 zfGTa3r5!zMDRqRXkViC1wuQV-IdGIn2h~R$rqvh4W}7HnI-(kRnMttFsm%429JrJy zJL?oSr9tIi_1ISo9G@^6-L9@4f$WIF)(SU7n6_r|IDl5S#EVz3d{Eds6M<*QY)Y-{ z+yRqrM@1WXDlcx4RH)v~_RgNsH)Gg;J%EiLSu1r?3sxh3)v$YzhNEB-y&X_(fw<$% zi?9@wZNhaTVV*?#2V7k=O0L9H<$jS6s;ncSl)wT@(Vurh$jmMjK$Gcx^sN-b+!P4K z5rWZ29_sFg$&Y@Y!kswg+y&F$Op!RzNKW(xp78J>OmA2RCmG2%%f3K{45z5|vD{kd z-kTMqq`U5ZV^z0EavnH3NX6u>)jbgf?4ZtdABadvppIE4@FF~6(l!wey3dr`5*-If21u!xs z1$d}^ubi~`E3$={e^$_IoOtf9GTILZz8%z4XQtLjdtt5n%3SMsm2*FH?^_T(^uHjgg_Vy_50gByv zBj{+e)rn-7sgAa>73f+%p1oJi&to=BG75qV|j9>kn!&+~=~e@UvLNwE-AbxIJVA$NeI z^ElGmuxv9`4l}P7Ikj`o^ZgAqZj*L=wb-!1gq`!niY^SjqKgF>LOk;QFD#yx87*(* zt>O!SPI{z8LpOTNFRA%1CnElku=zKW0V#Qzn<=%j?#a`H-9+~+H@)v=-8dr*Wx`NI z`iycv`HSkFWtv!oJjRPtRXyLW>P6|YUZf$dw}>r{ctXUf#O2SEZynm)g;Vx>BHN;A zXcC6`7^QZN(dJ)_yg_5+!51S}2w|KullFz*ysl3oN*sg?oGn7O9`=sVIB7>Ov-OU%6oW+NqjLnp2ie{I6%Tbyxzne zz#6$%H)?_0aSMP);jkL9zFNj-j!sIBjv~>3z(sE#Y z`baYEv!&cK{XSx$Klq>b;VSp=xF6!5$4&rajeeiqiK!fW?2nf;soN#N!DY8FL9+5Y z(W>q|vv9A6pGMd=_>PUe`tJz>-blfzg9*tL%t)rNtlcWQC9>tR z3=Qs);O{;bJlq=tc8J2h4#UyOcnsJn^Cb)dYs4Th6)skpL11{iLt&}PX80n8V^M~& z4puQ2ElRvja|%D<^6$q8UiFUqY1|SpE(+t&U|ipo>O58x@}ScX%@TH+?nja5l0( zm|m;4K#$#Qy*Urje(o)y1e|j$j@syqz8Nlsn);?z7Q{Z|)q!(KWhY}~K0j#%GG|%a zM|H>ZAwlCGMf|@JGCQzyQnSD-H@ zpIEYZ6S|8fV6Hqenj$3J?LW@y%TX-SD=?e_-FT8Pq_Bg{9TSha zko=mF>@#iTn$tipbmkjpMT0rqviO9Hv&&h{eBZa4@F#>PbO+3!F~VA5DT9@;$g%_@ z8szTiGZLC`{0`x#@ionNx>c9BDsrn_cDwN6<3B(h-jG! z*4FK9LiEK=HZg(65DV|dojy7`8vf;AQVoGZcvd`kAa%^dKA2QWY*}i2kK2AqtBmKm zb73sAU;8Yc1^EIy)p#fezD<%vxTh;{$~wi!eBV{J z74PI4Bg;i%r+Fj0fpJ@Ot%hW)1Yzp6Q4{0)CL)hJ*>4>FXkPDrh2P9}?2_tS(sq}$ zQTAsOOTp(rMqB%?Ezw8JHZT+)}?SPIEt<*)bgwOH|l6hE=CHv&UxQAUu@8yp9s z&-LG5aSVt*!Ca%)W402@_3-1q4iA&B!^5F;I21-hhReCjyIYF~*KHpw!t2V(Xvp`s zk*!rKc>)I|NZ3?w738Q)KtCxug5Nr@@FYexF(rst=~{7GVj zChayiT@cD5zBhw=hQfcZ#)6LB3G@eXB;GWWp}lUQ%k&rFCf>Bf(^VmCCW>A-X2(+P znUODab$>sS0vr3OpsY6s=xF*2rYg&ce#>P_>V-sGhKEKA{R-?uAmG>VI*m)`Cc%`? zQ}6^(I^Hg~ z6O`}LOR-pF+-2l4WF#Z&fvUev0$rb-L2^4-M$9Ro@@A1En4FV^2L%Z?t_RIOb>QCmmjfpP^*CdS2 zc%L{i`kfHv)~};qG(ArhVq$DPMmvO;N>;N5d}3Z?Bzz+Ll_a1o2FCwZ(n2D#wDHD% z@xK}`U8CuPP5!GDPqoO#V#{pTtAg0p@E!EK4fKF1PJ!&{nQO>mXf zn5TrFb~HR2F5=Un_+zjThKqWiB+MrxJBjWb4Nt^7zHNyv6BRb;J@SrmeuTC4E>m=G zRvXFQ64YvO$rF?H!n1yiCkmWQc1tHe7o`&Hj)~nfc4Y zd+`z7GY0J{<^P7>*8GH?U&-qc7|z8{x{b^V;W~7@#mz6c?Z-5J(5;uQQS}_RW*~OP zAz|kYm%=b{wI2l8@%xAoZo^RQTsLTfUmz8F+G*J%~UYjLXwoHCxQ>YfO5fK#qj~WD zr(fo~+Yd3VRcbTRHlRmc{hg!|SYNpVb%7>la1lzhs4P5qCtf-2>%65DQ9|K6cb5(p zP@!ML{o+?pld#k4!Gj>A0GqZIiK0-U&spEUgDk<; z1-GM}^wFXIAs5(4zpumg8Scnpth1S38a^V4E2iNnR?9t=SXAxpxFzxTE+NT~ZO+K1 znFHcZ$le``ql!%rF*~g$qL+mq14YiM?^n_oyCM{=o8)glc-5de1%Q7{snqT(Vr~gbt>=ZgNR*t#X_2`tGEUx(SJ6dlS7jaK9-mIb>Ulf{a7Rs`SFBbUaE(RF zfR>;$m^hXB>%u!OTN325E{V86J6paScqBDLlMV^S1swpykNgQ`k}Tm9q^l!9%SeFf z2$LvKbjZI5G{y6gws{GeQerb$ANe=G?Zi)RP0 ze7~{`h&^m^dbd!Z_=Q09PU%9!Wwgn+viq8hiI;zu zux7+M)r?%r6cfo!GbTz1sEM)M7<+L~_pKO-nDT=^iLX4oe+2z~Z1)Hk1OYGX^^XXR6%4Haz*(|z5N_YrvGfWsg8a{?tu=p(P& zpUuFZ6!^If9=I&^!_SA;&N?s>;<&j#3tuY%?M`I{=kVu*%Cf#eV3hrMb_jpk;HU5; z{G^Wb;o~FA=t2BBh*w|mS@4CUJy*}-3*Vr}F%%YcIk$O#Dd zBOI?!YUZ01=-uQEQmGM>3<&5-gy`p=h$X+-fu($hrCf}cocc0doq0lw)oH#<(xR_@ z-I;9pyW*W0NQq|-w89(T;LrY6l|6L;ONWDJ5d5g%q=K6!DtqoAmV4OY|2!Ev`Mt9S zet+awp5n$FwjnK1k?IL-+j+F~5v*9|n^X|t)?f=}sld(83{er}MP)_MccXU9-Gru_ zo_tJ!X4`MuOGApYg{@deVt@XJ{81TXP4q4Rs+Vw#P7u~x(!z(Sr1hyqfj{)u>p58xnJpJ-V)yWMgP`P6r2KEb*1 z{Pz`orbW@GkFID_xVo-nkfQ%2D|!R4E4k??9?SL}P@~oh)fwbX5VA@Ud>K|VwK6(tm8myk zA_bs4kg*;+6{TgEqV^>W-Gm)m2IGp47oEbH*QFgj-E ztW_*!GN@BGu#`ef0+9JCVyn(*CREk!h`&|v6-$LDQg0w%R$q08_e*C0)LH{Wds3|X z(z^S{(_6><26lo-{_tZzD~)@pn3EAsegz}BlE2aA&h|fA;b{B>92u( z{14Ngbzj^q7dbbGWvqHX!uKM^X}t;qEO0gn*uAj%oUpa=zV$^S zv9&d39#0sG#<(&xROkD{M=n@ES}Iz_vho*;!B84<#cEV(x665$l|L3E=bVL`T*cMs zh)r~sp^SP&45we zh+rJNKuJ+z0he=!_jE*!A=p!&`9yQ6uX@)EZw}ImgM@-s@pgi6(kCasPGG7h&=gw- zyixwl)XPe8K|yr+j&{4+DPXDk27#|!5V$ab&9tmQV`91Kawe`RD=gw-30%xI(ucZy zRf#TiOhml)Z3Gc_ls;Twoe!lAEQgY#1K%MVXm8af6D~}xD^?p<8B4rwBW;NbSgVI` zclnM%6)8;e?i)r|CCp z&;T-lU;{p-#5$R7BY6q7U_+_^t)|kGUx#-CJq1&}-+<2>zeH5F^7UBYcuX>kA->Dk zn;*B%P89$o@S?b=8Ph;Y3uqN>-l5SvjkjVxA~&`zCzbGz;qa~1FVpKO@AhfL%)L-X z&xL)I|CgnTM+urND+sGz%)7;SitxEMn=_{cVfcJUx`CiV9iW&~jbDzY3H)P>Cv9>$ zFR=3UM10GFGcvi2`Yt7v{Et;+WBxn-=Kb9=YXW=pF42$_iUfpu+_W9^oPABgAv-Tt?Gy8HQy;?=t|N2~1F;~*u$zk(C+?})+*Gv~pZQH52X zUIq_!pTpg)@hAfQ;yXBh^!oPynkBQ@U#Fl@bba{0K!A&&=mxUVY503aW%if77*6rk z0sLta2EA7A3I*1m*_tn4b@QMmSk(hec)C1!J+|xNkV?=jx9^2hwq=3VMs6?now`7X zmIW9i*UWdZwVfi^_FEW3FRZ{69#J#}P=XTU3@ijg5pM|=Enm#)-RK56LD5T}*VRv9 zI>q#?4o^kg4yK#@51Av^?sT+QoW- zi9;6Ms>P~3QnOn+<#z7&o{3z>i7NKT;J#(fP}PC>O!&MiHURp?tB=13Gp0~6=tM(U zrh2>p-=V-%e4zy=x+0+&~Lg{$Kt@0lv~t6WLqpE>L~_m;xL$PR(R*N>mAJ4e9E=!>xN(MPHKP6Ww^&dn?jHimzvx25eJn--iQk)OF|->{(^5 zj{>t{CYqQqXDXsL2&spb7q|q~k;J-!8Ky2t-cXymOe(p_OoC8g{bp#-!jSAkk=c;D zVNqEyhcI^(-SVO2O}{{{x$tySa_`f+7ORC71iC2)YZkt)|NO+|PRGn`>3W}qgHGa$ z2HeN2SF~bu#=Ku589}gt!LG3tw}@?a>&WZKCZ+zxR257H)EN0s#9`-k0s_>`yr%Hl zKqQf3r0&!LM=Z#DZvWFws6ZS?hVz5#ha$rP&hzzk1LQ}{u=qokGVUQ@9zli8tdUuv zpA}TiVvpL;4)!dFgYfF54tU*p1yr|JYPykS?F2T`7@EaqZJmYJ&Vd^DNhd*rxs36N zW)uaiKJm@f6rrJ1dW>f*F5{UFk1^XO2O=OK9}xob5fG4%qoLsT>xKxE$N#dKW{pAO z!egJ?XdQTX#V}Y%KwCjX(LMHq z7TPTc&HmX749dhq;LCR+G4@^1bah~%2m564> z5Aiyj{${00MnWCp37V2AYgXPDJs)Q|H9a4O@;*m@1P=A&HK-YUPB~TVh@$&By%Ov# zt4R5xOXPP=g_~Mz7jOv#)t&;YT0j2arp6FxVFB=~B4u-j0H8?~*FgI!%G-PqehIYq zAHXeYx>0EL#iV&#fZkTN{6%z77En0^MPOmCu0=$ln2Pyc5vN#YS#4;$+H8T}!p$}W z_}!e$vOxWsxm#gHpd=}qK{eeAH9rUwznNuK$pUS;j?j^Jz{LwRC65A|fm=;hmURUF z9z(~Q18{K2s_4Ij5nVuUX!HmKTXXeGtmq6L;o;1`vL) zaOe33q@hDt5T5Ku)7fSdmy%ToC?pZB<`g&=IO$%uirIJj0;`yg>F}3T%#=iaSusD* z?PDdXUn%aDHr(9+vZ250Oa%6Zp%M&8C=;V6Pd*SUFTkcj-xk!g68!fKel+9UI21}t z=qc@@D6NCgm!uFVq2+TS&BG|gG%>htecokW zf+OIOC=R$qyboK^cv%E&#B8%C_vimNV3yk(Qea>x!{NeA_wW>)OE=*Dy9lavIUBrR zyG;Z;UMD*{S^2A%F}|2M!5gTeDXSA`3cf+c#68{@W{#^VM7lW>ZXyM0cWn(+Fc zMI+oe_+cbMn?Y8b*-R4;oxnB|R`q5ZV%-3(!$|7@8?}vP?U;v9*JSY6jK)|N+$$+8 zuZrcvxR?f%o4JjRYMMJ7!&fsX@}(#o9ToR?8Wo&-8w^&uc+0F+1f^3Hjb0zWPtQjv zc^lBT;9EB81Y0U21)saIlal9Sc6Bz`@i%WMU zuyRb6q3AEJQ=MIU(Wh?xXCJyj+5#T6m>KgE5hQ;Hrfj#@f=FPQJqlCd#f;bvteg;GX8#^pTHIp}KQde$%loef>ZT|}5pAnf3Ib2Z)( z*-u*IZV{B9{5VPjed6(1mUaGhz4`6Xxoc_51Wc3_kH$X(Y7u_dd!xf2DIGLmf1$?0 z(XW^+M|IwoNZ}y-S{+Cb@HNY$E+hLh6mD?+g|Ak|Q2_ej-z!U6Q0ZTSI{(rZRv07* zT?_|9ExaCGsAdMBMtS|}74&Y^oDC%~bUc*<^b<_OTxcolF*>7<8iv4c^Bz&gz6c^y zCgu+--s<6}U5M2YaDl`GWclgbStvnK&65*BJ*d=->?K%xsCn77@e8i5|4g&~GwYC+ zcrP`1NxaVWE@xxO-8e}5;K^ft*_jrOob5HDeMvEnZy|bHeW7oWo?}xvK zH^uw2%xkmQaDR9xm6c_P8)+azKNEm|^e&tS*bM6iHt}P6LX{G#WCRr5lciSF;#KUV_ksd4H6J(kZ_lPL zrreVOf1!dYtOC2)ReX_V<#|NQ?)yU?<{ zU)5=hCEED$!FW9UZnncfZCeQgey2a#lla1s-NLCQ;m*ULzNcAuW zN2?A{X04A5>G2090R-o7+R9lI*%(Kmt&9F<+Ss!P*t5q)E;!t>C^!l;xe%XC&0Oj7 z1z6;t!hK>}$Ep=kwq9Az%jq5sG@Ne z6hTh5U`Cy;PL=%HODz-_A2^qREM9+cyvjDFEkML^_~_6sLD(BE)8FB3M4K_dJ%f>Nj0>)G0>rer0Ct|cqLO9=EXgDfjzgMp29?v%@)HlUXlDV56Xc{gJdVD#(xOPw#JeS zm1l1ODG8&8I1JY-tYSLqdN-jWSX20N^nF zZIl9D7g7OS!ZUm;^9@$`6|@%7Hw%t|NPy$wZ5M6N?(u#|GT(|$&dl$B2HsixFLEYY9%5PUhf-jf!<32X z4)|)`!wIn9!9n4}DQxC;sOUpb(T94#yXrs>Dc^bq^^o4;2j*AzT_C@5Y%4Z`_C0zU zawt*(uZQ#YU2A{0?97K`gfoP9C9NsCXGA|^xx09`o9Ba~ehNmy>fW5(AV7A3+Aku5 zb2BEgE#scbfw}Nhw_@MbbV@766xf@D-`kNEjURK{w-Z$GDYtzWyeOQTyATr<(N9S& z?R#DjKliSY`9-y&(aqKO-MsWeH-B-N#`8mL#cwSM3A&nsGqH!*mln&sTE>(haUATHY`Kh_j0w| zQ5(G#<%3uVrqxQ?j(%~O+j)eQUry@5hd?X89RO9a|dK+6`3Cfm2E{EVWPj&So% zx#@UQa}w1J_cuj97};f>;)@{5Q@%otA9nFQ;W>LOz~9;yj>7B!zAlT2t1;(sMEeJ( z{DavNO7GrK1XC9G$cJ#(9S zDmEcU=6yq9t9d6-uxEMO;LlbB&O?7dSo8wE<|J5MjbuH(--<)^b(Z%f8aqkxtM{I9 zAkQU#Fd#F+PPU+v!Hbn8KbC!0q>@4)yw@)*#cqBn{L*eq0xcP(BM=X~8}8_ZaY7 zrlaUTtA7Q(rB+t>7nb`vus7;JvYRu4a#X}?5-sR6UTb@=i~UtJ)~6KuX#DwT{Q3T0 zXzjNU?8H`Yz=JNiHwq)I5A$szy_faS8+3)8-ZPO_w0H`6MuN4beJ^m@5HNqW#*O)O zg*!jaa}m%kv5M=uDD#WtXj@0F2OEY`8tU$EQ$q%VgEea1IT;ALFD@?0b(Se2+`QdiqLy{^T;M*1Z z0lv3=FVm_N?yW@GLF(|8u8|i0`X3^KG5>w|bXLFltr+7{ROcm?xi865qwEago|vj< zu0aE~qK{mo@GyP1T5dlC-)({KUR0%S)&33KY2Pa|Z5t8+zP1%v9D3Wts#Ml zDVn76-LkVmQ}kzcK57P}IN474t*=Jup1}6derp7j*mCDp%}F*?9<>a>!Z5-t~j5gJW7@ zN+Y^P${CNB>P|tzT-hpO<;9 z+`bPgRtr}`s?>lT##X#UD(KHvX}qYC96Vh7Yv9uw*~^-tqFLs6oM@!k5ud_YPiSe( zU9a-A6{-}5iz=c*>p(3RSXHFWX!3 zZu>sCUI*8p4+IXk}XHiY1w$CThH5Q zkz*l^pO=xfeZSxM^IekMFj$zxzZEr+nQ>vwwwban?V7{vyv?+0ySy z3tTzV@5&jt67F{;3|Edr$z7n*BrW<|^v2#cQP{%-p8Q|^ed1F2-ht_$GwZMI4>txz zE(jm%G!R5|b@XEiml6_6Z$y{#>Tjfu>WK38xJ>aHG5UTiuhQND+5&Ee~Km zAqHj5?T2@`OD;RrMFMSUJ=zhc7?~^47Sg+4xrXuyt?57z?7Vj`;-A8IJD?`Jkwve; zjgA>Fyo>HVxci^I2G6tK6{f#}i>5`wyWV#siIt;2h%0a;(PdbEt4S@cz==f77)LPp zZs}W1Y8M;m{q2z};de$F=-36Vw4Yt;4M#4JeK^&F8`bI;@bfcNb|0%h2l8LuWBDCd zgmZA$nJb8z3r`i*z#;+c($_IWWRj4100{j|iu^gb@n;RsfS2Hz=v{sF3s7S6_Pb)Z z5wo4}l)e*0uL7bM{s@SE*j|G~Z*DqO5nBk-a~LF4MG9-hdwGX1W|=b#icmrPD53Q@ z?w5en21>7m#{W?~V9HCNFTL;uIt-+|{6z=0`-LsI5isNBEgeKrxKZ*2hL+AK+0sS+ z5pX5_3%roN1Ne}^#njoaDj{TL=FD<4aamd6r7$Nz^WokyC3n!l-+=8gk>_uDf6 z2!zc1EtGQ*Sik1n>pdMIu=c!R-EBlxGQp>+JK#A&~kt z2qz!aXGoaF{dnsm^em!Hq^c;rd$1JBi29$RlmHr_!drKVA{Iaq!T^=x37I~z`G`a_ zyqg|RvF<-j(m!HM;SeYYqi@aybfI7c)_;%E*M@yVe`(K-bl)$qJW3;v&iFVBKQ|q+ z8f0TijPLZFL^q;!xHlwMVrZUfZ=rrV8y(Tlo*UbSK;Gr=;IXeealtv395 zHfj&cI@Oyg^DxUl*^?>rkjRvIm@;K<$8h#KHmVi>?h~aK{NOJ9{&YW@6}OquHA6oe zO7^ml!(UO$as!%g>zKZokFV_yMxfq&wE$r?%nf;SXB;>7%Kaq|FuO?5W~cpLW~Y5| zW~ZP@X8(Y~;p{(K%EtGa4K zVTQ7J6d>;|tHBpBJ*BI75B#~{Dh3@g;6Ud}+w51|hJ}=ASNmRvZ_K%NMkv z+3JXMmk85#@md$}aKRE+;)HLn@)&9BJVyE_uCgjZ9^f5u`L4#|!@%B&HRZ2%5eI1w zfPm_X)q0O>_FbAU)-~_fBLCjFPc*4UE32_lXl`1oCIz*!psjyA%7hx_)r(jN94w6= zU;+ejC4PY45?9Ov%d6(!rBnk@q1?`Z_v4zy2NT498YNLlNLc)Y#h{LIw}V&R{-W>gV^`h&rtj@NSKV$%6xM3Z z&}`(=swZGF`|@JwH$7M5w#a7#p3rT^Q>Q2kuiIZkGZdYLeCu!Hy#>Td;-rI6S@5wS4T$%|FfzLWlPVZ$9!&jY}K`ZTBaFh}; zuKi9$?fm(o5v8y2+|1}p*Q!8CK1-2UiYtYsJe9#xW{hU3f5hlpmio@S^k*dj5T@oV zh~7W2^-A=nfrREu(c{zM@2|t%pp|XK#lEi+H#6AZS-h`&K-ydiZ*uWA7e|?A5K5p2 z5$YPat?D=qtIeW-1*OAMOah*AJ#M6r3V8=H9dpef_S}?%E+Z$IWevOOoY*&d{MeP4 z_89^J#89UH+&p?Bs`lMqe-2}t_p<3A?L2oF0fEy0x>;l=opAvE9R?xjGCp?)oa|?# z4!ih8wsa=}_LpQz8gJJczD4IQ&9^7P80Vs$H5H>J18^fKBQ4`=S^RJ{@t)!3@M-#oVX22UID=qzAStEjH zwswQ!=Ut7yeXKkO^-lVzoA1)v_aJ#jR{rx(qf=95B$e<^wx-}{?JK@sWpC)e7vm|e z*Ld?3kXhpUDsF+6#slcuy5cXSb#x4#XGINTrpmX=Wyes=nT${mzN!|L@00`Ql4Jq! zClOSz6!*WSje{kKhE_T(|}ql`&`*PB6)%_L4^@n?}tk zMz&Dc?H4rZkmjrI>bE~N2pxtjYoKfK7Iy7vN1LzH!j}6bLW&3}#*8a4I%T8aN{^$y zqYWco5=^WRRCAbbIY9@F+ejIsCL(MW_2D~~TqE-x=$B+XEquoY+Kr6H=H-7wMa^;6 z#cO)vAqb)vf2%Ukl%I+Hbua$RBy;{RY^?e?Cf~wGT1T@P7g@o1ypfyiF&3qe#Xr;L zFVITRxA> z$vahN^@3X|+^tVMu>;sq4csYxkhU5!p;=V12T+HnkBGWmq()rGh}ZKwRyO^bEGHa+{ujpAj45BuN4Ch!?+cDHT56${Tr@WP>WT&oOLA&1>6}3?hB7Q z4q)(G$t0E)n$>Hs&c4G_jCm{4)}X0>jfHGI8`0XY7$tW|coVKz@JbNl!N}Bqz*|kx zV5?NVTeVlCm4A&QUC@e=p(g*4LXUnKBb1vy0rhO4)o;`n0~d^|?0+iX>f*ayxEocv zZs6o}@e}(Kaom6_fB7zV8?kKtx_a~!2@FYaK;&X$rNPKIhA zp(mSo6Pg_zFhXwW5B33)baIe z)iKL@>ExAu`8P#8YjxvoROjjS1aXlz7AiirUzNYcECgfKP>FVpD!ZjRtQ!>ndnjIP zSS~jC%}^+jkkx#BT5t+Vb92v!&LBn{yLo

    0P`5lq$WwQNq5dK%)+<`TA!R@{0fr zeutmfR1so-1+K_OUMBFTlOoyrWMkZ$(&xr2o&dYr+_amY(nlZ9sNlPms*hggLfiz;N?xo4J zuq_e!-b2hs8xy*^4t?Dh$2Z?z_E|tr_QC3xEH>dtj%f7oFzv7|UzN}k|Af5aYvLGR zA%DFEKb(a>&l4Yd#U@zTC4AyC@RBR6pp(L&fW3|<_WBZVR}dAE3s*M*b3MumPO<7X zJOk#s8zY{Hxo&3#b+Rvr%(V$gQGE=UYa?6!Yhtdgs`F^61IxPw(qYm8l4juRB2A!sFd6=!fGA)R+wX3v<_SN?LZrayVBap=u(uOe|U zZ&e2wU+KP0+`vS3LGfrZ85LIv`)pw_?zgi}!wuJ;#31V_Du?+_#F<+ zH^0zhIsa7j83 zU<$H;R$NPTLsv7)QWO3Wqk%PNeaXW-)LyShxIe9}p~na%awWzT7B|v`x6!G7C8dG! z`MWjePVd+K*Pd4ftUbI%xHz>Aa2p!ve}<6mDawlD=!3uFVw~>-Q+WhFC^CHd0v+f( z^cA+x*}}-&87&JxwH_9?f$Le8%Ijz`yR7J*{!y|B@6FYaYTxQA4#LWY*35p8W3;+e zAWB1J$rG?BBE~|7!byR`OpyE#_iPP+{;6Q7x@RkX0#)CSaZK=fnb)B;JQ)_0IWGQ9 z1oJz?>jGB{cNQJ~RdMQ49Nr8{TIjAwFcwN$F&gqM|DbT6O*F2QB@_dIo&xbFEgH%a zGc13n$JQsHd!(66OrIHKz4!+lA-{~?u$)8j1r#76=z%8TqVtWcecEJg|5kw+6wiR`!@r!Gc z5C;X5-Ry)n#D>Y-f^~-Zg+#6h368}EQ1}7W zxnE`O{hs0jxqA9unIBSFZtKLP?7vUYUtZZE^C%RfFzp*zI#6-;S3%i$R#N#PkF!}uJSz!n{pXm-5mYBv!3r^?hY7oE`1!7(x9;1kf*pd7IeZOZd8~X zguljI-BQnT>k-pIPg^`8DQ%_3cZs<5$OrKj_OU<6e78(9&$*k|Gk3MN#%4~*`cxt)iT>+W^E4fB(c|45KuAcV3kEPeih>_xNolUPPSqg zIVD%Soq>h-=IY5?pVUjX+VpWNc(r_gHlJPW?3gpN29qTuL<8pf@6pqCsQki2eZlW6 zP9RmM!Y0)5YQ?@&c4oiEDAk0E_r@pZ>dKA|=tNZ?w+vc3@7u`aSl|3K&JNzqCbY^t zAluL98fh?Hf*lxk(er+K&=m_Qpx54?t

    zrD{-y<@SBp*oh%%Y~ZTKMp9!v{yrcg z_8b2>`ncbDV4d;sDwnUS+rvXxt4QQijQXseXpH+I?`W@w3OKtlm{2j2N6WE*!fP8M zvVEt*Tif^3o!wH4Lc4~sow%DQGZ53aTr#s|+gr@4gzSO#y$;ZRe%=T&!uRT>S!{MI zhzqlirmQj3*vw+PZxs8g1%a!q1v_@=(=*tnps(f5k5jS&O{aN7Ad+nSvI8ba>*Xu4 zq?{3f<|Nsfw$8UV!5*?=WLH%`{YK-D@aZ-XV~clRiLun2WQ_pv7SkG#lkTXcQ<;7% z8cR}Ya?(X){f0r60?;RCBoZ8&?>oOp;klVLIk@$eP?IwvmHuR4Vb1zF*YQBi4@I&K z$=CDTk#eBfCJzb84PiNO#HO+h_3Ds1xvVutg-(ZJM%-e)4+S8>>*p}pEMpQDg2+j= z@WqkvzA^+mAaK;iHqdOG(*3oL`M_VoE7*aA3^ z*@nQ7(8Kh0P9~5_&&kkpGvV`&#`ZS&JkQDEAxx5bKIXSqD{Nx5!u@?n%FiqKN=~NS z6o%5X95FKOHJ!wFTJRkAs*1$RB;87cXb#NTh>0WWU^S8fXlZZM-J|HS!_# z@pjQm`=LZ>F**x|=G@JvV@5<9P%}ey9B}TOv(c^J$uGpJ`Mh)}A^o=Ke>__y+6PtQ z!KTwd6`6-%+S%)84~7b60;jguI~&-9Ak_9TtSei(0KO+UI<&;5=(!{GnIoa*>9Q|i zlX+Ft!GAF_aLhI&+7Jz#gw8uqb`tA4~MX2gAsOroN z$63`Gob!cSe+1aI91Fs@NuS6zjC?%$q2RkPY8u!ZeftL&mj)!Og5Ao4a?@!tWcSi9 zfMZ()@`LgigWj!E?7`V#7?FxS0OTp_6ive%?fN`uQx0?mHYyhy<%dSqMJM+4uf^^- z1|$7|3{5`nPouZiy*i6h(c@&yCJ;QN8cAlTJUeoVH^Br=V z-1G_1DGx>e9h%vHKAY4AK~*lhggOO#m6CO^O|8D_PK7t|^G#6&mOVv`Y{yv`zc@B8 z3>e(9y$V?OH+;Wh-w$K6!QKGV2|C+Z1$;HoXGF542nYn-3xiVs1rY+qmja#p7|6#8 zpEoja0_Nv;4QFA1fnamC>mag>0K!2ptC-s^FlRk=^DeXf zIc!7rdLm62r%fS?DP=kKW zxr9AC-KNj$=~!Pi%$FcevH|^hMD*to(VzQzTU3qR*)zfFnePJ{&HcKxVa_Ml=+Zy% z^K#QMeR_(Xn+Co91kIbAi8676uL5o@&pm-XjJ#@Y~tq)=6 z=VbQ8|Hg8%fQb?V1s)gYk-nS}Hh4wCSA3YemiG1m zl83KZ*kMh-SurL-Q90?$EDu5vXrJqquV6g`a>0AJCPf@qt^X#mn!@sDSSEBQ&}=w- zL%6E~iE=aTt!!<56h7Qw5{(C82m^Z|iXAX1W~cGlpI$K9;Yp-c*(E`&j|LEH*Z&={ zrUJ2U?&t6P{|UW7szP!-con&J@CI}Jv)2QmtP(Ulu^%}On&h}|*8eZ)(KCK3+7@Q< z^K3)O&=^j(B$zDr7Lp6t1_iEdNFIuk3|s4u4%3Qv;yDZ^+3xShdk>q`dm#S(f%EVG zq3&(KqpGgG@tkCkfP;HdgVNquV@vPNRBFjBtxTcHfpg@HoB^y!Np7Li8>PMV11cG{ zl|Xce=5#z5wJ3eBwA|ZzFTJgTS5a(5W|ELEz7m3v@EJae38Rn%OhQO9|Mgq@Oh}@( z?|a|>^M9Vdk7Ul-XMe7}_F8Ly{Z>mV;>+>Z^Z%pqH~TZj-;nvB)u38kwi^GARNK$7 znZ2z=wHHpjWHmRoLYQ_np0mHF_^r4VZB>5flBW#lBZ*S&rgoL2wHkgjeWpGA??cs> zLh+lHXIdSx9}eALqV`13rrL9RLIc9uvvQ`&4zX9cbr>+pCD}C9SG)ZEKzMY#TD4ZW zy_bkcsc2VJrP8SuiHS&C(QjQz_VlY8TgH&4neCz2>8f(igew!3@N5q?K3%un9-0T; zwEW9tV8!kl%AX4+M_W@>+vrh4J!Ih5to%CF2!tl>o1rT^)j!D^?OfhyhOd{p(yfsI zFLjLtH0w5+gNCtzHLPt z&rrB}Q;#;LYDVclR#5qq^pe`g(wr~)O;K&k9bu-yC-V4>w2zJV~=)IWK6Ykz}GF!jgneX0^5)5 zr|_&vB@t2ja~NuKGQ!K<${wcbwB@PGuQ9^c+CNpS<;1^Zv-gd;dvbVI791rkRp6I; z68BR0aq7l`bnF0$HDBg6hL+e3WDi|EXR~s@aQss0ZnTSmQ!Lcc)T8;m0$S zZdNV6gUk0B;j3v%vD{7bo6_lN*|o9AdpI7^cn&RyH1@=!%7L5OV_&(#AMsg(<6onB zZRiq>{|>R{X7pxx^z8Ld^zxW*;$@=n6X-Hl=2ZHBMb zY5u2KcW2BhovvW|IA+e|@Z(t}@xnW14D5-y%McLXYducm{nN8KW`w8dwn3T9$(UI> zWi+M6QH1IQ9>`n!th*<*;UY(P&g7EX!tV}_1tx1Sqixf`5IiFycf7`$l_gd`2XAI0 zJEz46r`UHXRN*RDYMgQ)hTjFsKAsVtv-U0iwHd)x-HyUuhUP;ct4p2e(7U4k7jEp@umc2^v(F*rDZU^?^ zE%N^9Hh6I(1{_>(cYLfE;ce3Wuf6#)uG#JR2V6aUSp{@(jX%_;m==FAVzxvjv<;%#bcC^8W0NqKawVUPRu+5Wgu;CjMPYD{^xHs0d+ zEDdWVn{nFpe(p0Hjl_8uGKAIB7lg-;TZ4DCJAOoQo#ZF)@=^Dr@~i`JIO*+uyp>O; z_>~lWKOCYhY!Yetr{S?VO|3+BxZN>^{^d)IorM5w`HQHkj8e|KO;(?1iNm{9 zv}JU5TzD4VHeYxixNVsxJg(b>r|>q9@R+w{3Qx{$lQlff(D1xi6OxKARnot`BCAyuc`FDG@8-ojr_;mdJq%Q2vfa*WnD_)835j?$MS^xMhgY?ogyq=@lNqNzFt)Qg`Q8I6lwR~wpgSUqBH3)bzIs5x6RiOoJoJVL(|;1i8S*zj}baw%&1-1 zf7N{GjPa`~X@Aj}-xVxt z`!giXQ2Yo9F;_st*(y)4KhDaod5&UAADaGxgEYjYy!qKw+xSVC`IDiieu1pH{JpRR->n*zAVV4q-;bTjCcF?S+`Frq5rX&kRLF@0Un?ZiCOc zxwgBDDpTLj_bT9Bdr3e%)P`(doxz?c8%%30yk5_>(+|+MZ-42mLOFcIj?5ZT0gaqOT+Rw>P+8Sq;oldDb7b(CVgyDXiP3WN-?S^c=Ldh zrYVW?KSlLiKN&H_12qu;WvqgiHUaJSG9{@n>~zX)Ew0~lZoNo$mU3KHaSl2H9t)XB z%LXJp%1x_e<7HGgX9q5rEbf;iA?@-&Gm5Lfe5E+t5TX3QQldN+`L)Xm-0)yUKk_|) zm|jkjLV`e;dg~#2+fAi>`%8NjfL}%fR*Z?ZBMCL`#;_9Isc3Knl1S{}Oj|%Gs`3tC z`BDayZ;Wy}xCBYW((}mbAi8XWY|YXSsKm>F(7Uyt{vN&@uuk}+CtP#vbYX9Df=Bu_WW-iG2fVBVGtVH?yz$YcL4~cv-Wd&1=5<@C2J&pn{b7m0VhpDNt zgD8C)cN)4?dI6`*MGq=38GaAI8JzBmvO&P5EbZYQ=#_lxuqd$#W%BD~ z^qYsDr`~#gi8$P9C>IQiomz3z_Q31Z9NT?wz`l3yhiFR$3iQOCdR3BzathEHpwdnz z9y%VHY^f|j1E2THeyZTN_RGvrQ*Z5|0(%0sxwjqB0jfxUFt{@l%`T_x_t6-ykz@bc zUuu9wBr&UE{Yzwy4tv*|;rq(`%0-qidND{@iMnp!A%9<4z$)iFBcS<1t@P>DvP3zp z4j+O{nmAl9X*KoMHo)OouWm(`l64TUUBVxVoFqTAt5PD0IYL97ddW2XZR%H#}|)oWax zDvTGqJ&~zXHT3r3DzuUbo+-!0Lox2lB!{gp(c1&(A;5d^u>oDam;O9ZC$l1X)+cEw zt!U7WMGeLK3tn56i2F}C&Kr>f8I3FLWzL|VA%)b^HWH3)#INUR@bD<7^4d|4Ei;_4 zSc~W$DL5YJkS$h$3@m#K{l4ne&CZY@F1dd<{o6(L+A^Ot=lZD$-oKzY;`J0O8%Y!r z9IKq?skhR{`%lY%hfiQoMr0H1mcBs>61|t6S(50O8P04V(f_pyXm=I`@R$=1oRxKy z>_yEz{1+Y=M_7`zCwPGUDmc8e^qg!83(7&ENO{CQq>rx@v#5Dxabhf-Rz`@8rLQbY z@X39}&PT59;Yua|KAd5No0N!$EoUBDmp6|idZdys`C?6jVOcS5yi?}QI8>k&H!^HNi7I( z&mXu_Kh}t{UQ*XKps8FZDz`@R_tLfXgE5v;3XJZlx0WZyi6goE);`paQDIU2OU}T$ zyo6P@I9$zTIaDmZlH-mwd>#ISaGUwxgE2^deRgk7wVjNOz7Az~QhP!G;uFn_4ijl((ZT#DCwdHGlV$GITWL;zcn>Qpwty8WqICLl1Zn!%e5711 zf!@WJWe+AfvbljNfZ4$PX9xAw+Ra10-jDg<{U2ZHHz}AYY+vHfVf1QjE#$IMP*Nkk zJ@b#)R8lV$E9h%PTU_usDit@|1NJid*a4t>YJ3@|Y&W8<)5Y3I!0I=ZOQ!FVcs`mM zEwx*C38(#?37D#NpP*(PtfMrPuPBT9?ex*6uU9-@o?2JB7TsVfJ@VvqkK;cZO_pVi zqOyv)T3f(>M)ce?IM^Fn7#Pd+*)W+XxiM|qe13{%(lFR5Yn3?1-YrnY95m-4*G3z8b``P6d2@&L^}6B zdR6g!wVJtCE8T|S9I!41eC;Y48@8cM3>?oW*C?Od*8;ws^lbVX>3dYoY|u&>cFFG} zGxq|2srOq|sC|8D35HC-cM6q37EH3uTodU^m-7nv-bZw*B)G<$w!VEJL9T=Jc}C^| zq6`EVHT%c1y;msKF59(?+jRkTRx|6hQnrwZV5I@B=nge=2W5v$W9z)F>Gq zp>PlP%=$zpP%|<)v-QyEe&1oHOK!2>cVIA${;JbTseOljDrxO^8X7fxYuO>YRgX?- zC=LA9X)J%MMiPxE6!;zYXhkOb4?vHiMsvw6ah7*fJ)PMoPH|w z%p=4+^i-v1R%xZoT4tDMYbIcj5PnQ64gQxIE-D=wGx!ICxy$+fN3xsI2Sy?hq7<9V z!dhlisa{5;Zqz9^0t?3Qb@;74XaWtCW${7YVb1k+%O0SxAIB4834ThXd7e>ZsLZvweZtA78#68@fJ@c#@Y}9;w>J@ zV_EyB*3dT$$gvD0DBcRbHe>7-jJz4CduF6o(bqEm%5WENm0(zmEy1|qlJ+l*9W_PR=zhO?cO~I1@){936(8x|B@o7@2>V-F-W=HkBGt zHdZgz#xm2WsS!Q3o|^Du?jW#-Hnyl#O|90(u19z4sh9QCiowaK%4T}ZF@R#_fF6;L zQ1#22=ECwlPOnFo$loUVx8AXW?Zxt!OqS&xw=WU%qa_hw;ijHaQ!BL6^&k^gyhYad ze|a6)DwI7}iSGi{2``Ur8qCm-ep|cB<+$yppyK{3L5ro3gFL`<$6dY3C5o6)@4zCSuCYt4ZB`yf+7!CP5x-hRROCR zZcygg>3-8%i|XK!@0?g`!IJ(^Ej=>pnznm}DOENeaT|tSS(=Ax&;qs~!fY}`Ta&*M zXCF8%qp&L4pC}kk*QIyQ5{$kfp3J4AU!K|1dZH^+8; zAH5@uo9ul=bE#mhZ2+K&-E(FAnb+(9H7REdhEf6pgnlN=pUu41Q4-(!2VfAPZQwUWtd$OY!aVPoYfrouH|w9Tm}gJCF5tTe1wwg(`yc)e$yCIh zk$Ox^?U<3;F?KD?#?<<;ykIt#jli)b9u}(`mEgFp5{y1Ytlpa}{btF%Mk)JfLN84* zUiUPu6GW~W>Ls>Zz2$zfsY++?ROfyM38X1}TYW{Ni{%!4hk3MmzWO5AQ;cA!CR{88 zpeI~YE^MM-cuMa;`iTvkq=A#n<0SKRk<|x3x|aqMX<}gyLQcX*?g_r)evs20gYGjWqdC0n& z@3uC&98i8-TPdd$xR%ep=UR~JOyVmX+-JRyZw=DZMczUB)FN+4=T~B%559vdK%vNx zFQCd!eA$UFH6p7LEr@Vs>Ye&UxhB+Y*wTX|CD2LbExs%sy7->+KKo>b-#t)Gv?BEUprf1XXGfS)fe?*QmiI zJw8*7CLLrrE((3%pvzQF_~YfK(r)_piIN~yu#NY-!z{S`N(Io9l&D{+_A5IWD~Q;Z zxSD0M#fujdw^fQShRh1Dbwsh0X?iIQg?5hp%8Y(FsbE1FETc!c;<}}b$H;xf*;ff(KfX-5{tt-`bL zRu69oo}w5#kLr|g3a&g&@es}0Mb(8;N?mbOWbMaY)cg(=eg)8JV23=CZ`(F=aT%1A ziL6eEsuPq|^HGev^mNASODC}{m#VM}nv>q=Aquh0F&pnw`rul=Sbogto6LLBQ_?+4 zX5n{=yhu7FV=@g{D!!M>ZWQ^)XdsAu?EZE->%~-we}4|l z6#X6+3s1rGIUJkg$bzg>Hn!ihJ?uWJs=So`9Aer`$zfwF$3->pBuXqK+5oV>$b^k>f}|+_Od7PZN)N- zQfD=3BK?RMUrD1043~zU+5v$<+yFTPnEbpGGOV0xaEvP1OraW=oYz=BR*Ho?D2ttxbhpT=QAK*KD#k|>nT@%8@cGA}7ZuJk7J6qJj;h#AQQJw_ zES9hDv#_j^RNEu+>YQU1XEYUGdBN!c^Kh!{(^(L5{Ksnv8gvuP;JU%0D#H{8pzv@;+zwVI6mf2j4s4f8kbE$hW#wv0yJ>=xk;WeG^HEf9^E>JBtGj zZHmbMkY08&j>&FL_$ULcz@OHF;|zt;#0e*na{k;$^sf^MFDQ(B^SL8HFM@Nm1|CQ| zFNo`!J5kIb_VAW}mY7i$*FHdnOWUD|D{+Wjzyc-*j)-xaIxpuC?%=1%A5Ts*Lt=7> zEI+-Ok2g_3dedNxg)#;=EzjM|zzfZOWe?vtKO{;3`VF`trPr_Y_?1h3Uxg^yO@VEx zhxjVGSQBsNPMKv-SvAX^emiVa?$`NK0|f`og1u%z6?cX0^2=`dAcZ&rT{4&K*SWTP z=Uh9zQMWGRUOM$d?5kz|c$J}Cc1on8t&4tNm%~kFn2&sk)lWA=%bn3n;w$&+*~Fy# z_3V$X;K6EV``ESCSzxIw{5HT}JrlppF3$3CK#S;>zn=(DC;iy4V)x{uL?-rhoC>OhT~Q?=j`GjL@FEK$ z|APy#I4I*4u|D7lbzGPtJEoH!o;jH7(Ys#KSTuElZ=m5s+cD60%O(tu0whIpX zq@Q8H4lN&r1ED`vpJnp_zDo{ZoboY`|q@;?BOf28kgr*xx6YTL7hJyF@2HXUkvL&+tJ5H z0Nyy~l??!+4TNq_5^GLCiqt0R?+@QG*?=1hBb>g+u+l34qF->8;ft;~4Z6IM1Rpo4 z?ECs@KA0amF`5CD4WTyxN5R{`aGIw`xB}nFW0E!Q$!Sa%RduC!m`FU`IK{yyX>{Ra zmD%W4c#2QnWvHRDQX^sG>yxQrXSs(2v^$ak^6Ld)E# z^4Z?v%K%+KqQA=bgWraD<)X%;!!vq#lupcXklx}{=%WU% zYTOv@x$%iweDyXoT++QY@mdvN_GXPzuGy|T)i~}XKGB01Vr{vqL{+6$&5ml;HJViw zOD9q4QRC&=T_q7!_&b#Ynzc{qdlJ?u)-tR5p&nfm>acfs+W*j62C|6)U{ zRUYaTeDxSN5>$pHR=tr>j|h&-j;LJ`CHT{}CnzxYZd~6EofycD#I9XZrYZH_z0q^Y zXiX?OP^j4%cTf(e)IXuOiC^eH-{nWIQmszha8$~z3k|WE_N2S*JL8Ri(Y(j`ruaJ? zt^d*2ttP82_J{8;LV<kQTquY~V0t&5t~+;GV( zh?;i#i>9?`s~W0EQKOfW^^(ItxH7EV4HUdtta*-C5~sgtgsS^Y`=LJ5I%L{;eKZ1x z;C;+u2U0WHgA_Yn_WH?PurqkzvFajT0Geth|s}?d+iXUi0sX#v_6P^B5 z_xM9Qh&d5f(L*iXK)UEDGJ>8q!;3a+*74Xi2$*N5KM9z}Y(|hH5vICy>o0KG;X7Y6 zhiIdDo4uDpC&<8Ew6$ki z)5TLj=gO}qt3JJp-L$?Rd=J<+rq$K@@G_S?+z-H|uwEIj2?so4Q`A@o-#kjKQBXU_ zPTxcZb+TdYF}jZd;ZFJ^9biet^};C-69RixK4f)&VViyBS`}t}NHQ$r7k0 z)Z$2>XZXEV%AjHQF|FQ)UVzLx&{(E&RwrhS_K;Kp^w*Q}EoIo7YuMYUVQ*IrdmHV% zsnIw|rRur%q$eSiEHVw#rM_y2`wR94><8?mculZ_R7pdRVd*uLAX&3=_p1%P{(|y= zJ@F}j0nOA`RHxD`M66kdKFjqO;n|ZhWaG7BQyp~}38yv6K57n?PSYI;^QyD~0LpLe z^LHQm#C-Hlu{|S?`cdI2=z8$ibu4Xu#a^G$qwtf`#?Z4^T9uG>@SmObz!9DXrUNC` zP!5<%L%`P*e3fQOt>YhZkt$xGH1q-nwH&=#px5~A>8q)=*1qhnVg79y^wXRuXOml! zOxtx0Pi#w)MzR%GqFk~TJC~ac_tOh75lWjhkyvbApWkZtch^(v6$gvSuS7=XcJ@k> z(w6pMG)k8j`HhLTABnYUaRTwq_gpDFjE+!n~gEIBRXDeMBpYe-0Mjlr*NN@M~5(w#-(26J8_f{Ra7d`!|8fOz-0u{qe?7*JES4gK62s zA)|iyCs&d|S~1xgTfz6}01CY?*b(a(ali424>z!=CTr)y&^u;LY6I?0>efYg)DX`+ z%}~fW6D70?g^d+Y0^-`V=eS7M{Fam){Z4NQ_%7iri9D%MuCH#O*&Ym{1EDjcrIQy^ z=m-2(L%`cWyh@tsb3}iM_W#Ql`uTnp=34br;F`F8qNr*p<#!k6-Sw&T_uhK4^LLf< z(*#Vc>!;G+!ul+cby-yPmCCC?RXa+tJPJpsRUgw&Cqu>_7y0dS&8&%@;2iY@$~dj? zg>)#?0CmfgFg3iLeyfAA%DxIf)OO5dwRXtDahIP;_)f`lwL4B!TyL26T!omZumIjr znx5*9kMy5o)?oIZXVyrdG?l1~AH`-3X<+KMW{nqb z^U5jnHz>~s-bm;5t|JJiXgjDs-*9OnO?xAqjUykmtYYv?JjMx>EBY8P#$Z2p8CT5tFfgWHKUYm%w1L?L}M z{s!K}`p>90LhSx0f80;xgC}<@UdMMkuZ!Nr>kWmgrDq;D{pyg38aCQAfLiHL)*2W~ z$9fJ{=Xrk*Pff%lsmreu3qB%qXh8(NViz113%c4i%KOKRm#7KveH(NfEug1Y=JR5# z^t(-g5d`u!v<}QrddV$L97$MIrUSs4%&}oUWl`Eboy$5+KV5J_GfaQWb@_hbnRG4j z%e5X{cX59-nlk(71qsdU{y3GdwSA1hE($y%$mTt4|hI-1GqRT&4h zpV3Y0brJ?dpl-g^mw#F0^-}9z;9^dX#`Xbr9B|NrI6&kb)cGsQ>!b4WPh$51`w|Q@ zdXm1zU!dU2MASa|jnU^1i7BUFi@S8vA+Cn~uB=n^(}nYRC7%ZoeJ%9Pbj0HJGWnZZ zOn(-Y(VzP^6t1EcQ@38%BC?v6-~u1E$J*mdKEdDa5Y=bDv#i*FX3_IydhSIl^Mj{0`5m=L>TsK2sWuAFao z(PWfx4K)CDxfyVOp8+S{A>qUisMdKkv{zAmdyBq?v{#Kxdj;M0#85Et{h{5-zTJZF zMxgx(B=P2rj3)X>QkRbs)Lb_Nl?g2*)Gso39- z;IA~-lZ<>@;gUG<5wZH0-0(7Pg$|~#CnHm>6=xnhTT&154&lv+Aa@G?zdVE(M!o@!3hH%nbThx#1!`) zG^{?FNPITdczHvwm0L;Xy5IU>XcWKqplS61KcJ9iFORyiyJ8D8>kOGmiS^C{WMmuO z1KImjKF|U4`F-NkXemE|({tXR%qyN=*Gs}{zT}Zhl?Q@*K8Y`?eazkVHbcT+41QP= zS^2nCg;irX)Lvw@(=;kka6Qgyd+P`7U6{KEQ0Rfd;tz1~2NK1wgm65 zt&rg3v7*82=?H#lFdF-th5zIn_)nIYkMz6GVJ;K?lsU9c`c+OKJsJ$+To;UBQQvW( ze8qoq0?5*F7#z#4g^C^p1nG8|QWWVc|G9#F?Xep>c*}-+lH-rFBEF%UezSR%VZldY z0coI%X869XvH2Q~HDqDsqr5{AIizKB+r{@TLY-hchveWLjFYRQd|m4Ag;8weL?|ha z!Q2z^$0N8SPvfxgU$CL?dRh~=L?78MPx~MdMcs>s8f>y(kQ?&1)IG8uH_tho>T`jW z;4OQIXT|J;sJ|*^<6n<>>U*ruv+ALRvs&Bc;{H@E)-#j{X0owyTa)F8sX?pMhCZ?t z;Fm2NthCxesC0>OzE!iTLm#9#`!oT2uA5AMbl(F+o`7}1v`)q*QaKmIJ#stgCBLsch_{7QP%{8di^UiaShER*j2RiCpOM-yijO734dPBnK)q0|KY?YLMy z8wZc=iT`RiSihzc%kCQVznu;+}><+fIrN?>CbLHO=*KV~sLMKvC!R4O^ zJ!*bN?6NQRV?4i0&uxHplqmS@r;$GonUB&;6s>yklN^t#zj%e^Aq!p{!t(&{J`j8t zE`u`scKVCiT|9pN`wG8PZ99UoP*G))STjpvEmOob?Amk7&zR%0;XHEu#m6M8yHU9= zw&%KVrXn_HxIAEU1W*zGjpH!xw?2cnagght8sI^SEFAlY8ou_zdw4taZE?$`N8oD| z<_oN(J>coR8a5&anR}hc!b-w<93ld5i(Ae;l19@yHf%1EP2BbdR_x!$E|S zTJf7mIJXjOn&Rd1%<|%1CHP&7F_^Wdc{A4iG?V&i7nbN4tpk((uRQ#7cAeQIQCMy{ z^5_>Cxm|7vkISL2H1=0e?bI(V5~-+-NE!Ho7SRY@yr`M z<&;ZRy46afe*kFXp`PR*``uWgTg_y4P~h!z?ew>C$T(m%`n@|bbhG8k&Xaw7nCnTo z%yNnkEWjaB2m`d4x)7%pnC16V#QgLSW;~7lP$ZeY)dhUf;8ruds95c)PzwK^FOyge zes3-Ec0b7wF1$+9$Y#V7NZf!`w}wmDI-sYm{InH|IM>6+=se&=8wGV15opTW@UkPl zXj5Sqq#LQjs2Il58ab90HZv6QJv(_z{ApeyuIeK`H+|*7ohYok8HMp;>(iVMmca7H zy&D8)-Bn8sA=C8-9dH&}_y{jUV1<3FX1o1>eRRV#6OiVkV5vdmWB#iWx5b0+8M`HC6jqsHUjd{Ry+zxMS`vt0GjH><{&L#A-MboN2p8&$8V~0EC>TZxzqLi<}BS9n|MHVq1VLtFhDc zjbdIjHgP20zCG$!hx_9*!>(ensajJQR?_Oi)X*rX0b7Y@&M^BGMUo1yGks?s{(@>% z;0(6febFk{tftu2Iy-ItC>G3SY<#2p5LvX?Q%>@eYFO}*8Rokp6ovJ{Tt1q28~!Z3 zu1}t>h5v;wB0TRHpJbqKQ?7%Y>PFZFu-Wu4f>SQ}Lp!MPRjJfN=S^QM`0605P1US@ z-G{oH$N)6M!BR+AxJ@9n4#s;8rt75t;+0AbeuJ^ovyrnz5#3t&!Qc4{pu#=VpGL^^ zXHk9+RyT1%4a=L*!kSWVMZ24YL3+F9|+-@NK73Kch! z{9RBBlo{PuSM+6GEx4+Xdz#sG+$+@mu^WckZ;0|j(21hLWKhAzm^JJ0lBysZJJyI=s-LN`!P8sYVbB%WDG3}VlUQwLM` zk^P6xO<68*o;6TL<8@tIF{}GytORdQgwC<2KWBuhX_}HQJJ;&*!*m@nAJ$$1!cxvl z46Cn|$q5L&ZD8NR^T10CuKxg>ZD4uF<9>-%J!!%`EJ*K8sL3rb4ZlSLey!wF>P+^d zJtfWX)|pD3;}1>xu*i4)0*cr<9&nbLjRvnsHwtzRZK9jh{9{;RS)mMR-)FVU$^Wz4 zvxCA5WqS$-+cSyVGYRd%iS%>-7wu`i(w;6k6f2X^bIORGb5XM%l#_jG%oJ)&)nH@b zPS1Priv0qUgxm8XmMOSx`DhyslMBy^7jal^#Y>6COu>r!l$UtXg2o8i;`cQM-$7fZ z`LPXRPkCN$X?4nG_?3?A+7a^+E773##Q6=JoOBK{iTv@ueOP+JmO}lO@<+cwZ|CUC z*DU#E57hZ?Qs>=GfQ_dSQ7&$&SbjCYibzUVkbyDWTjC^m?LKY0@@Sgls}b{RaJ&*C z8DgVyq}sp!t}Ff-M&Q9sfQtZYx8qdVT8jg0s?{CV^T?1iw6LB<1*bdKKyH1gXhu>Y z7DcZ5Z^c?`Qsme-@fIqYpA@_)%*i!xuPQdx+fzPwL40)&CJ(8;+^OO><+0t>B}f9g zg>ya7V21c=ojg7GRX71}ASn}V8RJ0bQ_HbFy4KD2P_|9rqnEQ&{RPLN?zcM*zI(8h zZj~Ee63TZg0Xq)|)Ny&lblgo*ZrKsjdR5+xeTBWLd|^;O#3KRaVnF#AcYs%8EeOjc zZC&L`Fr6lqv6&J@)4Jro3{GS1sl>DBVFhuB$q18AwC_1OL%_s<-fW^3}qD_5O@uckzP zdr_vIeR^8ZBVH_FD8$^wa2|ln3OG}l5oL;VOZIe3;c7yW7TjjybJQLkGyQ;Dqy^p; zUVSrFJ0o%5J3I?+wl@e*3VkUf4#Qb22_k~+WUPKTF4&-J$t*Oi4-9X$0Uy#Q3~$uA z0dH8lh$(vbLP1w+`dpgw+a~LXx7MQ0T4n>dQmif4OZQV?R2O=sS+Hnwb|heZXxQJ( z-rpZri+dC!ELqE9qz2QHmq|+?ib|Qq{r6YoU>iCV`Ko3ONG7+)$l|^?!>^(a0jm|> zafm`Cpo~ToQ@{HYMZH@K2eYs}hRYEkP0oB=qoAkxtr2*`4OPxqeYkLjX?0L1pdS8$ z@1+_A&BSGmOaSLfw{@;{asa2>=ViK)WNcV^VEHgST*3&(0WXJUraTgBX4@-A)oRe5{ghUHE2 z-Yea+Yu>YlJyS?K{*2#SWm*^cSl*TSL@LkNB8gA_RASG_T5}Mx{~_3?XEV4cj1% zWq6!BMj|aK%(wZhnM5nS*tw2jSSn($>-jWrSd-*aBP_{q-yC#hF3GYZBRbj$Zvj8G z&T?WTKARRkfrVPL=o6J?P&0Yx#l9Q-3h9GM+gMPBi!xig?sO$-N)K8*jr&4 zN0|@zCob7T6_-NT$h`8C68Xs~e_HC_m`q0R{DeQMCqCRL*L3v_0l+KS%z%wnkWBre zby~P+230RBtb2wX1~aO4EW;#)A}N{`_@m~lS(Qe;`zLs&X^q{9#ujDiCGo-u*hZ3E zh=q&&Kkko{2q}*JgHfPQHSBTsNP*{i0-<>S>EOGZscu6tzD1jb{Z008KNzf*K{qx zQyF4XxvEraR!_9&M)f9oD5@0O=0S0$%$pF_{|LV<1KPCvHW&UQ)eGIAP{#~}E?vLs zCi=zc{ZaA+X0S4JNvv|0bp349dsva7fa0Qxq{6XQNA%K-H|;@$9CN7B$EX%D{Jdg~nCa#K6;4oxPhAgk(6PF};R zwQRO4sSS5rs`X0FFG~4=gQZU)q3!sj3vZ17p+e&2JBF_~ePX}F%H8EVAnc#89lF!X=yGDP(R{ZGD^cj`+WW8^2-mHqrEiUmFQe&yJr9aNy)Up!tp1YpwO+4= z)1NNcx3Yz*s^AE1OSH!`XjZ1_m9RkKHI`lzo}G$3{IXL{xGV8>bYk5AyRM%4smFyr**_%5{nlPB{9qPUI+GZjX38Gw zvGo<0vAyFM;k$|cGz!duW16p3yc|t!wsIpz!6A8|wTQ(3qOGEYf$j*$>@RVXUK0!V;Ib#r1?LUff|? zG`P<(3%5?!nCj!o3i`=vtw)_D!2=s#A2;i(?tgPg?C=6<#Q(K~s0xq78*%Q1X)8AE zB+{D736@dMI)3xg=t-4Ja0_c>@1?od#4B(a={|Yh%UdrAl}pCUq76oP(N=%F((gM` zGzMg`1!4s%rv)P7<#L3qk7hO$Us6O#kDRA0DI+5Mis*4r(&K;8r(Hwo@$3IXdJw5w zy|W}qivAhfBB%cMsIvOL{}-ro&q%6Bid4{a$f3x%_+MkjPvOTr)7nWw!pf~wt&7B2 z#iaTte5y5}(-i(jZcrLd&v-@wc#jN&4J7g_S4?hu6OzInck_>~*bE!+{x8lur$by} zTf9?IL)x{!n#`yBYrhH@-uojoE5JfG3>!~)R;umVk;$F)E?Kf0#DY=VACx7kp|})g znlO7}pC4f#a2cz=qOhyf^sSESuQaGYrIuP$3dI@6-dGuRbX%Cd55*(aA)QLaTT@b_ zXGq@!%B1SK%CL^Np3~U1AonR5r=;j1&ore4t}#91RuM(8*Z?)0ABbQ9G3V79xtr;9 z?o)E1BwydaqHiW?_*ZNE@f`u*d9nI-sJ3mhGDGb*rp%fU4!rA)lA;tg{Kt~9wBjGB z4m8o4P}3i0w~wvY%%q7&wBb^yMSK~ zRT`?y8>(<_o-s_}e8OKU7PcLo)64^8Pr$17TYaCZa$@N;1E|L~t(9Bv%O(BtN>W0< zOnI7=aErmdZgVkbEyJG3Nc}-Gpw_Lo%K_-V2OC_8-ry`=zh*{O#}%m`9VYcXqw0RZ zEA#w(_6Jh>`wM8c7AW}056u+LC0(88ev+8g{iGE0!DrfL%Tbl}DWmFB-Ulzf8ivgU!(S|m-70L^y&E_?Yq;?*!WrP`}gn2_I++d z`$!zW_Sx;*y71H6N6ia?2|`ZmUUFKOXc;a|`ud%A&HikuJ90kP%2KsxgPNH95HCbY z!db!nG)Al{D|f3|&}&+~hPRybj^W+wtkmJ<=I^Y3f#x6|`GL8%TwulnsVl z<(O+v+%UMFsQtQ+i&;#?poj^HBCwvwXA{!2mwCI{^=poJD@XK|ykdDRt_ z&OU60xA6oiaUSTOTp13O6+LSishGRBuBE}@Oxj787K_hCTAxIaGf%&+#yJ-%vN&by zeH>peUg88S5#uAR^C@r%=I={HdJR2{`b_~QkokCSSmNX*-~TB}|LPErNMPQO1VQmd zqJYQmJ1$BV$fkmA{Uj@iV!n0QbcZTi$MfN@;yo~|jZcGws=5y^-x##2o zse{3Dl%7X~uV|xK{T&8x*c8E(h2*sDdUAw3u6C6sB`uEfndH5}jy5+c@2anisvHaF7@wZYp#M3_kvXT=(j?rXh!R{S017#cuX#BpcC4Drwf*9J%^B->}ZWE+YEbiR*WUcMqe{z;HanFIyzU)kVK`+V9cG1ar@18h-6kV&15!MMY~q&M0ZBA3jZuq_co}Z=w(f8+N0V(PYD#n zjDpTt;%)a*%4wbHJ!galT%;2sjm5RfN^eZS3buE?xWC$4$;HmBm|$Jr_)JGH1SaN z9DCu?P_RX zT7dh>>=%GixaQyqK0Bl~hHCTh#T5KK z?Lva@oV=imRTH0;;Cq@l{{j8sxKn2!m#H(5xD)v3oyR!YRrLM{99$fRca>hCBCVRZ z2C|<%57ha6HFA8KK3=pItN#P`vDi_Q0xV9NaBLt=7;#3cW_5KRP!7-t>F`^JF#WN5 zhmVh0V*83mlg0L0zJgdY%g&ggg%{1&tmej3#Ie!K@QbdBd52W1TNj&pa2@soH5$K) zV!%0Bs7HCnSskgHWo5jndiTz@roZl#Z(Z`ot8OOc;|s0}{?ZHwF-(iYMY~9*Y<3tb z?4RN+@o9E`Tpk3$;fvT!*gsY6d2v$lC2>s{&AKppbgM@v8TITRNIT88r%zBD;=az6 zO{BUaPKHMKE;0f9Q#ETqYzoqBqHCsjJL6%~c4hnHHF|b!c2_JT(SCU)N1=M|3HZ*5 zH8?6Jw(p!Nwl92mz}q}ayfu9?4KIU8*ef<&3J2yleh`f(m&8{JaM;Y06|eV|#8)L} z$LVo%UrAT+hFQ~AyxCWDU3S;Bq78suNLuk5in_tt8+(cDExGbGGi<)z=i9rY*xK9n zv-lAFC#StcBGq?E6Z4u$enO&D>%OK0{XjgiCXS^>lFXfA&A;HhX$!z{Qwij$U+FM0_X=;xV+}XuwSj)wt%;T0)9fqNU{Ss2$`;LrcR1|WBC4}v%!$@ zoFTUpHOuuJO+ZYV^F|HjfLOhQV{s;8yK6qPYJ^xd956IAizkB_Y1GUn^v)?w;+z(J zg_^?fByUZ*jv`ipTcbV6u?Zu~=k21f>$&LV8G=ED){t4kV94peWGsjonfM6JNacV;93Tn}e7a*NGy(T3Rd;P;ZaF!l(4RlGdZdzDx{hI-bXqBIO>({eYj zJef_zZ3kM%%~$XRnV)8b7e3L)Bh@@4@eY*Z#OwiI>&ow#5<+oyIOoYeBE$h)4JSEK zc1KE1s827c4gPudw4Bv_MQ>8Zy02HWE>kYevG>^CB8|68^Hq}q6Z7_nxw2le%7MwE zM4FTxWDaKUi(QNJF;=0~XT)n&rOWiSt(*r)Vhr-aHGN`JdvpM6!euFi&-SU)3ZLmC z#TMZqqWK0^ka~=3spZ&;PKNVAHk-pMp6#P=8?AxZ@8DS3o=KC3h_7C(`A;k$*!N&H zb8{$~EJcV}K(>UwEWw)v<%T@7XIiaBcN4L1g!taHZz$Q?AGa2j#=I%H@2aF#l`2)) zGt+WE>!+&6>qxbl)7V_1=wc1F&S-FyS(h|#M*@d3{8*;Hpj>R9GW!nV%cUw&!JF{q zj1xAhRjnyeI29%Pgye~9o;OMXCAGv_wO(vrOr9A|f0n)usLDZA**h!jddAlz=2ob_ zYVpj!CSmtvaZ=&;wQ20uMyyFwz%j%LKNi2C$!hQCY%OUtOJ89|iMn9_GgNOheU*rw zNr?`S^zIhVHt`g#k&4XA?vm?qSEYIwoyVw?Jr@t*UUVz#DyeMk(dr za45}DiAT}aKtVb2sCzbq|5CBNL$`LS-V0<~TDiNddiJJ{6SbPJT-fJQl9H@W&08ln z)ssRAc-vHKXTUn^4_z89o{I1i(oTQqRL?v+{pKs?8eH$ZEnd_2!h^U20cU z38t!6Ey#y*OdBDZ}N(}2F(}u?@7Lqv9}_bL|&~Vw#3H_w1T-23a zs-5n#_C(LPe4Rztk_DkEM?idB(Bt|3EPKlLeVqs@x+;1(g+4=Z+OASfX(6j3IhzmY zMq5*6TCUfuDl%%wR-YA4C-OF^(Mu_0V>b3`zC9~PMSD}MdYX75FXXXe6#EKBI%3+U z*(no$pB>SCr-%>0dPKuN%nZA@>t}hX2T25ih9&O;8ZmocTM7&2h*eIM&(bWjwO~$Y zDiO#SfRgc?O`ONWP2*pk&6|reAWxQNtXU#XHZ2hkpGu+ERT;m)U-ug6Lum=gnTPk^ zh@gqjCa}k91HQ^cBZK4H{*w)<;cds|m@31buka$*SJd#7;>LmKK#DcxX|>wLeVg4C zn*Mas4DstK)tVAAta`t1U*Qi%@LM>&xUm;~mwqMPKBN=BE?2GekQ&Y@cF7nyPrKA= zt=Q$nS@E<>M#>4fU`hLZ<-yG(P~Wf`KjN6m7qUJLPb#sVv5v>UUVux^vRz5Oc48MG zg8VX?8WkmD#*s`Rb#oCX83Cwak6X^z*_$*nFBO2v@G-?2dE765oOd>#jtdksWO4qe zAiw`pq;>VpSaM6#c@g*4JNVQm4o&2b`fei14=Kt_+tts_tyO)MD<<;c1u~CF(K+MC z{%Nw<)Tva($#$Y~aAw6h#`jq42KhCs1- zZ=Ktd#Hay#`kzhf6^Ori_Qe16hboDcTHpsl%{(#>yiZZ}l8?l?3N`hJR(jt7(y1Hd zCA+!yL_Q_#?><6Vz&;~=G|x6)a$yJr044T(l=!|=cmg$@;-6}MXbf(O;AjwBj{*Uz)vrKV#9Tfs8j>d>&!9y-wn$TROw z&0186-D!&JoaApdrQ=M^Hptq-Rfxs?^p){_C}zz(Tif8G+2MT7^1))=*84O}v#s29 z2{@PpjykmzOUg5naOG(l&l|<+Kaa%$vkp^i3KZi~e(8QIH)Ks+X+C!>E8Lnd(B&Vc+^ZUJQPWUfVnr4Sh6PmtdLs zJDF_sz*y)YVA=u~-<{Whya8NIbja;wto3Q(2T2~CE__^#@8I+El+nDtM0TKiej|51 zJsd}NhmhSpL$afWRXd%1=}PIk)74!1^|H?p6R)%TsN^2pWK%;s27=F^52p0fRE22& zf|8(pL>sF!?e{Vo-GdJ8Ni3vE+OvHZuwB2xExTWhpnKn^(Y?x{yX#kjWZ(WtvU`*f zWDkC6kmSK7Z5=#ZcY6fP7bRe`wqL{zlF@+xv!RJ-hG!u3D*~3NjY3UZzvul)~2} zjjrcHMP(Teetya*`FW zE`hct%OkGhJAbQ+?+mY^Lgq!EWwFC@V$$l z&B{6WILOO)T$IoT#LI7JVZBnbDyU~RYSZ*GC?v7^&?PGU`GL=5(MzAj3+129qU(pU z=qK*ae=5IzN-++-NLWzh*x)eTMze_@91&_6zf}mUm{1= zxAmXYxA{YI9I0<>KB;fhWE&Iu){#Z5Z;4#bv{io$3)4|yI{A0{hX&+H6C8CEfhbN@k$EjIeB+n}9{|qX z-1={ycO`|2Hl`Mly@4IRTwKboX?tEyUbpJj0riP9z?kB*GNG21>{{F($Qsu)1Jj4` ziiSCJU^qS$a2&?r@4e$ZA}c1!TNRPhhCBY{?ex^43h$&b^rvu)N8}t5IVXvCI6)c` zq`9sptMO`&0q{_oSHG_N4xmznx-5Cktp1VsVY3**5cmJeNx03jT(659iT=4IX=b(S z4W66Cs{+2xV54R=HS~xDm>aIVR%LH=X_6Bd9b+i^4U9}H*GWGZcZJ$C5**gW2^0ZT6w(-yY4js zs}p^MS+jC0<3nK14c|fIif)w$tJ}VX-yiV(LEuLKY;2f(W+X=jr^Ko4(_=1(NxLi@ zm-_x`{Ljb^AIj^l`MBDEi}I%-6ed2cdUpYw$X6bGOSNaEfbjp2sDlK*k|=I8*agl_ zYRZ>MO1AwAaH@cnx|mOOUQXHSw6Oy9$)#Cg4KBTfP7D|T$Hg)6FMms3n7=^3v2R}S ze^bzn9x%6XOD4VT_(A#*)tmHo-1SDhT=Sh2tGgxbD;W7Q4>!6!x8g!7)$k);HqVZu z9!0lrR0{uquPboxkgtY&TyH}Z;q*^Fusd`odK+ez| zr@ z5?_%6K9>gkNgD8F7sOKc)$}LlY7boUW{kn#dui}4_-Kjfg?sNdxro-gi+<~thye;4 zApTl4iIS}{6Jxn9xJC2T7fw*a?kB9e?nB>54=Ht4ddT69*1vIAYVCMu5$Y5266b?W zE3jF!^j)e|OXAZCzU#N*!^EdqaL%w!-D$gC{ktRym0@@*4C{#M?1-FmHcq0t)q-;- zt)oab;>MVDOtVgER$FWO=Usd+i}>z1#iwcIyP9<$N&A!7FKpE(K$d#QScotqd9YA* z7lz%qUwngySB=P8v;n{tIQE}73Alq^Qj1(9dlpzx36?9eDnwO9QbJ;LH*^g-ul{}a zH^ldIzTsiq!g`#ux)t!Lw|dw~3`x18zG&f>ui)|Xu}D{#v1D;B?OErF^%-g&8jkbA z;lM6dT_h-4ujZEQN-p}!ur3iL6-=S_5j}&owbtx?;oz?BF00e3k{ScPKK_p9ui0d( zMrMe$)r}vhV+OeFhQe2*gO?t|p8n@hPwMiq*eNCk{O*!6s;{57q!}K8@ki6272hkz z(duVC;(K)xa68fy{LBAC+}nUhRb30ib20-77}{yt#6ufocY#T`%E$u(R;t|`TBl7lG$gSz4zI_Yp=ET`eFCt6_NFc2;Tg7WHBTn zuslm68t?Nq7FA;TIaj&XYN)JbEx**g4EowN@7gbxX9mwr!Aailh-u*?j})XO)kwo3NvASCQW^KWa^iUPJM(mM#fHk>Uqb0k_Gd> z1n0jM7ZcFMGw}$W`Gxb3DzNB+>j9yVUNbuXXV6G6PC62O3VNx z3gQaLKvg#wi-k1*gXn`+%s<9?sfGE+W4>0*e|-j$dtWmDO&TBahQ<8Py(+U7eO7tv zN9KQW^8BMetZ$OzFpO{E48~k5((w>%>sgFyA;LEn0Y?2tJ&%6!o_CGi2YY_R>jk|6h~M@ek@>v6n#ROm!Ot;t-^=V3Q~)c8q4Qm! z(*aZCZN_nLebH3UVQe7zTdNSt(~~nR09$(-i+hsv8>`xizFO5(I+I6&gHo{li zIF@VC^WxMsXw_t9JE4En}-X5MP_rzGmLy&cf}*B75n44GJqO+63XnplQSpZ71C zg@dwW%@x<@0kfCqshPf7Rj9zJCf#+|>hmh65X!AXdf;S1Sb3!uEh`JcmE^P+ z(<&#!dO(T)1+*_lNTiFBSA;b{m56Xo>ingd!6Da*YhW8;1QQHOB00LhXL|)E8v(=Y zBIg2`{|+#QIq#VKm|!;BZ)$Iirv*YXSPL6==ERK+_#7 zWMxZkuZ0w#+OnG4sXI_1oHcGdYb@Q{S@eWXZY6ZvsTdw#lutUVnnhJ80epp+Fw<48 z8|$%My42$LMD8-Dx{+_jeKqP|*tlwrkySp|oH|(^I6j9@ea7c$p2M?Nk&CW|VMLF| z3yejt)D{?G=+HB^G}0fV$gK7p=J3~9DXizkj~Z{{7gWGePrZ*p@Bk7Vl2}zSswfys zUa3`$aq>UA9IVd4tI&p@Th<8|3he*Y1#B96NI9wVPB@aCMLEg)&mTDMXInZntH-T4 zmsHgtXA*BN?h5Y4Z@88}04Os#zZPiM*p?R6I_q{Ws;Wj`q@$0OsgLD);Mi#88>?2* z=!b??AnBjl;j22%_yMQGQ5a6gPHd#nP)%~GEI#jGYuu1IKmVu0jgmc#fi4kn&0mkfoT){4*`+0(Lm_&swlC28-VE zdoLD$q-0be5Qj&_@_DdU-X=YrrG`lvSYVjNwcS?v;c?v{)BF*2ChN+_OF>~r2{b*Bm2GFk=P+)SBVTWLe{DhfVo&c%WjgB#54-q&#J% zJh@YzGE<&1Ql4CDpifO;yl=y_m5o!phfA(QlwAEs^sYE?3;=BHAIJ!fLYUU!MC3Nf zysu|!5`OUU79cX?R&dbCD%VPAgOD{t;VrFEIe1bcJ(DA{bw+e=BON&BQTZ6{)^W2G zOjS3KB`ZBlNq)#mesCv0WF|jkBtN*49x$L*Sq6HhP#n)-Mt^|3eg@thWXdsqv8COx&QTf%EV zJpW*=R`utzb&U$|80e6h+HfbXQ`LA8H{Mg(KHlN;M2bAJ_0c#5O|up0D3APJ9*Cm) zdOlEOM-@+#H3%;|em^iMg>~DPkqB&y{$4vXr0AU(DDMsYgA)6|aaiPp*|_V0c*FN8 za5*!7kO5;}eE}W&Nvg5#ZQ;D>PW}K|M<(`25pH#A9Og^2Ix~SjbYt+lhVCuHpYvxT zo3iBNjWWMzN4TpPq^ac=t0{-<#*4TygO}aRoCruL-arBaezg5$MLiL%#_lc=4;b6n zb@g~x0uzUoDk5>il~p4Yu899z72;HD=`9#7^e4G^GZPXqjP3Bjh`zc}){uk-1(`d7 zG3@3NLLr=42{`c1lKbKKs`@9?fT_Psk#-D1V^{nF9BG?v4>A9EWqR0gC3<+=2t7P* zlpYQNJzQUme%J_?^02p~=n<87sGcYg#N;;#nnK9Z(~6K05_#tCT1tMRnRz>r7EX<~ zVN3vec=Z?1!`3iHGCgiAda>516vvH0VF&)7(8FS1NxJbO9uTZ*jWlwaD#ntkT035~ z7~rY*!2nmDND~Br?X2tn03DFD5RfGJ!ezjC^B|pNV+jMr*huT}3eB#1EP;6J=SRjo zKRo97p)t=(#ysCY=2=h%Ni@xiV^8pr{s6v7bcT-k?#ofgnMMcW(cHG4zaO)I4dAe( zeoqmw60jgJo>i0X?JX)%y&Wq)()ouJ72yLl7*AWi2#8ZH*a;+MhsFmrygcC_=$-=? zfi`}z#%KF8oelKl^R&Qc13me?7Oc1F$(G`ebnn5j^s%vWd#z_BlI911oGt6^el(ua z-rTY6-A1zjO}4p-q*k-aDvCSavEpcI%YXdEEw6l0(86tSiXer7?bp{k5*>hKat}^G z4_GBRxlOd^Eh5i7Z`q_d_2O(d#vcP#31*C30w&nIYUGuWiTMb=I5KBsxnfUb5<)`$ z?9PENU5t3DSN+>(U$7QZ7QzTecebCZL_D_HS>%Dm+$(UIW2DW;KF8iQ6@P4)qS^yv zs+}U3zUTVIWQcmSz&;g2+#$JBW3zotNf7085W)5xP5yc++c(m!?i--P2GMT(z(}`t zP`AjG+t^N&K#-^dN6~jwK_{fV9lhV{AECsGo<&PB6=;QSn^pjX_2@l-uvXnqy|Yqt zM^hg!r9SqiKAuBtO9*pU_r(5@6VIG4?Zm*exorc3BZsy`vA)(4`mX8}=MKJMuV1B^ zPsg=jD8`3^C!KWrqy?kS<@40wnPJ6rIWQQlWTBR^RG-^lSgV**GYS>HWJQ`Uk#a$5 z0;R?)QfndCS77u{7sfGYMRnP#wxX{{791=qa^tmyJhK!Y;$hW0RD9tIF}|f#e`C{! zoGJds)WF&tAgUE06LR}^B7uT}{-}yUAJb5manQXYJ-4kJJ(%$3ij%#-Q1C{!1ud&P z;2>>qpnm<;Kc7PO()kh6w!ijk;;BWk?M~3>x)aXX_k;;Hi=841Y{}Iik zZ^KA#WpA-_437LcaFnxQhGx&PLwI|c5QP6$^)?sXEHx(KuR^}q_Seprh{r$Czpc+=0M{M>UG5!lApbbVQ zU=ZV6ta0K5#Ki<0#t9e_6Cm@D!nV|>PQlshlc%6|%E%OCj!eOcD@{S>59Qzqn1VK( zg4+M@#(y@9|MfKfb7B0iPaXgIY>fX0<3Ch%i!3F{2Mc|?@?I;~%3pDqvfy7C7vP3ZA!h zeokX`Yc{$u&of)y7?wvI>~}gG*$(!iB^x{Ns}(T`Fadk9KBE}}RmfJ4zI#1tfk)HX>R}h!OLVUq zhwp!f@5fyF%?H9?t7y!03zxz=AIR-by#nlB{HfD1aLiOQ_(1y^-U=U%1y7f4mfm<~ zn;pfQDXpzt+-g6oEIs`$F&g`ko`-dMQm!}!oJG7R?|)bWV3$NC);YH<9g?k689R|S zEUX0+@w!tV&t?&{%WdCYiY@ z4Zmxqir?Q&7r%eVqu`B+_> zYz;BF;)0_b4p~JyD6{;bLYe2Sptv~2n^|QCZR9BI7xMR_TIb!;5r070J`3m);g$gQ ztt>_T0e$;SfC(8WP%v6x<@Om^ZlPTc>Q|&NmYt4e@3+fh!D(3VA-kYYvM^JQpKWQx z3_d9W^<#C}8rwouVVS8=CW86VHw3(#0^hnk`W-k%S=Ck&?~q^JS1}Oj0Hn0DRSU4!b)Jgw@*Ppzv@-90o1A@&^^j*r%@?SBF17L*Jfn zopH%L3MHTRDpd!6sgN6)@}9h8dR~++sYk2(sHmc3ch9K)m@?rYR#^5+C;F^`+PJbk zAEpS#W*EC~#na>A);LrHlUg#a4(ArCK|x531I1>T?Cuxtel{RejzQSrN3TLfyJd^2 zvhin0)eJQFyvWM#p8Z%B zV{z6uYJM#9Q+*k7sCU+j~EBIIOd7377ji zg4$XfEr7AJ2&1=^-iZp0&ft)X1z&a`O3yu>8%K4+ze|xPC~MrZH!8CQQMTYHRuhjD zclV^_9*4tW>IB41c|kSaEaci4$NG}aKk)PSyC(NJ@qoZQDhdDVHFM@OM0(PxaK7u{ zd8fnAd$hnB(I%h~pO50@Wv6{al4HVOVdL7m$t?hHu~jTYYk_azNAPZ1_gbuy{PG(* zws%lQcNmb)f=0t*S-Q*zd&U*knh!{dv1rW{pSQW}S|FQyCa3b7<^yTQ$~99J?~&4% z%?F%4|G^JdoDoQ$HRsdeK8mZ#UhH1xG=p#QKl6sb|>)M9*}^(F5G>OWHaI+=d_ zFJ$*bjR#auL=`2FR*)w9 zV-LEVzaOXzL6V2NVi%k+W{pPh5vbO_Zyh*_UPW156!!I?23rcYVxFU`9kMFC&tn0G zZGzo)5cxD?b_TDvhB9Q&VMXezupCj0vSnszOlu7e9eCp%WK#Ui!NKqB`VLY?dMm3l zwoOJisTA&fY10c|#r||dfB0P)b$QsnrJ_-?bvmQ&VFcU{0IM@YF@BvPdmBstL1Zfs zP}v(Uo2wWr?7Y(x(8iTnJwFhzDI+;c(+SdZ0~MfZF83u0lrRD57LXn%_HH)tLVGht z15GX-A--jAgqh#Q?ay2M0pRYMH1pm;l`qPPs;y_qN9CfNUJP*;JQba1X8p@}BFY!# zbgJgUshH@PZ{|Iz1;eLw{%}s0Ht^m&bNZ8N@a;m_BAi*@S#)m{f2N@4sv8Q~r^nzci|*}*zxQYOfB~n+OYrxRiST&VC%q3j z+W}3*(Am|o#gI6Lic-gYQjd%@U=7MURP>;a<1dJf?pv@i3a&!YiJPZi&(@$Z@ z4x=Ng76b$TvHS>vaYR2v@KIz1v|vNj?|rX0NbSCIPq2BNx}oQE>0UjjmsJL1$X@-` z?|B#gPu5SbdoT^1S1bg|bDvK-iU9Oduw-ExKww1n!6(!3D!}i(u(GB5`xu-e(Ce4_ zQFs1A63zQhXPsmH_Xp2S^Wib~4p1~e^OWqAJ@3k_60gHeTO)R=itYP%#uMOLOeRKWugjZ?QTjjdSEXI?cNzKpNIy z+V2MjckKb<+*7acSl79~0C9@E8Nhk?)y|4yCUk31M*YZ}vtT6eAZ zW^SnaF;WOe-%&f!8>f!mn%BH6asUgnk389Q?@c4iZ|DSDXQ47Po-D$r$m1>O;>~vm zm-IWc&CF|pC!)yG&bl2~5Z4K6`mHo^3#8WuG~HY=*i2dE`1Om2hge8AU=kXkobN*{CzT9 z$O>_T41J2e+D=P_tfv|FK;O-f0QP-(@6A|s)_{*qLl-9&vd&pCtmJKY+bN-2w@+|a zb=j~&sPRL)B;>ubi%NVZvuX5IM+16?8;0az7aGx&<^4$bL2PAJ1$f$eTUe!EB8DrD zfyZk308-ep!n=Vh%d^$BS)~W^7&C*kUZ(TD*1_OuNjgBpT15>1mPLRahB}yjq36xy z^$P1JX6K8+swmqeNIqBzvG~g8;*G7b9%Ez3nrsg z!dYv^e<4MLRMlTPS?0Z{8~PRW zFxLGnyoet_`aGXs=>A_w1M(0JD+UD&o`B|`50P{S3v z!k>%qr)=F~;r6p^$>MTYJZ=<%!DPe|%DAzFGA=?&n-p)T=<7-P%XL|ErB>@?CSOOs zW@tf$io4+a1FE(x$GkUAO%2A-96eC~#y`0oac2$Xtr%>`cEnboslvB~mpH34eW1gZ zkyu^X=({Q(DITJBZVH|l&J9%z{^CC+yj4w!wZN)QA|>!Si>zQ{Or~1ueu&H%4{NRG zHNehV@Q8!A=z*6-92B!M2itWgwuh~HjJU8<>Ew;!1khUh;p^EusWB0SClm*6Lh}S@ zlWaGsW+v`9Zqn_O@TO~rGqK|bDr|R?!jyw@ula}70Q*Jwjp*{a_F<9UTv4Ea3A{+m`b!`ER3 zhSYYL<85M0<6*TmuA6JG@S$n&Auh&SjW@&(kl);8;?@I~gek{SW6YpAiysIMrtx}O zql!Tbh?xtBM<(CE!860aVC{j^uWQLk=jY;w5S@~YGnN=ZHd5Tn?TQ%OR@~`esKipR|OuY!J)ST8RW;Cx&+=h?C-oQu;jUn1%XA+WoaH% zsyd>O>&31oh92GC=j0Q|)) zR8M#^yXPNpeFHVG8`s?fFrTqxSBAnL?9z<9T|l=%-a88@@5})x+R^>*SShOm66LzD z16((!K8Zb2UdW^i(-(xEz)&kTq4|H*w->UYZ!ctM{J7uK4&D0@Z_Q-sIJN@5zV{C5 zo-&XDe=`T5exOr^&`sIx`@W#o4yisj1aq>W{!RxS;phTHZ{c@ms3!E@T->1p3whPY z&J>wQZ*e-EPY*V9&FFMG#-1-|W6s;h-ctmJ+^Tu?jESg6J?>Bc3O`H%2KP`^Tj`H6 z@cVV2e%Ix&{gdU*SJ;lQ70*xusGpMgD^|DiS80mppbT3QIHL?%8br@jf-_<}@?aur zc8iT*Y2N+CZwXxAuuuzBKv|aHjJRCZ2=elU;x(qMc zF&8YDLm$AyMa=afLK#*7e=c`n0w|ly9SD|K6kZeqbGWj87esImSl1&zo3Jn2vPc=L1QiMdd<5qYB}mp8I{}VoRsQOl@{}oKf9pY&2wb^d|w?EtR96dC!zu1uJvHzt$xsiSNE2`9#w;2(6gK?>bfUUHBt z9l7FTVab-NAG9k*hxXuz{*$>I>xJ^yVEN;0&3fX}f|oOvuMi~avK!MKpC$aY!(%1s zOhTfjY7-@vx42M12!Do!48A3JyNKhi#;ScQj$I#fm%rys?xD3tvcJX$dclo>@ z7fnm0Cu^v7tJ@W~TDyUE&zwUf*52ZeMK^mov75z{Wpl0s%oCG&n63J|_?WmLtg2oj z`bbT^A5NaX-2U6P(+twA>=Ap9w83cS-7MrEo+^Bc5_;Hj!LW;zp0CEK2vmMrB=+0( zbFsvJqX>Oh+Hdp`H5jA))~BMF)821w$@{JEf3)A~?EO|J_FJ9UZ*~8_?YFuy`>icy zzjb|XNN~U5nBjgyd4j#)&c(*=x2`3}#+?5n$X(%j&RZ&9nG;3l!fS3rrT>7E^A->L zcu40;P8T3Gr@RxR2z3mdxvv7+~LH4VlR7ek|eNPU3#|qsRDnFm)DFUF1-n-k+3& z%X+&o;4P^8rgr3!FHo`xlhnMMLYO2$#7%EgQpoc@0DgdQMGUspskz%zda1@xb~3aBu~q)L;S z-fWi|=)*RK&q*spoQ0AC_IgBPb@#ed^9d<8UJEojkekZw z2Y|^8%T_eQ$M)!V5Y1qnoai%u{DK@r;W}{9H!HUsA=~5ymx)oM%h+%sUs+s(dVs;0 z^wewcnbIX1+j9rHQPqt38LHH$O8uJiNq26%=i0*BLZ0<&pd_IQbJA3Axa3&C`CPRwj;}HolKYNtJ=!aa@#6~a3EHONz&gF0{&eyp3KK!W7g4B%Dl7qDiNGZ zXlDKP5MuJN^m&9_iJRF(+f+>xfrV#?fWfoqXL~+L!iQO1@ibO9CUh+9n2;@5oAsTm zZ|+BI`zf5A%TEArQAR;~>CO9N-_$1Fw0}D0O>g4O(XnMVCEhF>TgDN$-+Xgy8KKNP z>l;@;I`)k_Zoj!@?3+-c%<8e<$)+-&j{7?Yi3~dtSk*#wsnAhJYdYdK&}lW_UW24* zKU?D;kCAAyS)G3hH!A_SL0HqMrmtK#m9#hoQ`b%ZS|CSs{>tNG0#GHt=jN3C$1u*zOWX#Ul`-yT+U;4Mfb;4=k{y?%cp66@ew@w+3WbqsG30! zJ969@)PUZve+YhXFD}H;QAnK*>gKB=;I28R+tP-SZVl?bk8SCYrG2v0uJDbNQ9)PVL#(b%98YjO=xh|4TQocjLmrQbrx829_ zQ;)X2OJf$Smnl4GN<0Xf0idLqiTWLQ%ORGBLCXWb>UAhjve0Bp=LeJhN9-v2 zO0Qf!GlT7z`4zdkD8mhtl1)BZf`j?Ro!vhf6Mu=cgkio>u9O>6y!EBuR)l_#2&&9p z7g45OC*QAyTm*&Uq?)o5T@Ultp79DC8N9`p+t2No9Hfhb(GzwrB9&g$uqPd8??7oT z-;O}O`(aW2a{5_Mc8q-ea#1}#?Hq^I5B0cUtHwAnb;`EGoy8&8a2bI*90*DE}th!|=@Pl}gB6ywDu#IK_G zO@6~kpSWE5UA}t>ed5K(MBV0-^r;t1sp=P>6kjq+sZ=_N&yM$uQL~4 z3#!A+1?Eh0o5mO29x@lYjJIf+@VCf9oV{+}5ft!s4`L!*d3(N%Uc;4F;|d^Asr8Y| z>IANLnKG^%2(>ch#&Qv}#h|%_MVC*DL!k#6eS=2dpm8#2oM4PdV();Fvbx_NYtLiA z8uRb9y3*_&R^U*sE4RAS3LLO`3||D^AZU}}+ixegj@_VytVZ^!y+LVLch4BYW-O#8 ziNIO3zKrclM3ZrY80Nzx3sEt)APSw+)a*7V9^1aYu7$-C!MTZ7(K#~$6-;d6w*F2VntI6FOwFAb%B4Tw-+|+4~ zML;(qWaQl_m&aPj?jyI2RBvthk7|;H9*Eg;RXAZ(F_6~wY})dDU9TQ=;+a9F%+?u~ z>Lf)*-MDKOt3&6Hbw3&pe}0k%6>LQ-6V>zs1wz81g3kz(F^-k4wBn8EwnqrsuuMR1zg7Ow}~vS=}S_QBhe%Kw6O zKAY{xbXzCVf$*7n-)#1J)_tC4R@cn7?6bNjL3Vxd*14>1<|4M`I1Kc_Q5gdx?e3GA z@@cUDW^;aAZa=NC-GDTe_vPTln=$lN`$uYEA5b5O`?kwk;4lnk2O9RVE%mAub=#MF zHwI5cJi)HGkZzIzWx1a3( zX56+ntwX7O_oGBx5*7h$grg5T)}XsVVko=beg~FGKp370oC&?JfOUksOs<}!nWYk1 zMFS-v0)I9pRvWCKw64F4;PQY<`(gEUO@qp0!|3@5{Ww=jriz z_xbp{z=@yt0oKG&D?duZV0HJVNnzPDsF?+l&wG@I0l|I-IY7u{9e=IN=n=c1B5yz-N##YrZ+t&FcEC7^~y!fu38^5$xI5egt?R%XMVyWwT&XBwfhXU9I_%|3YIt0u8t_?5; zMf1}w`FA*1{bA1u)zz`NZ^c~S${;~$}ys#%pHbj!}b(>oj-F4EmVv@ zbfLM|j)EK6j(In?L{+vYlnB_&_S|z5;PEuoId0ciU5FmD78uxOw|m%_kBW&n{#cA93^kD&dJ5baYw z=wj>Y;nX<*)KLZzIZdtIdmL#Pk5-Up6&5P`bAQbnTQOdTrxl}FvARCCaBGH(5V)P_@oIIB& zo`_yEf4guP8nAey&KGZ%&3m@Oix7!Mo^YYk=WgL3eDT(93|%nB-$X#rKEmPFMYhaU z0e|!gH*+tl$Y$;(6*Ji62jJh`Xp*3zNrI${fSUFU=*FK=0_8U!^Ep(>rkjXARn1J< z4rNM;kMTx(GPGdh4BdNxnNCy-?`!S8iliUHOHT)NFmoQtcc`R4+sJw?92|B#Y>5Ps zwQPrWqr6qB_8%-C#Gt`Dh3XNj`XlA>=PSn4*BX5X)2fIY7jfBTV=b;apRqdo8jMAi z<>UuEhX;;H&B6K@I>Rwa%2hM(KDNcTQ|b+dVz4;PnX^5uK4a~rYq4G;5&itVuGxE$ z&@JAH6NqCA$B1YniOr4G_#Ga^6_^Otxg-C^YTrTOAt6b=;+v%yIa@JGSi-gDUTnUR z`5O0cV!L|mzWGEUyJSbhyL6UrLm>gj(V;`|Z~-qW-?ejn zWasKhTxWK`%l8u3neQXijTN|AV|#T0^JFbRZ>+Eqi>r0-5cW&lMkni6!P&4FEhhWW zfRWF99(CHiM~ct*DnmM_*aWQV4rC(G*M9M2;s1pg6{~Afuya@q7`O;{^KFYa!6NfE zGcyCTZ_r`6@dLW&Fb0QSk*V`G+M;MyVh4b}%gl1)xRHcgSzu<~jd7?Km|4#y4d3xG z_a`)d4~X&0df;z2e6!i)tQXLAyx-eV%yn<09bg1IK_hvq@V^Ep=-ml3beYfmt!;LE z#^xs#!-&H0_`LgdPI!b>zA_G3@CbRUv0wx2LUxoX7ZO~~2_VzytcTp1i7D ztCBY(uA5tzRA&4LU`7fsLjW^VfSCg5P64_FFe?R^C4f^>fKvo;Y6@_w0A{BE zvjs3G1(+j%(^7!b1aNu^aJm4_NCD0ez?mt)nF2T~1vpCp^HP9$0$83h$mL>?D^q}# z0$7~_tQNqU6kv@2)}{b!1#n#oaGd~dNC9pTz>O)ujRLqS1-MB7UrqtOEP$I+fSU#I z)fC{X0=P8=xK#kRr2w}H;La4_P62!?1^AW#hEjkb0oAODi{qo)fB18YoQi~!aJ<43Gq;CmoB7~x`-J*yV~W&%L;M< z7km?&3uLB-ex&fCz;vmyb;>16d$quR;n{vSIRP7M-Ym@hP+%l1oQ1U?5UemI#4Z)i z=6S!8x!+}-ba`5BG%T(mtdnU7L2NApKd5*T=NqpfK1T{&WTk>!7Ka+r5i)l@TgZkQ z3)x|MHJj~md7U)gTJUWb7pZty|NidHv5^pGtn%+K#^cFO3`D+w4=N@1ML?9eNBv%s z34Opf4A7_3$gMwg8~{-OsIJ6jkOrkz0A2#%!tsg10O-YH8Hr-j5iPJ7{cYq=Oi&e4 zRElT;e-{?aRBeJB0H?6vBmfL13i>-Sk2{e^8UoOJepdI;H9)+a&}?53(%v8~XLs>a z@c9mGWLBb)062toT%Ty5)C`~&%rj-ABiN1onCojPxlj#7ojRIJmVn3h?M3KK38*5S zkokVt=bL@N8WT~1?m1xKr_mrfb;IZO^Fdg6wEJO0%o1f!K-*>y17OjtG}|=WJ!(Rr z-_oXHBW6b-hd#@dD5Kv3jh%f7fGcN>7h`+|fJap6peh{^epB$`Nd*SpI-d=Bdl%p* zGW`4?wo(^bhcIqKNU8$CPO@mRPSijU7pUNP=%%D(tFEFxp@)l17?SX8(!?Z}npi~);?{rk6 zbsao8o_08P;veRZpg!BdiV{`IpyTC!Le}dMORc<>M{o1hlx2UP?kkyz@=0e&%@}@- zM|s$aPlO|B?Y|G9;>SM4SX`?Zc;D?`$0~OX;VtHa8)*&mW_YmyUPOY3HvRzb2p`i+ zbE+f<8y51gA|3RZSrTC7g)qcBx5C#wQH9mPzbz$ucW%9GKS$u%zqkAT&wmzL!)5!! zaGY+&3wis)-5!jEC`4kikcmN#`~pL%9_GO=S`v*YgTRwbYC}CMZ7cdQSdAs%MNqA1 zB#xn?!9gByL>+){G4G9AQ+2Yga4j_@z|$PtSA- zzb_k9UJ}v+uV=wg?q3?q!iMpWHFGW-y%N>=c@$0!bshba)8WS(%3dK4+01t2fBmA& zN-J~3qW6c?0Pm#%s;R+cu9@p{keb)bZ8_!Sj%GIoS@q?HYEeWIJmH+f_4FwI87>IB z(upxHV273T-U42ej?@ElPh{?U+|2#sZ07#w83k-Iz=u%nX!5#r$7M=vOm~aK%jwyd zvB;#0Y}vhT`18|j_*00b^mGiVP_`(26aFko--RfB2-WBjEA&} z5RQi58)m{u4TaeDL!a-yk+;~wQ?^G-NKGqU=+D}v0B>8*1Qpr$|=QrVH5$ThrvAxUDOu=k|ARrWzJ+NP3Mzvta#avbPP6 z_cocgb>sHphebFXG&-&)H9LO9I`6DFgJl2}k3(S=`sVtNeWd2xQYhk&W7zR!4?BU` zi=oAjw*e9-FmXr=hO%{UxEPI=y;x-gosW3Q_Hr?%EW8>4mhW)U*9hMld4CCpp=L{DfLz*=wG}l6=2rW)|5pO_^`Y=_PMn+j5XP zkqwTcu3&Jx;_|8{oiEX3bnb zONe&qvbuTTm@Ujzd9z9nBwLc2)PbY+eZ6LyITNu6sT9H!5|8lIOO$6@ZksN&40Qeq z%6Pl0q@?>tjH1JKJXm{|nfY57R=1G6`l}b1dG~168JF%XnW7u7kj*8ER?_6)sTh!p zK)COh*`9W!dx1*-4V0<1DRxMkLgGxsxopmpgwZDMWP>A>pK!xXfNTNSIs(WS0CK#{ zKd}u6?LeR!@6ZkM(d6fS@#0Oo`QUz396$+cd^;A2Z{JFP@dNTb3**~y-xxj>Qe=Bb zj-2Afweq5P@do+6cyV={v=`#rJE$Bkm9Br8EGSSQ73kv?dp9`}sHj3ny?5Kuj_0aU zgUanFr8DQ~!h@8Cya=raLlBtWW*A1{YWTr?W@b&INuEaI+L|f>kBmydvuz2uJ*-%h z9Inh6m4*8^DFj2F(~iRZnj@CN2T2I%IZ&-j|(d|V>f1?F!@7>0k-D(a_A zL|l2GID(^weW6YYKMPy&qea(hOQVQUW$(5-v1w4a1@C%l34dWwpUm8Nza)@>vgiz}_PHx2T#^#%NW3$V? zu^HM)!sss9+aj|CEwVQRf7?%@ulA(-8o52Ha9|`}%(sVF4Tv$W3Mtk8TDImqZjRQZ zo13U>#dSa#?D^dNAV$PXy}wb+EQcICnS6y48hihcZr?wo|1IUmo~+q`B{E>2qWriK zXS?XDz>T?{((UEw31YHx^8g-dA-bapUEua1x`d< z;Iz=B1{yTjRhsaZafJ(4s{S6UCwE3$?FL1sm8|!kCu5c*6 z(7KxB`x}|t&v9t1$c#5Q9dh|)I9+d|aIkOX*mpQgBAzD4zQkEi>#aU^n|~8t<@C{2 zPBS_BdP0IhSJ{F--E)9$btdb)xqFLE$ajZ_kTObK8fJ`M@60zdjiim*Rx9pzeq-P7 z{N`_azw;X{Snoo=<>G!2_Gh*lnE71deuqO6uXmbPc8}7d;&$JMXCo>Nk9K9G8@9)A z;qP5tdV%<==_#?0oTzaMu{Pi{Q%bNkDi&h$|W5LP$n3Bm*It$&gHhxRW7n zgk&W{vJf&Q88QVSQG@7d|;EETZ56eOLYS zWh53|zq7p&BLy0@#aRZikOmiIB_TZE!@HmEMQUwvNHP8_crOfLyYdE`=Ti898b64j z;u{5P)$?z_6`?P=O@cF8Jd5Hf+6l!*vNVP|BnAcUTf^vuE={KMI~_ZJTvPb zWwvA6?U{HU8YP?atDpKhA@&1wk#EU{vTE3hj#r%i{XoVK9M^@JmVKK<_VR_ZdFGD` zjGAYWzmW|gaZ#iX&|wd+*+wGIctg=I<#-e7dMxcjmIgH)tnKB5~6i$Fda!_;pVbr+#BTMZBVc<7(b7! zfxCZ(caK4$tT!;XYZY_rt59)n6)MiHLci79Fi^tUi_TzIPKAfStzs%hL{L#ZIK#3-|yq^2TLAz zur+y;VBcX}C-gg2O+V{#U%+_4>UJB&_rn?_yJ*G6b29GJ0$au9WXUYm6I0EZ ze}Rpkp>h&D$;MWq3S&SUqhF@1Kii@m`&xNx^0hL`sN~i`g_{&sht+jbxHMteCz}Ov zvb>>4AwJcon7kwh!eh1*s9f)_B*gF%f4^SSCN&q#N52 z_nE6m@zl4`EI`NXn@EI-H{`;Oriuj3widDJi`o9#HFNdLB%u^h{tRD4HPxbo2-IA= zZaiT{q3EjKZ%Z;&v$~e56S0qip((aV+;85OuPuE$izK7ip3^5f$L4Sd=y?l6%-QEzEfr30#T zIQd$$5$I%~ndjmyvzjq-Iol_}O7d^E@8Yn5!ID{yvTQX>s)ty<=Rt8_8TR2UIPiUG zrz%9UY9LJw%!{fLa!x|<3bm?%g4DY{%Q}&ULK08JHU+~d;P%S)RUJq9w#pye$=0Ng zCqYTJio?Ur452uorlCmMPP=S#ZA_0>g~7i{ed6XcI{<7zlfOMNVfqj0OgtCu^dv~< z+i5p0N|DaL29H)DHJ$;_7{bxEJ$H+}=aSLozc5dZkTm>w_YX+{W32wgi@0{pJf~vL zbjpC_Tx7~~-tcWx*p@CIt3#)MQH4i5QC8Q1CRkFB|IwaVy7T7|)Mv0RPxN{EWU@O$ zX1A9PqBx%jbxV=^C$^=yd$_sg<3u`YY5k^c66up2d0o!R| zU`&vJ{=!ULhEwci2#edo!Whk1~Ty7jfY+^?p*n+G5*1PhO*0rL7Y66KhWf7 zTlP~p2506jnY(sjz{$ELWr*@RRIA4=?b8CvF5rusbWbGlWkm}4xV{VfrHpr*{E;F)1FFAR7b z_BPrw>w(Hu4qNx3c2xJl++g=uhKfW4GpQGlC2o}hGjp{kVnvQY~Uy-MTV*n(2WP>B+UghnUj-r7Zm1?!1gnIk9Zm?N*+S}g_Ymn9jLKz z)Az~Ho|$-lIUlS^m9KX220$KF$JyE^E~AFP&-W@u$vUIt5ydF3WtCh2Hvn+2QL+($ zj|4BrStVvB+yyx`9+Ses@Iv08NEc}{3){NCnfyWD zBRq_sL?gef)Q`eD40{#>zM<9*aH6A9OL5Mf2)>$CL2XmxW z>x9$H$51<~6Z7@{2tiY&TRW9rS${n_1U(wQ$ zfc9AxszE;Da+`X-g8smQR+=jKENCSVAqR_o;9Z3*TG6D6Ij`caQN^4H{&>@h+A8Lp zBFK2UcxoVjV*ZGsW#{(iwsq%A(3C{XP7~9Q zzyvkH(%mNupUXI8^P-ieX7u##OwjC$&I-q7`H!|9+)>&2Vwy`w%# z_0Gc1X^$S5HFCZ#jxe_}aVpwhO`OIiyV5SQGCeI53GQIY(+>6r3+=XVBYK)`tgmck zRsSVOR6=S-&FUns7Dzqho~kp`fVW*uye&|?^8#6`68eSKP&zv9t{6fdmpELew+i=8 zb|l|cShmQC74j?@*7a&qEHFq+4Y-+P+rsi+_4M&dLe`k=A#%m{Eb&XQS&YSQ?s`@g zeAPu(T8=LY*^pJp4pSOwNrMyu*qSAm?5MY73j*{y5EiQO#3hYyOUL?+CwgT*GffNp zfN$G{y)vHY5)asS!z;!8f$;_WtyS0v(c3hI|y549O*5=il{=TC*9UXB751IRdg5RdtBk&!4po{F)oT=N7DR?k0~U%Ig=Iw z>RzPo59W5=!QA>CC?>cA#RPZEV3U6a|L)6XlNY$zK`Z0P@wpwH;TTAaJKToeGzw{Wz0s`isBD^pQ$Lw8Jbq%hIs-~j* zY*eX9M^L9}%>5Vow|AveK%{iH41On|BHOy{c~#`U;U-2F-z?2t73Db{Niah9u5SO3fGV==6av6@00 zu1015qC1Q?srobN{7tI9ZnXM0srtIn>fhW+dZt(qhUI!Pz_z;HgbeW@2J-9v2S~C# z#>X%j+`vG8sA6%48VF|DG4Iw}im~XKVRT&m5^VosxiB1joK!*7aPSdZCnUG28$CX4 zusSE0t`HAWyGCD+6zTp~Nk~o5!A9Us42z_ja~@L7=P;{}_W>v3meIX=JU-W)x2d|U9;d*2?t5Y7yu+_R^39YUeDRsRqOK;2GcS`TTnBh@3+6Zir z+3R|KMj@Yzz;5iCmD7BH> ziN>xUSCLD=XR4{aL1BYJQ?42MGr%8{1&U$d1PRL#9weg@iW4-l;420X;jZJRTzoE&;+Jeuk`C$Ku3 zqHwL9LPKGt=-%;GCT+PCQaZajLw6E5)%`!)ybRP?4+bUns{ zKx|RI%1!vCtQ^PQZ$9pljdC1UpHWLez|4}3s#*N9YA)WOn*L3?>E8^*PPw1=E|l-- zTAoQ#>v9-px3PF5+u`5TK4cVcfW*t~F@unW_d-cpJi@lD#%Z||h3{{a)7pxt2^U?f z(I$!e))}jDk5`4rp8}g7!E0;tYV+mcc(4xldYWopcN;vZ=IuNGa~K_xUto2>!<#Fq z8rK|~9MR4H3%g^yj;l!LXLddDnZwcbZZ%pX`^wcoEv+uzj>^Jj$<{Zrkz(VgyPQE% zh$)wYyC`a?&bOnOtN396(Dzx@!y+8eL)fGG&ZXAz3q!j`wV;QmixdJ-c~iy>S0J#qd`H5#%%eV*@ElzJQzCx||+wXuT{O z&*EZ`P1lrx4#k`)$-%~$7HG89zcn*&u4c|GP%9c067l(~Juq@;aDAj}&ok)xWi<-I zd3fMMrbgNBgL_Ebz}NR>+BiDORTGBOmR8Xz45vG<#SqV16$$HK9#9+3oD}-s473K22T&qGc(7JUW(sk<`vAhsZPJt zMS+IxYw}Tl(K$;Nxac=)!T%9^&I72Aa{(J(^fisuWz8E^j#y-(W`yn?TzT49jeAju zy{ke%f>*OOKOqm2B8H{#ArQ36RdFi>IHFoHUc#NrcC5xjvYOzmj`js72W6~|j%WMq zs}d3T*5YbI#DVKtVXr^`IIZtr(AGjf+YW_OfD3uxxG%S#-A7K(zy9iQT&qinV$q4S z1NNn{c%9DwgltbwDaF5Hb#1^+lLJ24I?G75PEE18SUE|ODJBi-N#|S4hw&6!m_N{@ zEw$DnQBEpfmT(mZObW^o(7Ewmg;TQ?zAv#);*HApx6{(8{3==k3;7^h^GoVQDF!kn zkuuGi%=Vte59j<9I@pV<)j9p>{TnB&-^~}VgWN-a?W(mvD8aU%;$h&pYJHTpD+3pt z=UDD>Jun@&$R~d3GvCrvDC(0d_M)5*?^sv5$&K@^ikqBEM<_t@L4r*?Ko8B|L1)F2 zqUhFm+GlncqC*&l=1tM2HoC31{`%GVf$9GXzIk4={_6Gt(O`X? zrtoQKG!x$^dyliKe6h=m*Mi~?5J9ig6eYrIh#_+Y-7wHr8*xL-P}4IGCG7w?2K*$$Drc-Q|; z+uMLgRb30ib7lreFmM7!jT$v*R8p`dHEo#^Y7RLE&cF<)fIk&mKS%RQi+?av)aJJ* zL$Z535DO^YE0z1&-s^o^3%$y(kjwa#a*5yD^Tv{4eovSOW48hVPz#%IL((fpzYmqCj7yALef=OKEV4*)r{-}L^F%x}#QpIorLlhib}qw`lZX+AY) zaUZ%({|tBO7nJb-#314PUQGCe67IL7M;OZSF(u?-@H{#NK%8<24uMZ=(D`}dSU3zR zUtvn4Qwzo5WJq^A`bGb5X)SXnTi;|7g0NHRrAa5#HmZZn2#HmHzC@R2rFf;O;*vgY z-q+0YOx^)2^rvuOU&0%4bWa>Vh+a+Ij*Gq%ktLUg+9Oj;f4-vgNMsfF%3A|G`3m64 zS0GQm0(o+nZLTOPj=K+7^E-hxSAjJr3QJ}i){#obnqpS-3O`78lWvUp?(_|(y}rf6 zcvB!g$OaKmZ|vjnHD#PIn-1AiPyjK7wBiJyu7KVsM3O4lVT18C!dY>>i7gcUcSv6p z1GNvdC6ZT>SG(fz@NLfydtgttPd;etZiszyt)2)%??m8INLSu?6Sd=A?7Yl+!v6~0 z3u$XindwLY*`QcM@{3hB3q(a91uFVOGy4+AuKPaJNx`0V<~Eg$VZnB)S6_HEnhJkP z?YTfE=^Ca11+@~bD2uzAhLdfKGHkyZQghGcb@`jcxd+;NwO%Wypxygk?(RYB=Hjjv z?febwuy!WXd$SfRUp~&59cGQ~r~f{6VrzRiwKFPqe{9T7VWIZVfOZR~#Zt=d88;thh0>xD)-d;|oxK@U)$73vxSOmby{Py=*FKF0&pr-i*SBwElq9 zEx1SA*bhq&9SzT|GsG;Ju_h-{psc3W}i@H?>O0E zm!~v+2n$tY*zdr@;AAq3qypW@4!NPHKR^ebF4m>HGYzRyT8y+C^pFbF?7Hwn{7b&-QECT2 zDJ&MA`7EmZz6;1kp4;CF)IEb=NxNh%nIgsCm05($G6H8+OSTOATF*ch_{}{JzYFH} z1IA+KRjo%@eq7u-XHl_~JGPh=Ey@e*Kr_uGA0NGq_(edo>H*j{G7Ghy6%LAOKP&M= zOx6mBw2IlqDhZ_#brK5OZb$SS)Ou`U(LSj?23)pPcWDI*VCJ`E-R0i^6-inwW!Wuc z{qq>g5sLmsq>kB<2wE!|N}srlU&dz{1y^Hk$H9_!VJT&EUpT%!66-Zhh9Z5Vuy>#e zw9Pr>KGJgxJ=$gb0{<@h1?+&QPuk$mC$?hpG$Vf9#=U}0y}jLx?31+JDM+LE8ot8~ zC=e#Vc=U??pCRA(RC0UJ*Kux7d=?x7Ww2UoinuYZ2p6GePVJ!JGX7WG?KtV34wRdd z^}2I=Q6URWd=BP4t{C?V&}~T0`UUJ7Og;uxNLtW}PFsBYW#N6C6l5{tdH? ztQ3N4Cc!n%;rhP`uI-Vn1j%xMWX?2#3G!@Kc2LZ;tmk`jODy835UAMF=Xw`vkQQlIIuUmo+yDP>S)a z@XaYWo(#X91pIEigqPVF7J^{{e#eHv@59L3gYX+S%b0vC=5c&lawG|c2hh7tc8Bp3!b4F66rJRWHz7|uc%jw1N= z;TjBQOWP7_Fd2S;<)HKCZXU9q0|d{R1kaiOBs>HEa6L95JgcsR;Y1Xqr(ykl0|d~c zbMQA~Is#*x(8F>}W z?!SrRAbHJ|{ht92F29DIBXvAdjYP#K7K}GC%H#4BJ)|oES=W)`WJ;|h1c`@{MfD)* z1Cm>&nMTH5gjgNs+JS!*M{D`DJIIufBUmzyHE=zyj&x@*cC*YH9Q(w%t+HP9l$;w- z*8x|u4>B}yzBeedIZq8#-ZPy<(YDcF(H&q#54k*Z9zp9w`sym`)jCONfDRoQD06;> zk?}5($d0!1YwLJ7_L<$-=R}Jz!5-+w0mlJx?m2zFH@5`^fa<#v>OsO`w#;i**WFJ& zkVO}qMMaMQNFLLbCICqdr-v&5CjWnd`!4$!H$&59t$)t&Nwd?!NPvIyMG3gZ{Ea0 z;ph`K%Lo)?VnL}Au(N1cUL%RS-b?UwV%0RHShY*#V)YFW0_IL;XE=991N=II9&rZj zr-z_gvO!NS1&rl3;1MLz*j7Pr=wUde-3dZcZd)=ftj|6*c zD1NOkte7Yw%KLI6tBSGqFKerb>D$bMKLKryWg2>&_JB-VZ-%z@ETQI&&XV*}`ohYn%k} zeXoZ`@=)of#!=|@b!9s#PVCm*6J2^~dhpRhDQIRI5K{o{@dO{;Gb;FK6a3!~|67pU zumf8k9p{Yih42WM5H0VMbsH|8S%zJOPuVgWsMF`?pwDzg*at~j5ZotEayRj4 zCPWVaNpTk(nXtG*gpkSgct0BZa*|4J9B<1e9CQgGeObP^@!TXJO7xiy>5Vxe|8ul6#8Ji}Et17tc`ZgwzDFQYHem2Q96X6HgrtAnw!fJce zWM7R^D|dH_mp0@Jb-q4Qf5NUXG&S$sas9u=>AeyS)YAQwEF5JY$-<@LnJnMwj0fG0 zCb0@#AI%0TaFrVTWVE`@$&NYEO3qPd!erf1__}D76+gQ?Ups+6$nAD7j^}(2)(2f} z_e;j)k-+`R5xY!ZjFV?exs6h=f3y^Dlv?|x%6`))H^ z(a&|N79w)4Bc-a@_?S16Jz;;~TijFbC;hQp!5lp8BDAvKCQrKCF0*zZH04Rx1fyeR z_CfITu|QxV>Vs3OO=SymV!-s6@nZ^ub?K1x#rC20(8c=6vs|8AU;%xK7WWjPZ>Ev| zXHJTCsX9~wP&&*n=|6X~ZnwDcBEkg7#xYVngjiANW#e3)GSnu%M4yBsGIOZx11t&o z&pXk$ZLog~hY7FiJiE*8@;reG$6uOH3P3LW8M_tpyeH#bPYkz1Rh(I(~*GSQ4^vrNJNW< zl-MHUg~@^(pvO`Hp0fUQimczChCX=7XKrUQfA9GjP*#(x-Ruj1#Ah7D`*EZu^cPiI zT_#8sp?jv0Mo(#IdO??Sj=EW+D*-^^jFYvg`jVUuNP3UV`~`afrVUbSFJKTwIP50M z>Ii;Pgb$o72C=I-Ie0<%tbpU?{b3goXk>Pp5Ndceub?;Z8Cd01?>>% zFm%YcduT=I)wKmCxQ$OIZx{x`t!R!xE1EFFrZGZjFHMEXh74b-FxkR;Fo!PBFL3sc z;q4i)$lsX#p>JPr^Vx3)I(HY%2s#!90Kl53+&~z@s=Gf;;@M@$ii@P8SYZn)tf~&v zf)_dtIj2eV7x#bHiZN!!H6WLsM+0HT?@8v01+{L;pDBFB_Pl z<$!S8@G+d`;`3seWh|E%K9jdX2YG6*B(#b33zl1fOlX6w@dn7qATF~u$OqZ*(N7lI zNOs$C&iC(9S3vOIf^JE%{st)+7P&DRG=Z}m&Xiz}h&r1aa2aFf6!tnySlQc=&!&&PO9t)K-_yf(AlN% zJ85@iPvH#^xUsUQKzC~UMtVJY}<>-I?M=u)1PmG zKAVVsNQS(;G%5!zT0;sD<^DEt<)195T-)Vj@!*MZL|_X|C~8Dv6=}qQq8h4g%Rva>Yw(5Fgw7pOma4aDxp*sm~_2C)ZDK@2rT^V%x=Q8uOIrpm@9?91ZAH~6D> z!A8GTYv_X2TGR?dPB+pjjEoxgQVCiF2<+BZVNd+c-g(_YB=o#@i}k_9fiQZW(X029 zW1U0zkl_fRb4D~1^=gBXvd@ir&3CWAt$Ya?Dk%LvWm=pU(0WqDwE-i|pJw4Q+NOmrHY-yN&7!AM zXbmfQMuI?SRwjN=e4<*eHamcorHgB@vJ_qnKA>SgvjXpEhzsVInfGBxFUyqmnNy2^ zaRyfCmjmN z`kM^4^+O(-2%)P`sQ1V4wdP9bz)J`!wyO3@1X}^rVeyX9-c~UnR@Kq?kGoh`c-t@W z=3uYlk1L{bmgL6Igl`(jA8YlQnbR~ol<{4q>$ollbkmSF9!-4jW2^Vz zN}+aP4t;m1`k0d&ZI;Nt@W!TTEm*qhBr0RjO$FL{J!GSy4HaOH+)b)}SNsKvN0&+U zcvxV+R-(|&j(1@zn#W)cTaBFzks$+oeuAF8Yh)O7^^x%hMpq%EYvyG7(C%FU-J8D= z@sNHWK#cHXbSyMFlM+{BA|qgiG~D*YKmjbP{w82jb{0=kLt17cXuOU7>~~nkq25t= z?7cP%y=ZD{aNOgxKjDH1711&cX(d@RfxL%@)7nSYkoaSWLFu$QOxZ!g9r9tw^)t*Y%0K!({L1Vs@Z&~7eJ~CTe|>2C6wmz0xZ)I8&f^g#z zzQ(Y3fT{HnZ@xiYP4|k?42b^KjQU1F+NqQYRycl)&6a@rVrMN!N{dc zgNhm7Mb8eC{%YX_1+XO<`f5bhd4XxG_Y=}yV*9!6<-m$5Uag$#Q1A=T4~RF6)*1*$KvIlS>cn}sP& zsz;-PcyWN!EJ2m*o5pSd(p22I7saMXS2L-TpC)XQqs3toWjhgmnrhqcGG5zoC2lQ( zO1%6#Bugo zv9TEguG&tg)(}INth*eCy^{y!hsF8@bNk^8{Yv~^!&lmkG0;3-kY`F z4AFn}Wu}}`^oPZ@+Y#r&Id`kpJKB3=^aXgGD++5#GdxZ4vP*+9LpH9P@ z>>Xrqr(YDSkzSI-*SbaREeORMp*Jb$c45VWK?n$Yobg7dM}jHXjo@)Q8pY=ol5wUX zNqQ>kc|Jfr1M0wcm-hDa04TvbmO913Cis5(ZR-(0e&;nLvRexs+%W|p5l(dvHm;{4 z_?eCsGMcUJASFZksGa~K0!Uz>iMkr z$KcTbJkpInQ2qT`in8|)Kb^-!^BjKR7V@5Q(jsv1?3$|t}H;R-V> zX*)-ikPhNDQd{@@9=nBj%exM!({ZmMH6t6G>XmZ$|*A{)49pFZp^V(|j;^J#9h2kGVtYq~C<2j*F+yXl@Aifrp#6O_i z-s+g~ob{Wjea3UP;i>l3q{9s;;y(fJh*f9to~(=4-zc&C7tz9DyxA9xyobctE?g); zCmc_qbt~(eF##Q2(MCnJ5XIl+^^nOz6(;E+T4Ot_5nh7t$Ety5-Pdza91lNy7r8Y_ zWGaMZZSl=fjm-sShPxm`R7U*U3Rv$_PP9*{t zuD~@@z2|Dq#7T|SO%NOnNm=GLhG5Dy3D>cbUD2g9MIWb#8}CZNH%=#4lm((fakP|V z!)a($J)QW{sT%n{PQ{$=L}BpU^C~+;z5}!%n(8Vfh!`s3Yk!x_{J1e>zVN*H<^V0; zi7cp{v&yVX>$0XE5dDkDE<$zoHG^{@0C1wC-xToiv4_kU_rtPU3n-7^tmHN0`4ub_ z{}?Jv>GqD0YMP_t<4usBm~z0BYlDi=Op8qWB1JWfskmSmnzg4B1t=gT^Um-y^K5v$ zrc10r5l$vemK3M9aL%4zD$u6Ju1TcOE$zZvp6ES8<;Q00J1-Fs&{UddEjFkMZFyY^ zI~0CsmJR6GlTv(U2RacOL`hQo-GQwi5)G6JtUBg>3zb43hhBl5m|>(%j0gSAWw-E+ zVD;V%b(&eUZM1Nt?Hnz%3eoG}Bfuhh1^mjSs3~a$D-s}a8g&(2@%U_gjoDcfLL4q8 zpk&>T4H1YI&gCG&?h6eO2VIs}2ln2*XaZ7mSWS!yyU(b{Jp&a|{rXy35!{x6tW))> zfm+Mdpv>CCwz-I}KQN)9`!P%O`!qzOkmjSh3J(=Q_0Tu8%5t*&Wn!+C{K%nasR#>k& zS{HkxkpB?)dDGZKJDnI|H~U&;egvW9IL3}jqH8y9i&3~Fp)qZ58p>jQ56?fC9avj9 zfPt*)I>=#l(rXZ`u2wun7Kezu(v3`NsXzdF4Nvr>6=BuQ?Q$BY=g^$w=>cFOxO^`l zY*kMx92#Z*#Bm`H9v46_iCYU6054Fo_5uV;7Fa19qp+RAeH0!^VQ$pJaoE>H2f`0X z@i6~IINcV&!NcGCw%VV=7q~h#9#S1WzB7?j614its(mHMsmFZFD|Qi8XOcVSF(Fp9 zB#|%r+KIyl33C)aI~zA>FThaM6&P0vj0+A=TH`CxdAENYToUO>-I`J6Bg z$`hQje=`Ls`?h5K)i`o=;|g3G@Pj)Tevqsm&8@98a+v3eYG-} zLNtAVX!^DR{;JVFZZr`5oS1L%bq;w75&dXUM`kU<71lU-BU{miB0H2I4bNXpbRF5j z1pl@*e?c+xzGQ`v7nT{cNyh6`bQHcdzn1|CqSp%Pl6bd`x>!5!Ips>bC zM6O^7B0qgLpz7#YH7TNxWp2iTFdv5&5ZQ!pRc*s* zq~~wKU0u)prmV+dM;|Bs_q+hUQgD)=aFf|mI~=ZJ-D+$|tkba_S=Trf^I4*u`dLHT zG3j=|m#U{}fGs#E8vg-vK116%iD2eEj$^LoW5(1H&Xf2^{~P{I>(6;5_(n1Pa)$Uq4@J#dzgdL4T2;*P*BBow~^HyIgg2X%hn=sE<)7SupC zMsTqCYX?ZQTr^)yn%At4?3-k)otG$@d&I>6`EFVJ8QL~jxRLb9*U_G;+x$6A*cLy+ z)ASO@9mIKFw4u!2T79q+rKb#i&8|b_%y|j896= z7Db%bDjO@ve5cT$uvV%2eZ?PuPx}<+T5A{jD;x16>o+)! zg?2J;VyM{(S7BP@_4Wn^YlZC2kv`F>jqUtH%Iv$wP-ltV8^az)Yyq7sWYfU3TWWNOsx!^BZ<4tZR37cOgBU_JBqeXVl0^KPbJCl2@8^2(#jLu6VZvw(WP9l zCwsvEYy&Cp3zp^IGv=N{m6(e5i>Ch}#|PrP577L{(e_WG8A<0A`5$WUW$n0-KPBwq zwvZaF;&?}_dWp7Q6M26Lin{{ad6$D)=M9D$m)cJ_JQd z0rAYFcqYWNlHyqq&rXVGLp&!bo&)iTN%4sgpPUq*4Dl&R@hK3`Pm1S5d}>mBD#Qzt z;sx9TAi2;30(7pz9t0L*7gr5;LWomOcU1Hu!;H@23dPlpg((MolU05RSsa>`LVxX+ zbV=Wgv=oq|6*J&4jb5FAKNk0YW-&kM`7czIZnTT9P=(@}2DiTrD06OVVi)(IjaE?6 zzfyLq=tsu^({VA0RrBp=)Vekfv|zvQKxDfu*&c?zi_QVlY9d=$i>_(MK~m)301ekFaOTm~qCbm%)*{M7VE7iI z{1@Y9Fgz`Q*~oIYQJ|7bp@|~qM7T-0%e&HOdV*Sgc zgu_h&kJUcu8;VnLl3cuM@%i;+jbpdjD}Z$^(EM6&rl_G-U0#>Uy1N<#Z!YWZJ|Y`- zhuagFhk8^A|01?hm`A{u_MK>$m2|Ua#9fX?v8o6q9s1F|Uv5m&0{v)k6Hl_-9toyJ z%{|h6c!rU2rEnl`h`y+7WQ=ut+Mtn)dlKHFWg}}eDk!pyR}>>7W%y^s$VxRoDy>*;GEKID+CTauyOilTG&GMKYtp4r{0T zzQxy(lbsuSp8=yM>l=&8`ssH;Sueoof7Gngf~pGu&^|5H=hT9Iq95%I0X0je*CYEa z=Ie~^+Ymp%x^s^>y2VxHePqWZdj}ukQdIBRr=3h1p~{Y!@|9gUj++;})knXsw=Vn- z`#^)E8XD0W`&1Ya*M~=UEe9@gd!rt{UtyP$3rR!=Xwl18o7;& z^%f^N^{c-R<>k{M;VHWlbqvtbJt}p%m|+_GDNIQi`0+GoaeIQ=FpcW4^(ho1lX=T- zLAnP>8rZ8yFGgOo5xtT`p+p|)MjJM0>SsOPAp_RPHghjaGfPL7;42Gh`y$H(``HQ@ zb;E2U?FF<|fO8R+aaP_zHAxu_>LC)rZ->#|i~N;XMqUvivKu3Reuzg-$xg#wby%;qMe&~`#eW(t)RFLYzkBzNSm?r8NIVq5R%v`PZ+6+?Y^uzgX~&X>F(>|cl1jE>$tN#jbv; z8WTSwomI4Tb>em!+=9$BO#c*%oyWXU41JDe+o&t;`1vr#&trdKUt;Oj+cDgN1w)f59K!$VmM|fmN2HaZP0+)gF65L`*vb#9fTeaz@JBf33~^Al|3fT#c-pGTQb9NlZ$)a z!SFs8_pXEC{Vwj^2Ezwj+{=tPk1Vd}_ZBF5foK-Jck1hr-z%QQ(u&9|N=U3)cP<`x zdr+I0;K+@6otx)bP>t5dh-%|1D*NVn7FDAGvi02PS2TT|$5oB~{OAPXnknxqLMxU< z17zGM5|qV)6O)ujFhdT^|6x=2h3a-yXD2k8F@F|(|A^;*2R~*Gczd8WwBB^lkCJ^@ z7kV@#i(HAu&?q3CS4j<`Ys9tb1Ecu>^Pxt|`gQ$7@}W}AT5=YY=o}0vqZ(6RA4uJY z#2YszS5jnJkt-P~p{>kOuy|@`=~=%+Lvjk+2UR@t6UG1jvqchl{ESXZ@<+g{DBVSG zF)_2E(yh_Ax%`BZ`aacJ{;Ulegvw^I1=WRowwh_Cf<2?8cu;EXkxXx3&vc&XJ@P8iO72w4{`Y+L%7@0F4YEA4P7_ff$HoV0eet(7D37qU*Cp)B~ z5u$!sUpUk-l&sg|1&t|NB55(JcRXqg`p*(K@*=)nMXz*vUNm1vU!>Pq(T~^hPEUw@ zXKn9{)4y_cLQoZ&U5pO#0mHo9T9C|w3IopBD8M_nQ@7K_;)~|R;!1w8xWc?xbg?tx zw%bv@hK^refxebXesFt|{UH~N03IQeiMo;={#)|5yk<)KAP-s!t+>w=c!9=NP@`FVxBqefGS_?*QA-!tM)`Y(twsV@e_+?o0l9JA4gv*lM(< z(bUql$sezTk5>A^ziV$J2&^S{NV}Rx<8gPh9kO$Ik?sNYjSd*^j2io-ggI=(EjTOp zI=;pOcQ0&$5@1=i0N#6Uq3*ZzK8JV+A~&5S-Fz;CG@GH5(hlYK2cKt3MV1_QtW**YN{r+lGpgXW?x#(@zq--n^4;?af?suqvB|pNd)HX9+R$Z{C-j@kZ+DDZUyOCO4EqU%~_H zz*06_$pgCLwx2+*VfHFvMk%LL_wF66p)nU%M%7LG3Fvzg3&gRfN$bFYz&eeE&o_KzM*&h~4{hS%*w_v1uW+cPz3H$u5}$>nZ`a*@9SlNJ8Y zTa$zizeV34(e}{S2BR$9X>7rb;WSpz8(=eChXnzHlI?%i5Z?WLL_J$t2o^8m8gm(s zZ*sA-;au2UF2j@UGJabK2%cXjt$c_Cs8F0W{MaOvk*`xp>}IsoxB{1t9noVQ1+|#1 zJPU9cT{)vt7n6`H9RI%6_#lWKJ+aUNMT1gJ$pQFX% z(snESdC!V0LgfG8-}m9~)Jc&4(&xZgE}32nEPT4^==Z&>I$FKC^xiJkzcm+|6YERd zn{vCwjj`ZatI%D{O5C+-Jg7SM`Hq?3fz6NdWMTDt2--R<<@WpyM>^Zcc$`+B;oB76 zelvlO--PBs0hyiIad!;n{Few=eG9k59_MLuFs-7_+?x$_Le?^UMHgACxb-?^n--0Kts6oVz=XHf|%E8m_w+|)yEx>V=Q2`%D!v|Pn*nf-6-p0GZ zZ|d76+cXG2lQXzs=96Sh%r3#|cj;HSp%X4qrr&KhA~4@Z`FTh>s_G-|C?WL}`qg?; z<{D}5qUfeuRNirlasygFc?gE^evy%Ot!!j8&oByp2?(W26*^_sh&20nnH`(KvVMo8 zy3piuG9+{1< z-J-a*LD(m*Y#?(V+*U3W%nGQ&G1YdR0%h6S$yBB3d6OhEZ=MVvqL&xf&S3K93KYPc zMvi;D#WO-xn~~n)huc{owRqc}96Y4P`3{f`;lcY-7Q+dOr^?1N0zL#7`Ds8Q{cm2H z7oEt9dfR8Rom6*}$@-tMQ;PqTY2a5}X(2fXd^x{0**^~1gGAep(8udl`V)r~A7 zef_vv!W(WKZisWdTDb>TRPyfm>4an%_o4cV5<4Ohaj1y(XSfztH5t&a=f?NX1y7{Q zMh=sLeQ3m`!?6`Hlkc*;fb4iX9fIEPsm9wVh$JoknsiKAZ(ehF7IZAH3pO`qksWVR zq4dHnf;{atB#6bTYlXz&ZU zM&C^(9;D1=c1~-yYdvYcu~6Tu%M;3LX%KrEV_X%6`&`fiM8o!{dqw!022j(-lt88^ zkhEho42;2Msj4N*NgWVLoRCH*qyOx&rZIWv<&{kJu_+fsx z_#q``pcjx8sF{do`iWxka8V|FM3pf%CmTZP6w1K@S7B%(gjy&x1wt}?&4*A2sm!!4 zZo{?1=Bq%Skk0FTm)4W+OM_8JT3HsHjBEDFLQH1AzH>NazD<6^#mGRriz(~SQ!)3v zHyp8QWwOP4r4;fflqn&&!7&j0V!FvbhWrU#OngydU(xyH4|s1JqFN&QZCB&u4`Ilh zh9BW-G{a4%j`;Cn>1~6#ykZ0F_k^IsjOVmH($8ilJF)Ro{1y{YkW&hEKVie{6qmZhSS_Ar6aWa2+F0SZZID1xKuG83+@-rkBH>2JVN&TZtVET0= z6X?_xEP`Eg=?`v_ut)lwOlhm2OfyJR_%g{s-IK(%AxXDM%#RgIj1qy#u8@A+^oXSY zHib2zHZ>ke*syTx=Kpp~Xuyro<_>n)@hOVh2s@ksC~=|G+-0Y%@OQ2A}6 z^Wo7ANawR?G`o0Ee}4~l(&<@-LigBbr{hBDQvFQ=7dsxlHitk(m7_^zCGeG4UX(7g zW!ce&ZU0J#N|%v(NV*C=fVZUEEB|K!~W7K~yYP7Y8t5P+l)2kGnv>_Hk58Wdki~M~m-M znbB$F?{cxnUl&)REeE%;xoSMuqPUIUpXnOyd+pFw#QyTREV4T*8~I%B6%F()!i|19 zwM&BUGBS3hSK!icF8+v`gA`TRt=DGHGp~z2mRTg+l8rl&KObsuPbIzhwE;OL_PU_z z3t#tY+78qNw}>mzKVM#e=>L#?1#MgKA_Qs) zUq!X7<8>~`q}T=?TQhz@{tIGNJeI=CX*_n+>?eD z*7dcUxWC_4bTLa(5B57vcPN0Ia+~5Irc|H+=LkG$O4msG`Yv{}>smF5PtR&}|kB|K=)k&s8(q$bXEq@wh)4 z(`P~op5U-?boVwPh$~dGt;bPFjDDDVo6}=U0vOO8(`dlr^A<2|-5vfrf z#Q1EZAlKp8_x^ z<|y)Fe|)%q7jw2zQuzd`4euliZsho9#}hn^jEGeKg#(X)XyLH(wETD-C;{y`r&jv0 zF!~l&HDY3N(0HWkI4>II0;&O)9hS76YjN*G?F%=ERcQGcch9Y`Z|_!3=_Dgx!V!jl zon<4T3M_+tK)=NPHt$a)Q{`&sTEr9%_VRmDlqRX%%Yl#7NaF?QTHN}5E)UzWOe;i}R4M~n5HJAO!qKB>Eja*Md} zFi^m0kWJ{yJ@1eEZi6UdDjP*&jXEJm9jf|yQLt$kP zyPW)~H=$UIV_*4?Dtj?yTg~NYDxd|IU33ENe@en!mAx1$zYNI1dHZ-lNqg3A75y(G zA>$!rIR{PkCKhO#o_ik&0ybO!|RDC%W3R*eWdIK6D~-5$A!m^x|y`VS$k&xneE z8p70km4Z=7#g8yex-T>Q^wl<#z@KCHH6+A-xQ4baoN7~j_Zk_6 zXmM_FFM2h1v0j`;XD~iN5{^WFmE;cv=!DD!);T*hf!*Cd=!-+0k!P&jVZ&g5SxD=( z`6l|~V$~}ncswrnGNC!yC@7S)kj>vFi_Ttjr2*@6+Abt73dJ>zlJ?ttpywJ2v(IZi z0zNk>8(3rXrlI&q_XV`lJ(jYxm%Dc%JtF0X^4eh4%#p<{p95BSJ^cGCZ9r5b38`%( zR!)31C^X|IQ&Ix<+d75C6nt8I{Ppl`z^*V9gd9`0A@Ja6EikR9KrmsXM~c(zX+3}f zit&lX^h8e_oUUw(K8N(Coc#&(!}sHaB?l)+lq_@kCUCk#h>%J{hZJ3C79dy zqCXo7PmQz>Y`$A@!pboLX6qWNVwSPwaXwj}OTkN4e*W_L0tS7LIbXX;BJMCMHIJB~ zdp8G=y}O<;Ts+*LVdKQSrvk^7*Nn>Ul0U^+B$2>g9DW4P#5=Zp4Szg;;z9EE2?hqW|`bsOmu5 zOijkbCaF-EfzKiXIFyj4=wlrE%NWq> zDWdjgD>=ztN;i0fgmdE!#B4xKC1Qg1`uX?FRT#OwkJu*xa3H`HIbGV>-s@pY*ah!s zNGw1-2JaYi1)7I1kLW4E6E?|a;OPgb7!K9>S*WDyX;T-s-S%JW&!QB1@C=-(y|)PG zg?;`u+92ZG*2>;=fc$p+Bf489ww71Hc_p#F70P?jNpbM=bgf~G%n`HbJ~ef+sGo5C^xxSr<&V@)Pqc(FYPPgk85Q*x*-kWpM%BX z5+Hla?Wk3r0{pb^{ek@=ZcSTJ%+h9%?cqjWr%QkIbs*w$GO=mj_c2IF*>@9NQ^DkL zA@$A3|Ac=r8W&w@jX7Ds9zB`F9wBe^MLOo7uM*oU39sZ3XQV%$gT$57QOPz~}HEy1BF!zwE=*BPd^OIZA)STy*vrus~-&L~klWv3TiYbfN+* z(379Smy_@X>UjE7{PG2Uc@Ih+gEsniW1u-v?a&pn6ov@^IVbw(<17Td`NQ}NwBNf* ztoN?6%0i>WI;GaIc3OaNG0UG#EK`&3*c>B&@vwDpYAnLb<|h0P8!nS>LUHTS^75t< z)OL5`GC2T91K|l+;+X_K8HB#Mny}JUP^ly|Dy-WiwD9%yde~unjjhE>v{>e{tMCa8 zs&6-UK9r5z*NHVh6*r!>%7#5p(gIe=5i5C5HfBIV+7Hp)i{c1k_7W*c+J6=NHJ-Mr zo-Ra9uri*OjoDVWr`g3tCFdDwv)x99KdHU>4woQ`v3|L1C^H zP@8=z0OqF%816MEZLT5&rPc`4m9~^J+J(0;A-Bpm7gV!TFEo56WVWFuly5wel&%2rKdAUAu2<^;SLNn9oXT`w% z?h5ZfXyAT##fJl-f&1MRvKcZJzWM!b3*PTG`%MmZmJSp-QO=86E=SEAshiK-uruhnUp* zhJ1^{Lc>K|jIFigS_8e~wYiMRFF7ms;-#X$*(J(n@UaHEqFe`m+}Fci^^Mi~t);1| ze#K9q1JTi1V?TtCJ%A5&$N3FO*GQ5!^pV_?%t~MJVfFxIiRKpu`fY_`T^9^y;dN^8 zq)oM5@weo{+o|vtpG7-83Q8?a{OB|ypJjZ4u3koqc-g; z{|bsYflBnOz*0^}sN@)SEyby?K--tllKZ*=$+UF3)wJE2Ind%HEUv%Ev(~v&2Bgv+ zPQedU2T7%gE78J$g4FBHr+*G3bt64FTdZFI1#EXtfg0h9Yk zcz1ctrkvWr()-QK{r1wo7FP84mH&81&3RqCbSbY^o}OBV-lcOJqpuC)FB>Mc(0 zk<&h~rYvPYAH^LL>&XslRmpbh8G8l_W!Q(Ws`s?gS1cTe6QGI9lbbkgqqboW zu@&pg4&DfvR^FGCDQFumPVfy$oHLf;_VR}B?`MWQvMOwk6zsE0T>;T6O7T5TR7E4^ z!7c#6Nv6VOq>*=dC;QY*;=>9%fkwp@|0$J~u9MlLf8tuuGIx;9d}~x@aK-I6kbo|` zR@Gu7q#a~@MRd1lv9$6Z0|ykTz`JD69(<26?EXP5{h-R++f=qc@lfM_9U>9DdvV71 zIfI{Do$&@2YgbWK(a2wK;oHj1qT&sfnc|kT?Py~vSUf}gX|}xGE|`H7s~Jc`*K=6` z$m!ZHaCh7I!L@yaUw@hg1v4<(irJdY*jTE+jq=+t z|NHb|(e_b-8Mw@921KgA4fEsObskc0K)F5q3F>t%B*D2f{QE^Ge1QMIItL#vk}O^C z9r(9jgxo;6E!hJf+l$4=QTM2P*%bVXZI}NRHDk(uosM_DPHlIg3*8VN!Fx)_2i_Zt zYoQ}g*z*V=e5gfYxxCrp1UvxzKuHJAfv#-&CnkvGchLTbs*hNC4{VPuDa^e=+@cB$ zjlZi_{@^BVDYP-a7w+P7!`<2BtI;yw2RtvoG!auVvW&jhtbQ1 z{b!1!LtJU2x?-$HVi$^x{3()AkfxJs1xH8{Ukw2<_i8{A=XJ>`F`rFhF{pbAonP{_ z&0gYxN-N4o1GlwpCd;qSi%Y_L;+B9cgi~W;)u-ka%}kcD4WZyf^0dD6kly{L9?God>R;hAqNkB)O7~-zkg%8}UJ^IS9_U=%G9{4H9fnQbI4aD_P?^e#o7rcV5Jj*YG%AkgVSKY!9 zNh^89j!sg`_dp&7Nu-K$}3)i1ep$6Zh8lDq6Sw@T9!0*ETBhAQ&m7*^(@U)Mb8~2Bgp&+e%F;lsYiioy3+f^ z#8xT*VPi%LQ0?qM?%~Qt)FNv=<7hG#T$v(nX%=1kDta!Jj@aG@6;$-(Kvg+rRbpMM zqos6~(3aaES+A_sWn*eC`8?-OVS@9gI_JaZgGozRbQ|p9q8?=E(K4g2#qKa ze^tLEbV|XF6b#2@0qgQ+slpLrq0t^#UH(<@S?DBtV6DEh!~D?=*pJ@9jVFgM``Vf; zNovF2koHhQe3TszZ|||vuK=41I?wG$51F5P<`W0KgLwI_@U{yG?LVSKT)o9oU~sYJsyXMQ^N1nJMf>|FW)YADaT%BeM^qYd`|lpIZ>AEi=;E${u}uFwv`NlFWP zxZkxiN8LXmQ|8 z3%QaFo*b2XM2L;30B!)=^2eW@v>GZ_pN!vjX|O1Tji;dpEY?+Cp+?Onpt9fH9o4S z+n@OCA}L8EoF5yNv(w;20IKtp=@v?VDVy$als&RBIYp}6iy2g-v=$Ynd|g(&1IQdt zZ*p@4t|Ys0JYG%O$~}s{0t>s0M>nB)JoC|l^@cCOPM7o-$(M?;i%XTA4&Ms3IXzTF zdP^WXari2ns8Zb696WKESlvzhTuafC%+f>n zlr#x=(n}{$p$n4^Q_|OvbiPLS0bk{(19o;9zs@#c zD(Nc`t8x{La3SDXrRtQoFagN#oR;F)z_gfE&=uS1@TIS;mgp5u51%oi&D4GV&Dn1f zM%tcdk_n?_X9-acmv^rr>ek?PVu00Q?sY0#_&OSKQCVrNtUsPTWW_o?dy#(3I~Gz< zCfgEHHsS7yW}5HC=xa^8Xv1=1)${ZsrvTS7Rq=nRvh@Vwl@lp>gku&yz1feNf~}CU zapph@w?r}Gy1_hL_+iB5WcSgYY07Jc6ymG_g!@CvPU-;)8u3ShXeRo9djyz{3}<|g z#11;+?}}BZ!(YEJ6Qwxg;r+1GwZOY%G}70k8VmQpYyu+hbh7v3G#_`OLF72SC&ozv zBfc3s7ycp4CQ9Mx%1)coL`;~d2AQ%Gh9;fKD2R+(>;%u3&ic#vU*Nq{`ST-J&!|0TgJh;^urce_H6hC?sn)B-69`b0$n zMK$ub!0(jc=~2ZJdn%Tc=$#^%sl7u|t!8T9kW`zQ+CL=K&QqtL`A01<8p)sNUa5lAFYX8!BD_L<2; zZSVbm{-4kP&xg!8XPwCQ<0s4o3>u1x}I`{=K#_R{WKel=W*h z8G=$RVOw*0K!8i6a!}0G(M$&?bGXWz?aYPOYcgP^8vGFScw-!8sn4-t|Him1?`8Yp z4Lod<4=7VDf&bs=o61nRL&g0cSzMyFc%rtre7%W}YjlPq>GZ`$dKaiiAmlKL{b{%< z60b3~>0pED@md3BTh6i$YY4$aAe(v8o~m#pby;YC7MOLW2uzl#hr`j-Wwo3d%@k%9 zr79sRN(69j!R_iZ;4WtbLY}gB% zs8pt1flNtuT4!?*FA$-QTjl4leq`&bj&zxTe|cA}`4g$zP3?J&T2D{7-9dd9m$jxd zBhHVDupz40Retzlh7k{GuB*l8gqZ<+3IPEb{pMJi#niBSU3Epb%SJgyG|D%uQO{Dw zuj;PLMcKw)glJMmgSJG^jpetgELHk5H3Z1dASJOy-Y zv)>1*`!OHAIqajx*icxg(elqE4a=|%9`T_^#0~$iG2n)O6R^R^-Dg-YzQV;Wt~ab% z^Nfwv{Agv~isdmw9E$uh0#3Lojn{ad$Jai8cc3>_Ex|m3_mPDhf=wO(!f|w>e)W;X zmmM5nfW79;a5IZgE@IV#3SVM~eeSz|GntQi#dUGQYksdjatD199aYR{{`bXnbW$?9s0c*x;dCWdC`>d@VP8zNUH}t`f4fgRe`1?5Be9S3oNxAX7z7dC+IQaXp^<$-8 z6K!r~00H3VvL+QST!G;4KTZUHZ}sF>cm-lthlhf{|Cocn`}`ylxA}6re9H#5e0C`K zyYe?fy03lK4(AU4dSaX|9+UChN9z|Kk$2Ld5FPM{?h#^*nlVnz4d(Z<+ocR#ta%wY z+S0>9$I-9gL}~d-r&le1W-K-}C%_?J8vNK*L(SAl%%4P`!POna-g-*donj|cQ9=uQ z!P{qhUn~y3)B`48Y-J&j^gCE=)yu@KTlmVZ28*}ul!@CzG7;{@inq7-*!GOt#({P= z9>uR)^>!I%88_P6y^wpewUfJW{inVHU-zAE@ouE_BFUkmgIH{;y7?DlQ&P;W=(!dc z81woVmh-&_i16js1VWBLZ#tqr(CgJC=Q4zJ*oSOIZ=z=NjzVFwB*R$4cKtFifK-Yt|ZF?4BiS%IzaXqJqHI zOEO-tuoq`U;0|xaR)!qiyet8;Vmm{R63QfS68j}f-f^1?;ka&jem(u(cgH?OGoR^3 zLLDiscN~WHF|{5)wNwlZQMYaBv?7W@St;S(47k4xmxyp)0~+V~$>`+6 z`YjQJtE;4|jXjAba>L=dvY9fk?uM_r6L#(@JJ%uUp$_f@)n-zyR$oP~%WiXJr)Dl3 zEO1jxTIiQravCjJ5#okaIe^Fuxwe%xKPOnK)#d$>1eNcD0@{h`=O+#!tQS5fp zS$x>caIQmX2T?>#!fPNy0X_tqYw6|8sUhS$fPC;Sm~>yLuBbqx(8gv$5$P^cmRFN3 zkA0gqv!}mz*vVrCJOdu!j_@t_iT#BhtZE-!qPLN+8nb-N|PQOVcy7n zUUgzjfw~66-(SOzyfSh9ZyeH2{L5IUyz{0d_M@wg2B*3d8QaI;n-JeBoHfijfG{h< z^z(fH_aA{KPt?Gx9y4Q6`4bB^2KZhWOy2q+(OTGcmkD9N=y%guLb(XsL$bq6nsdlJC4A5*YL0G_K7~Py5o>^ z4%lG$pXsHy{ct#K_!ka(L<_yGfD2?z>2-6A^)VVrvkkr4#$KEM?v?zvm-#O}a@a>V z7C_V;T@ZggqQR%`IE*^H{uH|1FAMC`l+OP_nd;`x!wi?pjoulz{pD7`fMIsE2;k44H^%I(QuI|BiLSw2?aDWA8aenWJF+cl{FL?F#*Q9BvWP z&0BbV+U6&7NwRrB3!h4JD@}97NlLaxCK55A(?pMVV|p} zXy3k%%EvQ3qGyC8;`Q&~n4O}XWVKhP@D{_FPLHVc`cx-Nw4EKon>75IyZHTlJIO@4hvxPM~2qM=^~XI(djLyvCkXRO-5(OIKw zrUq4S1|s3q+>tVtHT0-@Xw@Ljd=63=%vX1 zHV4k>g}kEOXZ}|<)F`tQV@PU`SDZiqS-b!4gRXjc1TBPsR90aS7VX%f_EOg(Z=QDW zo;LP@NvE_f)H|N>u*n$FI%Bkm+!t6|>!2CR?lT-9;CsMExl2gekk7t7i&CEH$ zPiwO$^HZ-B03@qnd4Rp2C-@S(6eoc|3IWZ>q#$53p(@GFn!_HyzB}LJU$on6C>0*H zd~G(UEYoYe((%fWuk`HC(yI#SR`z^@IIzA>=~e29;>JS_yLl~i(i)+;9ewxZyapx%e`UHK@l?KKS`2QY`AG$;buSADMyYnS-EQGUfj3^1l zD-YbtBtec5!yTs{a$0PdGL*weX}8A@{i@*6ir6E8@vG&OaKVtwr8b{R0Ggq>pZ+QV zt}K9SRsvj^32-4b+ehzbdX#zw^35JQ3Ikr{;MELzGRWzHRYpn#W(bd%B?< zrLPucf0_zB3Pw8|{)cqsuu`q`D_6q(V?Kf2shfXKz?U@d|H3eA;Td@g%~Ny3BuQSj z7zn8%+}rYMy`rd87mw_sQAv)6Tc{Wh)3=jntk*0mRjs4E1h8%ckBrw}BYZp6Jq^^# z#oJgO5cNj5J&O-8hkK_K%z;~RD}%2~trWgKZcUPN#hET)?sUr0aLQR#=uCUi@3pcB zHjA8a-`mjBn7Vk2sT?k*iAw1?ksFoKNj)g{u$654tX(7D#w%~Cl zz4aDO!0s0wXFQ0v2!BFv7g^IEL{SqlYzzErt!ebQs1U(hjy*_^IaJiz!W*fmIJbPQ zm4@W(1t=0NI}9m=<7gW6x!YXW<0aVIntwD7C1SRltNQw*sCQ(v+|k?i=&hJpICejwrKkKC(;QoIT|9 z_wRuyCZC^U}-}>ALwe;oREn zh;A;(#93)QyECt? zo_m>UGnV!J_v1*sMCHYFaaqr;#hKe=ZJv$c2lKhoI_U%ZvM%bv{o_B8GMr3O26?{K zOW+eX=;E6sS!!uQOV`D}kTmAW9AU;1ed8U+Upqgs#1LJ&xt>Pm;dDLqD|+g~X}UQ- zQ%{|rp__9j>#1`mnQO9BS7(_IO-p?!$9!-`>Vwlg;+i>UR_dIYUXnuQS=slI__@<7 ze0%3uv(gZA1~QFug$>JuuT-HCURN{MnsFD%7GzGPUbvferE`6@2pQ(}FWwFF+f2ns zz3GL!;a%)f(yDLm5m@zIT|ZS9os{#bp`5D{Ic=-?uXE1RK5-G5+bMJYP-frVw$rkh z%siDd$8or8eYW`}8cE8ZF_eF*%r7(dHuIzjkt&dFu1@17pZ;7jZ{O#JDDr=br*pBvejZOz=6^;6B&8L}nM4rTtu@Y0vd(s#<1OyYvSHI#S7@Vv7r zujy_MhJ50z-G(m=Wu1{|!$sTvpWAQ&UZeoN2<29Wa!*X;wqbpyiD8WvXGvH$=O!~p zbBDF!%_eT~7TMw)ZpJG^dCP|9-6HdDLtf5UJd|;B?nszD-jsydbP2Ps4`u$R;icXu zOWi=Fe$iY#lMDXrP~Q88l6Y*Q?JV;(-SB z;6ns)x&Eyj)H8$Xx#x|X)FF?RYZ_o-RjK7xX=-_8!Xi!(&V1R-;nPAh#?d#=5C3L1 zed85ToJ!5Dq|f$4GZmpk-Y0UP;2pO8Uvpk|Ih0F!PkA+}_yta*hwSF)IeZefUgkej zPwmOAj;?cXMD>nTw>S$^GVEEo#h^RyLV=j$EYQUhV`YQaFVRKOK{WWD*1Keb$5N?h z?qXZi`edUm6pU6>%2uq;l`Tf*@Lj`t_qc39$HeM6d}zEwr<(KPRBakn`=bf6THP!< zxc+lIg#NpdpnYa^t+#Mtl^x?z#%a8wa?+_B|3oUMikk5whV_)YhG2c?@YWnXJaT;2 z&mRJ`V|d0_4-Wx4(Z%UWAiw|FVOf8CSoZK{0&w-53>&z4r#n1}o@St@H`+DIUatFW zvX}q%*-^dBJNz-d%tSA5wtKk{y`1X$tn8(e3ULo_QRg9haH>WQ4l-sB&scpZIXIJe zaPCbsc@moZX}h|tq0c{+93<)HroknHakV`2 z$APm`NPSA3wNdm^%^TwyG3C|DzMl875l;EX{Ame-^p?^mwfsr?PxzC*+};seN-Xa@ z1TiPVQ@n`ZxKuM_ZxJMkKyDge{f3`WTTyu4-tC}sQ z?V*Ra==x{tiMS_zDA*=CsJ`EPJ86|W1n-Xj9lQ^p8-n*Q&e`yOH2VKOydSpVJ$V?s zVX67pFo4g1Mdt#7_?Fq~o~4h)3JBusj};K4ANyF4AMe=1AphZLkl&gBInA1-k2!|J z99E-~zfnL?pBG!-A)$U$LVbiko%g8;(~7AD9qac{3#L8&F+*^?<3)Q20x`$h2Sums z;-?C>cXJ}kE=%gw{U*RA+_Y?u~_)fSoAK~qL$Bi zP$&KJfkC(B{0BWZ<|YE#a7hcaD{g2kJz7JY9Sp?ezO<(LuAtu!7HDFI*LFDY7m}4p zwVPxvaEj~91!JC z5jacb^&5!sa>~dZFvL62daOOxZ9K_l!kW4q8%li>b;~)Py4vZ6_YN%lrN9M%J~4(% z=0rJp?o*FXlzV7x2MW|ntqFe*$7Phk4kWhNiG2qOXo{szl&8P?8gQI1Q-%39GGmea zSFXB3cY&_%4~>)lY|7Igrk_O*$LKf2&H;MoS?Lbeq@x6CdM)bRxpf6@k@*d;^?>3P zbufP3|H-}OMV9xl+NJJsW;2M!<;A0j$0rVNJo{H*Ap4*0z-3Xo^ZzjbCL{qsPnp*Z z2f%jM;UNH^|K7Du0AQ`-lK}8T8vwe<{E^pMKma%l092a39HrJu7u(bwBuRZQL$lt@ zegzSPHdZbipo{Ck>zHm@`ST8TdEck2?z5zz_F^G~ne5AJ>4z(BE`gi zrL$4tuA!Hiy7|;}A1M#kx=_MBm1y;eYsma{%B+_ADy?)+sxP#$j~(`Kq+VV1hH>QQ z4fd6+`re_2s^zu%vf3ro5VdR|&@;ta5GPWlHJ#NqhU>?{s%u96#;TN*>r=$#p5v-< zNG+o|`cw9py59hj%5%zPJ||odHlx=wY<|H#gf)Qe+dUNYwy{7IsgvBAHM$bJKh<{jo*K1evbNV!nO{_Hy`~^myP3JV%K3$#P;;NvI zv}#SKIwl6HCy2hL-atG{S7*Z{af4YziqhY?T?Y`l9E{0oNI@^0`MMYni<#U5Q03hU z?#2Kr@YAQ3BW5z2$S(;gnoNO7maPJCp>_avjfBWT|sqLah*yFcl=86eXn()}SFUigtF?RRI#v_SRsL$K)0t0k>XQx_a21fVROkA@#v2}C6JEts5JLzg4! zj(Ws|t|Pknjmd^Qd0GVreucLuPu^PVY7i+iGVY)wVHg{oO(~IE;Cd z2@Zvi6V3?Dn(;5LD{F7@`0EpKr#JS`!}*Tqck?L^jzULx5bm2Od^Pm^ong-hlU2TO zz>$(@N22O#h4Ioub-ebGI=%=i-{-&fzo}#TusWc@ib>V^PWfPiLrGM<1mxOw-{ZBM zDIOwFc~@ERakR6rlXd<0(|mL1O|P87%_a5^)}^kP@udQF<(Z*F9TzEW?H93czq0xw z>>uvLK6ZaQHdXG#u6;iOB(8uBKpWykt~g9Tmte*K^L@8^jcDAp7!kidO1jq9r54XZ z)W+@tb=4W7cq^{a%~`r>YbUwt3|%91y+k#;PU7brwskuA{KzRjKLRtC;%PI8J7;)Y zoodj-o_^GlJ=p4foPIX;9h#Mrvi9aA;CgB$J`iqo-19#McbL9nWD)K+Z3u6esTAF3 zx^oN>ww!QConkJYo?B6%rG6s|h?%99S6G?CRk8lIArz(Q{t8U4JF*M3>UmkzhH-q8 zahH5!myD&`4MmdzKXj%P-&j4{3F2qXiIZFuXo`zZQ>`9B!k(;_=_KqA@?@ zuznD8LlizUhHKqfpe|QnR#taKU0F*|?W2)cUXQJoh483UTcCbeCrz1FHLEyBO-!b3 z>dzS+Vkf+6c?}VUZ+^z`6995sq=DY43HY>LdcM6BDp31DD#sdAQwCuo*6Wbsu3Poh zaA>?nbaK8m`36s5U(Bam@w$#_>c3TZT`ZZ7+fJZ?r(B(L9)P~0A6Z7nEg{d3dETfZ694{7kEy$$9qy676evtjv<{-w;9rt+b zr!G8w`OEgx01)|(G1DKERvS66q~T4r{sN8$+6)7&?K^pueRWqV+uHHN%6c5fKW_VL zTmRkwo0lQH>dgEGPP}LLJ2LSUC!V3(7_4Gqjqv-+Mb*oqLW{9uwHROJ*YYodl*V8w zh28O0myy|OmzgV5VA{ZO9@XNPQ{pmlaUyzdUfVGJMW-Ab=$39cTqACLTky+HwCSUD z8|Y`ZJX2Hr>Xw*oH)n^(mG$Uy!mmga(a(`$xtxBUPm~kotO!a#vAGOfK47b3#k&QF z9SfVImGW~o?3#I}{wPOXzSH}tlL^bLN73J2`&}9v1jzSe`MT&)#B^mdt5uy7a0@%Q zRb3PC+cTcZppWU*c=j}!iS@;-(y_cqTV-vBN$0vK~QirsG zbIYKw&F5tZXgen|u_Y92i*y)SvEdN!N-mvPAOi&3QEO#)D~B>OWqhw78PE0|8BbR> z5$%_8bzyhdjL^UcC@pRC;7;w5v<>87v}l$aWfACQG+{Q|;-JL{G^?pA&+-oUJqS9y z3|3dG&So%b4-%fV%iVC$@b6Z@u>y`lRaHQ6<&oc1699#^Xxyq9RJ+XY%}&-NAUUB2i661+86 z2-1?*e02%S09HV$zmXHiz$)a;aQg&fn`r1cPGr0mGBI5neEt>^$9+W01HEZ#01gm* zq9uwrF&nQ1~l>vzr+O+aGtIK$%=!ca}7u zwass<*|DOA0h0E{I^x0)v&PkVn39Ix4Fz0*WFjZnz}qd2*rk~_gW`Ci1^AH>=er?) zNQ-1P8J2d-^-MLTwK69ptd(s_&E_>{3zCM(9}uxTSehiz$C$__>}uW8u9kD!#?T)O z(e9IOwQ7*fW+kCLn+-%v>>fo+lpix%LNk0=M>)VxF+&`H zQ0xhxBEr`OyA<64yA%*RpSt5X3`&Uj)e#{aqa4?i0~%32Exu1{7|?9uH&%N?nt3q! zTf13f+SgGMdH+uzu|FNMr|^hIbDmR-HRq)Xhc3L?yL|rRvrXrtVxE&O-xBlE=340^ zwS_+O2AE-lOpnu`FZQxGgMWl3y5|e(o&^rWq?d5RkQ>2blCG9_x=Huf!o8|A%r#6s zdv9*dHjPp#5f%|zpwA(FlatJU*=POG;TGL);z*tCZ=|X$W6D&vQOt+49Nnk7WXwgNcQtOBW!9MWo>QeK>x*=K@P(7ZL*9 z>Q2s#ESF%FzO337$o?50xIS}J;WP_+KxEzzm ztJfjpJu&)Sd6i|yIw?ECEb=hF13_MfU@2#0Dth(Z23sygw~pcCHY6{rWfCBh57>ui z5*Cz_CE3)cxl^q=RBAJZB$+v4)7&IkJ4ke=f$7f4aZ<`t*AP_t-%bd1{YUsE%XbZ3 zZnb#P_Z>!He;SW^i$pX=xN3xe@whIj!GqTzx%CvyG@s1grIDm!eaXpgFZFErT*~m` zZm-XO*(=(-xm`wJV4S)Mp0>QAE9yhI2Bp%i?8k1M#J@S>$samA@oJgwbHxf@en2gs zww`{x>z#%YrJE<$lk8RAY_NI%2NOsz8&5Aln|_F6bvg9%g258o2N{O)t`WYFWrWX- zQI3SKjf?MZxVG_{6w1n2D7z-l)0MG62CqvGTJVA!c90PEE!Y;$x3bsFx2BOmw%;j* zF`sKNU;VfJ`6bc9bm|ED9>uYC27t3u93o6jV+7~i2@0Bx#o1D|Fdaw zOk2a3wQJG}=0a0krB z<+yDLiqGs+4VR$YBP1w3zEhR_hUKVw+#MO!rD1YZlbk(77ZXAcHSoA>;3-?iAd2+< zMO)&dA6`-DKw+0~hqaCFo`igfbwTQDq8l-pm)g)F^U__{keqfLwZA(gr@fcV9!+My zJY1M@+w|aamK0bL+0Q4lcMq4o+_o&j%K*)15{h?HOkqe$nnBNp+$`p3_IV>)xM{Ja z#f0YAPfF+jG{>{!Y_W!j$w6C!QTqqw3aF+&(bqMHn9eGPCs38BYbkaSE++3^S2z7kV6Ebo)U;KWO?L1$QY#|+&3;BZs@b^Q^&A* zsZtu3QN>* z24iKW1Wv*S%og~&{|fuN{|bk4Ty&pVG*_{p!(yJ!Xd z9l&u9oxdpd-gl$)=F(sEJ6E{yF`Is%CN%S9ve^N_YN#Cx-X2t#h4 zTg|W0E$4lZfLQX`*okfyD3q6k3_%4HxP|ut>gCrILo~n#%qt)7?d)UDyBngY#v@{e zdCzf2BF^&DzjLH`{k^_rmt+8yh!Kw10aW&L)LbpMnm*X+7#8n+&hOYHiIV?gc=9w# z7W+8#+dUhS72J$ece70Xmt=DMw~mxKmiMU?T{!_8Rr97t9B{ck1I*+BNABfiwg3Ki zDJkK;3~SaSyi;Bd4>(HC94oyz)z?hld~9Z?8=1Gk@lmH?`f7yc%&iu#(PzmM@PYrb zE^1grNi?j08YWUlxh5M4PYA&9%%Q$D*I_v~sBf(qqf}830#t}J^!pkhq`q~p*ZlGt zB~^OJ>6fqWm9N$)UUTX{Cf?Z&sy}VNOCA2f7(3B9EN8-bLp1W+Au*j=aC@Q!(Zj#} zsQf+(A9CCRQm%AJ+r(X3w{w&|!@5d%ZQ)>j;fVRFBZc=<;f_YM;l*78{%WY^{7cv)D;x^pWALvgQJyZyHB`^ZPF8fhV%1(lMbx(_G zRI6p3d*!BLr*66{16QY5^GR4A(3(!^*j&_xb{FdtyNkLA$qmMR;eiZyUTtE7alqqm zA~C1MD_TBjV>Pi-gpJjsf!L+uyI$+e&$-M2Q#WW$eaZdBu&&Op;r+!rt?5L^#6VSs zxZKnmh+#9a#^3k?2-md9@o^eP@r>)s0f^!RQUH=pttzxOv{MqqN3^KYcq4g zT2n+DU*GYmo>t39bBlf0e{G;d_xKhq&|dtC^7^y85bkCJz}e6c5#gqDIsrmk7CMr` zPRW#d8l0wfJ-6NKZy^f2--jI!YNA^?WtfZi>e&7X8UDjwb;o5-_+q*iIMb=;cG?ST zt<;+dfm0!6pZP;)ux$zaR+KEeMN-Q;}7HXhJ7fMK_~K^T3|q87=eFZ5?R!;+H&c~tuL`l z-A)KpKVoZIBjAm|9Sz7+PskJUiqKy{o|<91@|}b{m$5nhw~?n>9omVm$qTuIwVJvh z%x3+}%v5g;tgA)Z$fFK7h)yC+Il~V--1A@dOkZphr@YW>3eke=;eSp#PcW$#=oYi{X zcdN!ADJE(Fap0*sttwwhv8H`nSKigl8Jo1+i=@VDJ?~JyGj7+Q8h}M4GAX(>;}>q_ zJ>ATt(yqA0F&~%dhQWToTn0L#(h8~uKubut&M6! z?o{2JOjUNkPjkZA_8sDB=!UbQnk0>gvY1O z>4GD>37m&rw|L@*JPj8S@JjS}0%v;{Seajyz7v->JYY@U&3p2JQ|;I!b?0Fd{hr2z zeH1(=-hto8?;uU}Lz0d^qzsL%^mkI8af}`G1S+$PnY~1Tdegb=5nXt76@aZqW$0mS3kVjT#XkrAaM2Fw#QC ztu(xThlB|s7AC{|S`FUNQW!GL44IH8(3`5gVMC?`kh#tcFX)YI9|bGLh7M1hM36QD zL9f?JzZKp*=UbWV=-xwep7iN?b;zgZDF2d^zsT_U;$MMJRhG|nd=%{IKlL}!mHjT!mv)&2|>!8vx%G$HCHOcL|xqSaX2-! z{RD) z{OP3zf*`J?!bsT7J@lZJDe%4>#*z!u=y&PDv1<8~3!QXZxX?krnG2DjbFS$uMS*Fn zTv(2L#lxY|^z3>CZ%qyf5%r@-e*HLnZsE!>c- ztkv_{SW5)hwj}5c>A0DXVJ|ZCTfDINo%^xtkaOz4tplaI=^$F+|-aLqr@ zzgUEb#rPKMhp`Vh&$LH5<)U1)d{n;g!TS&GYSQcsZmh=0V#jh4_JLzu>Y$xce7E)J=n8fd78gEAwNyH36ZM< z_f0%?G4ZjRJ>BBBm3M-_g`Hpn+Y`%hTu)TcY${btHp8Ri*ROq|%j~>Ru(9Y~^Ld1L zOnp8L-c`#<>G~O5XJTUb>x?IAzXb7z$c`r*=u3Saq?sfaQB%6Q=atAivlm+Sexnh0qr64ZksF{&84!5U4)#l0{`&?G+l?CKVbb8^u3D+oemk(8`j_lF+JN5$MPEUYBkc@ z#{BXV6xz}D?JXQBGPnpBAr2T~Ydbcx#O~>QDVs?bb8*H?{RYjn z^3efmkb<}Z!QMd`<`bLi{Ie53FY#VGel{ew;JqT)A`Q#3)j(&8j9RQdSCPu$_0Vs_ z-(byA;iCYM4?J7^5mOblIve_9+tYSYb^(Wqha z%>&kqyQvPm*2T$4*?+*%%4GtpQt}7~nT#Jq3=EI!{i5dR3QCauZFIp#&3p*B$cutD#o8wgZlZt2J>& z3!IF(?f9hXH$q*S`nm8twcI-|rhcOqPqjpAytC=45l;Nn|cNj)M5&P#j%$MUli z`McC_H`0H~SVI}>h(S!N#TuzbZ7cevTeNCDjU=LKS1S~?1o3o-RhvFHQHm!w#1Rqg zdg~MQqPV)K1C}9MaFSsOTzRlY9=JAs)l+%I4z>a%VDDQ1XGtr0Xjg0wZs40MBxGMX zK$3KcZC(=d1~DNMkHz$81VY5kDvHVx+ZR*YwG{}bmUN87ck_jyBoPYT#CaS%uKh)k z=sx7mafj4gw#cX3opigZO53kuD@$J47)m#{f~x3Z zYkrI`^_VQLGe;sI{QOfqgD}}e)$2HJnHv_?7J7Vroc#b^Y;FbV74=vJt5C~AG9ZQ? z?m0>%60?f(kQ6WKo7xQ$ZG$=JyI&Gu(I$3x%L#VKYnB1MJ=Q!-z*h#DFpx0LxbLy_ z1!*7>oO$N$WB&Ljmd5X)$+o7^CRQHHxq$NQ9OP1Bbh}sH_S0>M3*25q`GCFoQdQO7 zOH2VBsK+LwZG7%^(Qs_U~W@e@prgB8eB#zXRMv2VK#o7+rVT*S&Un z$iAWshbO)tfuZRUWjnpD7DkyLN5d;;z8n%j5EEUyeE$!A_|$CpR>w;{3xN<{R|t60e6Qx4D_ z7ojjY_9>*A^r4OBKzqh!Q8D@)s+^^ zRb!i_P3By+IC(cSd(j#vU$fVsh(kjyj3)Q_-oxEH$TfA>ZZ=VdbwP}62Zn9`of)YgTDCE4@b9#$iYCRfBCD=C%#Fnr+2`dhrhT;+MNK>|WS+ z_D13A`V392>Me!&K+=?I^-+BIAvPIfXE?B3Ak^!{$%h5f9Q-_%KhD$52Uz|<>({s2 zaXDg$#P<%8VcE9+JsR_u7{99w*FKMEq6wSA4wdhwVZ^AYEs&-Nna|3s$lZ8`cX*H}<$H?NL=re$o$Ca@5Ghe>l4un>Yfe zlrXD@&#s4JSX8qU>sj8bAdxcWaWxkmjLz#%(l^Oe_<#&E6>#U5Ml%&|#Sllaj6vh} zP3cs%4)SST|j>Uou; zKII6dNgmVWQyL6cuUax~4D(mXRph56P}}mtJVZBN;uaNBOxBuC8n#e`HMW>re5f^j zK>XzfVl_7~k6{^~xYSCiWu`F1S3qFLq1PMp;?XKLDERzv{&EICAo<4&IWg^3|=l{aP>X&)v!k zISu@aDX@Y~%<_3<=iQ;_s9XyI9Zi&3Ol6|z-v@0HJM+%R=l1e-@?5;w6=bQ=EiHg& z=40`x|I*O&0G?q#RGciL*#3r#W#+-`N2zPp2AvM^*)IHX1pT@({xRu?DIfaPK)dJWJsw>~c zt9L=}7c;pB@dr}zfL0Tg*TfO&44(whuO@V6EByyV<;#Yse3?z3lb#+I(o}2v^AzJni+#>2$cO2An)8BBL>E&9uZ=CJdj3%z8 zgz(a>95N6ya9<1-8n1)A_q<26-G{jJL*zYwBzb3r-n6<0hY@+pro$_FF7nLaxEOXk zAy4u=mZpGsHhomR5pKYwuUS+FqShEil z*j8M8CQ_dKIe@#QBke9WN|c_oHTbHA-MTKFICt;2c~LhFFKpIrqp`@46FIf&B2(1g zTL}Vh-{DX~p)dCF%^LieW*r>StnN=}R^^Cho&Vd-N)UQ^2j9g8%Pe(j@C`}mw~Z$B z_CTs7=a}9f8cy$t@efh=1j%06Fs?fd9?ytn;cnVkF*VHJw#@kyd|gcp8;(JyPcTcU{kYQ?ST+)f2v$1-S?Q z?>gE0bNqlBl;>fjs`SEk9Stkl4mD3xD%?t=*3+s(chyLF)p0I}6*1NN8L#t|)z}Mu zdJXnJHOoV4^0B6~np)PW>YcWcM^n2l!r{J*g!x9F;Xh2HP;ZE15Z63@8i;rXOK;)G zZNx$0O)s2qnW^+@xtFy-pHh6!YYsi97Tbp#n)-RSy6K{;LY-gls#3j2n@)C2!TLj9 zqdVBB{;FbQ1#)y;r)>zwbWz*XD=KKPCiD6p&YxrSv-qHWzLvX>8sU{D5}(lmrv`P= zqTjzAio5#!=QtQmh+z;KX2$Y~y<_j06QE8ejvlq18@5N<*?RZyA4(EYd9wwnl7n3dX^nx+Y)MSANBIGQWI#8h1*q8(bEcmd+KFtL{PGW zISO_Xf^CK07F7)wz2XwFZl<5}{hOKNdHpXixA2Lhwh_t{EpR4AWPGmJC3C1dYCYj7 zC7+<=23>5H>?IyD;)gx1&Q-g@{bLDfk(d~*=O#LM2^Q-Fo$Fe~* zI8y#VWM|4X?Zucn}|DJ&eTi+T&N#ipsn_Z6_c@DSOi^7jAFnaHIi6R93*>u}rJ zUal1-Oi`<1cFi~ydiGZMHp(^;k&&p)pbsIE4s)FKqiV^~3qX=tTcyOh8;d?hZj9p# z``?t1_K)?6@NgW^ls>`%eks+wOH%@8W4cl;z9Px=AQfFy9{u43tlUCgjh;&7u;i08 zSEX+5#&*K`JE+?2yHJL5Nf+(8ErC#s)YTxUb>7v;Z5t5Ra4>0bR7nqMG?%e!vThFH zfyvtkQ5=KOO>T_oo`h2`@Io5sO_MdNf#q;?!i$A@L^gx?KAMp_?f?j%%D`m8621Ao zEOpAz_qsR{iI+N3r0UVsPK9`s_dW5qi{6QT2}@WEXbIwIKNIPesR>Uy6}mVcj;2Z% zT?V`80zj7Ew*6NFaX9H(bDB~b?SUBu?_D?9@c#WxHoVUO-X8$o2`3Z%I4#3s`OK-m zyh)cQY-djWubaGy+uz@`C~>>%rlpD7L+~^%&Hj&JLSADXM;f+=c zxioxNST6bSY4nlnITtzg2!F>%BXc;mNDjLgkAHOSOXEXHA6X_kOmLwt_s5`i(e*>v zKF@-4?c%}=LZS*nq6Rg1MCuX;3fNGei&}YLF{hBiK%kD{EL8N}2H?(tXI89qalU*ZuJfvr%XVDAbyGd~*50b8lVZS?+its`Yr z{AsUn*Ry-h9ui@^R`xtkpi;WfxS22%*L+ym*XZJkIJ)sn0$sy%tXcO4&UUhXJM*3p z2M15+^S=7IB6A}O@~|T~wFEJ`r5zPK*S@O-Vri>yap$$cW@>ZFD0_WEeC)Yhvg0da z5$Q{l*~Rvk9#2o>>ok?)9piSXnuvEya`;IcL3*41nn;JtFkFW@-Z4C?*I{7m=%!&Y zr_jJQdPEmU`-AZj`!#da^7N(jGx;%f$8pFR12C0}@AJA^i%vy9Lz#oM>p!oFxAWS# zeO_^v=0Cy5^kqciB@PCFaQ+yiZ(O{nYG7NQX9&gb!+Jz*}Bk}mml;blq z?V}cJ{rlk0#0XT=fbY~SIISmh(V*D^kkXzhpdFD#C@l*e-YE9?pc@oUuw5AR{w_aCT zeE!j87()-v#l=(2m~Xxf zo+|Z-BidVX330DU7(12M|i z?6uN;&}N8~nZB1aL5O5W>V0HeQh%oUJ_A#_NyHxrC7b#+QtZvG052sk`gF`AKJ=1O zLE}Mf%lx!Xxa;$u)|yD~CBl5gD=y9xGhWigm%K_7jBwn_AeN21qSmV%FZy-#)9{4P zC^?g3N5TsIG6I`ORD2-LF^fSHtipc@r@CGQ}{|fRytwZ(uMs# zYliL#R1v~W(>#Iw{aQIYx%`Kb`+>kOueq+q6R(!|wUhAcky1FRppRnS``5jqmi&q( zt+R$%v;p?~pp{LHT@8J@jY@uL-7g5%h7D^{Ba2E2 zOP`Swb|?HF-VFDDH>Dt8J)aQ|WLAdaqwQp_;R%C*RPwEtHjH1YzPP&^uwR8tDDZs?2JBF@drSm6jJW z{AS6`DJi8T*KN2FO?-414;jrrmQ00YX;rqe>CmJ$Y$-2n<3NWq`LesT6_a9QoORn2 zDHx(G=8F+(t+^@vM%l#Yag&`|y6KM`1btJ^WPB6c(h9L>Q|ApZ$$xVmrxKC(SikD$ zaNO@j8lK@O>Fqc0((4AVD9dQWGX1Q19{WdJ~G$sd`l zY)7Ku4{&a?jO(;!%%c?4OK_2eX9@(TW(}1<>1M`srv>F&ni0|_DHbN9k4p-!Bm`tRj(japFG=8NWa<*czJ0u=Z4+p zCJvNty_|jlX&i<=L;_vn1*ecyK-nXE*E&WdnA?E?nrMmqA^C>nhu79ZcPwweg#iwZ zLQlZd`ReB1ay;2hKs#z<$YV`@-q^S=LRq!yl1(Ky%5b+HYmf9DQC5F39{)=UeH*!M z6=Wdpc$c~&Ma?iWNTEwrZ+I}n%+C^YvV8vEU{Ji&T}0EMZ`(K=CpyM))0;e)shVTc>059Yn)Ov_tm3UHz0PC3i1&H5fim>n zp}s({E<+WBac3)VgpNXCnPo%qi#=GafkUp&A&J9MmeJn{B<2)NfizWg7)?~on z+WyQ%Tz3C6U$!pc%gdz~pclRhWHW=M7h!*PK8?Bt=n&Wuh@Q6LiLOg=jeLC#d)r^=@xKUo*}&p1P>EE?>y9O^6u9TKoFM>vB z18HM44T3^{9vX!eUxPEB;CI@j@z6?n-MRtrH?`@Q)I1bg7Twl^Onh{FiGZY`j%-v* zg8`yY8vF+Or0&`D%bO7iBsdq3=B91*B!bh%<*I_Pbat6@fUu)S59H}PdT6-B`;m3@=o6Q@nPOj@>(R}i(T3waC-2{m zE*0HPy+?;MQ5`5ljK7;Ez z;uTg)EAA@gHz|cbq-q%Kik)5?CZ#Bcwm*Bh9-|qpmVG~KBB2FR29v|mN{5P2AqZ( z>C|&;@H{=|20Q6u=1;cqj+#`@7Yz}U+t156iJ4e`9`ocy5F75ub_wVA@@feOFaaX( z7V^2p`jznTH|XB%KYlK^|x1{g`2k$QBR2;SGH>1WOTSig|+wH}olCVd0*=)(_E}=wwG?|CXu+y;H4AR{@uI zB}q_AUKl2`WH5pM2m*eMMsc^qyt%_LZ)^hd3^VWb0VmH?Nt3r?@O7~dbZXJ}9Vv1i z+YDOHvdFaWGr=NZLXuXK3AXxYkYW%{4KA7mlUMC8e0r06MLmZ^rwb-+TKM!hU9|t) z7VvuB!0=dmQ_`^ZaVWB6)eup1!|}DcqlVAka)F3$BbXj~#QVB<74V3jg=1`vFF^GJ z^d&Vz)k(ne3OO4T|0{-5FnttEE^UfSBm~8F+%zIycfVbSF2aNm=oms*Nq&@DukHw& zyO&A<8z1K${drGJS8CMq`f#5^Q)+croh=pGtx=!9kL$0>J>T`ck5;R>GTSKe`FcO>MJWI?Ca%}>=M%V$; z5jFslf%abN9C)c*m+QF8Uf-ErU9vr7SME_7l?GCod}6@ks#Z&~I2OVCh`Z>sene#r z&~2+b@3i51U$y?k-1W3i91WDAx^b(PEwE-dplVp>~bGzKI&XfI7 z|2*R#H1VMpx;9Rqeo@c4n6Li%s-|>%;#Ins{HyI8o zYA{NVbA^0K6#=>hv&F7Q_5u+Ri)@7bz2w7;%QihB_Jx6wgWWOC&Vh;JR%-dYJ_l^k zRbR~rEVa?dEt-A)O&D+=2K+}oY*BTa-M{dOG_jdkoMErNev2{$-elE|(5JRAP2IEE zy_eDEh2Wduqcp|r&EAZSeF`;H^&Z~X7pF$7-T68$9sm9R0pp!bv_AUd-gw+5c`t;L z98J#fcQ41|X*Fuek;|ZgHdm7r@dj;(t0^#L0ayFl(=zl*x%lA_8!$ z<>_f=5Ct^#<_6J~j>+a$T(2H)>c4v{-UxmsO@{0L83V@JEm!)=0K~}Lf5L&YQ`BOP zMK~i5IFZYGG#{_n?W9X&;Gc$anpO?>D^pA4Op`g`v^UZ~ zIn5wGA}KJ=_9K1IOfk12PgB3(tGOFx58F?Y2{aZl!FMvd=vA)tv?Pb|)8c4T?Ec+= zoVxih^czJxn7;)2PN+XdlDXR+)EIHy&0CS_KLPm-3}a`}Ey zg<8|P?=-Cob$m*llJbLzG-T^4Nk~e>`?lNbg%7F4dtV>J8)og1pDuG?@jGNJtNFt? zESE$x;$hQW=?bg!YYhKoxp+M>0lzY=RysU>y=;((_6@Lr8h-`8z6!7BEFBYMeJ~zE z_CX^M|GUE5@j=SS4~l+|JRQHY&lCBb*`o?fmj#X=R-QAP3d(kl%J?anF=P06+x2HV z@j=?C4{nqns2`I*4f)gm=lRo-KVxKmHTWFNwJAsRCv-D=g*C=_m<$Q|g z*EUdWEAv(m%dbAdEI-0TZPpTxHDjqy4F1Kzf_OAiUjSPBJU%C#JO?BF!{t-!%Ekr*PeJtxHo9#3)wkLemFiAVRuCPC!Ty80=1VuFRYe@-jeQwl7@XJM7^$-RqzRC%ZN9%i?Ajt!hIP-p>Fy-uv363BLcC6 zef~Q56Y|Qq-M49h6TKXjHz^Imqfk{KcBS}sguA)S(QcB#Zb$<_s2kPW-*mR)2J7M5 z@;6jn&(5g&r>nDjVROS`Kv^kPmWR5RC0PP`8ClM3W|w>wysAF;bE*R70f=Dtpy*@+ znY`MO_AVv%vBwpgEqM-dReL(jisZE9q?G2x%oazwjSUK@jQ7Udm4l5m)Y36(de4Z&%3fu^ZPf4gV8H4N=FJT zLN0uqg}CX#P~k+thj-bcXMtt*lOuW@u>Q?AlT3LuKmKPTMr(On_^=@smr5UcBi#x3 zpO?xNvFIA!6zLoHbo%Sqz0V6p*Ye!2VZU8zvRCly#}`K7|6K9p%b}!y>}p zV$rGRMgqN^#WP>KoD<&5^rcrT1A)<+F1YH89im=y9a)=-D8Ee~Xn||rQ!T_h>iBHJ ze~q>9Hm54WP5oM+|MQwUr%4k>WWL*>2b4XI^|YHIIhqjfL=6sM82+C!i`ix<^&=uA ziVr-w`@Q1)hBp|U)S#+>V>pEjr5GF;N^uUofY!Hd_$#k?YD2Odk2QZSPQSA4eE^!3 z8AvEDQX(fIrgUM%{ar@w43D*bp&{0l8rGj)U~>7>oqRE;^JID!ZcogB)WLpwo-e;Z zc|H=_%%?W+Jkb_K3LcZ{4lj9LHMaueBR6~p3@E@tF7Y5G?8!Y$rO3j}^SqHuavxd+ z(W(?jedDZfMyh0a8EiR3__qF^Ve+noJh|1fS3Ysxop-@2zPDi%ektME=Au_a(o8Dy zE7=5w0}qFlsTlr7V%UpVl#TEA`c0mFuD91#qDKG-z-;*I4W%*fYUJ+4L`SO`5WF=L zm~QXjzCPjDf2IW9u+_;+Sg-Q`-_*SgcvRK3Fgzz2AmG5B*r>F{8f$1XEw!YEmeJVe zgc&%aXMkD|P_*6~Z@zkKtHq3ZTMUUO18h%D$W;((rFh@gTW?=2S_J_O%p@@R0U==wf0(T@3j_XQyz)TpKqbi z^CdN?V+{8^fQ}x4*ex$55^BvA5#}&-z5SaO3SG~8(cobptL!wR%XOK^bb2VbTo8~U zVZkL2enENYbiItm0~k~<;^Dm_vVAW)!>=0=T&_kO{u8Th$Ff_{e_!Y4TMqEX_AiO` zmf?DvRR@sI@@j$esGtcS1VJu*0@yLHQD6tySshb;p901|d^>dBV{EU$9o~}AD+vIl z3xIk7C&q*;f>Q&7*>^*sDsPp40s?jT)Pn-PSf;yzWqMy>srnyrJ2q9ry8ed9Gfx?9F*p{^i&^x70He-r*s==x){7 zykuwl^Y>qILJ!-A2cKhZ^b#8Q6+GVr?Mu0$eRI`(+l#U0eVov|f(kW1d&BJfoDIN7 zLPJ_T!Ox%}7hODKgIAapz~kUdo}JLHJ?$@yTxatG2s*U8W zYy2+{njT;B{}iDE9*@E_>l>EBHF?F)kY$J@@#Vz;55{|zZe)p)qcx4u@O=wW8{gL$ zkvXA@tnF~oW;|}X!sEA}Lmq?eXk;53gJ)9Mj{0D0a?jyysK}I)Uo56WNp8G_Ce)Fieu~%U-tQCFB}B7%C$4Ebif0+Fl}U^Q*vqF|f7kgboTvnI8^+dM|Ew z8uau@b2OQfIbFm5GM_vbyN#kK+TiNOO8RutA{1%3p9+ubtB03tI=o#mRJx0wA5r{Q zpm-j1Ia+yr&k?3H%F;n4dHu>k;W;O|&-Z$JvK!BWuE(yUm(`#9v4dVM@V4v>6{V^= zO+#$p+u86JlB&~eDLPGps?(GJtzAj9HUP4e1;^VoTCx*q=cTH zbn?GR7SwA}h+;HnHR16r!a<{$&dM*_6d=>+R}rz=|0S+}GIH)$f~T!YawUH1(u*gN z$CGp0#^V}15ryq7_t4S4+@aA#A)4&0vS%v#n)E@kR5ZZ~21^`XF){~9Y@6!Qs;4S2 zN5n*s zSXNCE2T`ztMN<`Hy$JLBiHWj8sfyDZZh)qUR!~G zQsCZTu-A$bItM^MJ_-+vfq16%B~j2x5DTO#PEp@fy^?TNUrX~7mt66{-NFSJrg+^@ zG7y#t`=SP)I{{l+-Le_K0o%u_Ah4zU9I`8^V?Uk4bu2dPYQ8Aa4nqP0qNukVxq zR6wi0)|b5GClyyoANJ~JmO<+LLB2nL8Fpm2WDYZz#JE4my0km6{nX7Xc z0Ug1AQV`Sp28y(TwrQZ3=^isIuT&+MM54(S%TwP3;(2C^NjxL+ z79o;_0W0TJxtvupn-gemX&+bd*7Zm``+zT6R}ziF%r6@+Y(R8wu68+T=``H^Lrs6FA?76r@JR%8Csv$XY7l^mA0=9+XuZ-?pD6h>OR7$EFM--x zq&T(h6n=u`At8YLyaOi~x1){5^lEw~Qen~C;l8>}BU*EXg2p0|c64gJgBWTtK(5E0 zjGBaV**CfyKVRSG_McsLQ`?3<)Lb=3wBB^#AbUoT*SIk*hQAMCss`lX#oo$G8fN!hCY?_)VzB)F!9_I(XR_g8AUZmD;;Dy>tP7IQ@g}(z0r_hb{i2)B< z_i}%P{OW}#4~PsCk2`wx#TpCfCrZ~o#0703v9VeX9-sh#*PYt47C0by4cG| zuy+#dTNGIWy3=~cvC`{C1GsDnlAR4MHnY>kW>*18NK%}KeDBNMyQIQNSC+))bcg~P0E>&aN0ZsJ#whZ7 zhv9!-#TB2G9f3x~oZ*dP&%SQKW6X*ARhV`E>*oBIQmYCmaw7#iJXU~#0tF9o5ts># z6O3lPY=5>9WlzN+}x}5 z95z|X7-iIYlb@OjWbt^MEXEcXB#O>HHm3$i;tj*s_qV!>KKbSGF??J--o(dNfn0=!XA+TYe+Vm z4;Jx}g_;2sMGa^_=W*`yl>oOkk=U+A)p@4y=7Ng9fsb-9Y?Y1TFIxjq<>!@%4+Tm| z&27n~dXA8fD`Tc4!+JKPUy-k_TXFvn=vE@jzWY`$Ob9tDXdaAF))1TBft0nY`BeMY zhtHoD8e<07h?Ce5Y5yHvCfDTR=fd~SN+L8{Dw_mEH=$WKT88_B{2S}hvi}>`BP4lW zydJfGIo7``)+4bDS)aHb03TxujI>G#n_QH$1 z%+vk3I>6>J5vbEJ5cTFs6lz`Hwqdj?8rnt>|+Y5 zmij@YB9XZ0hf4nLtfuY66`K9=7MCG>S>k zS5|fb1^9WE;r7_TnwNX>pQo`l{zU@B_>1CfRCw>wPpO2gIx&36VahrwN`fuctvKl9 z7YBN0YTTe-C9T*V=$hTs&Y{xXLE1S|92FEKsPb+OD%)dyQ>490a4=ogx1m}vc$)d6 zMRy8m@~w#|I+AEeQlU&B;9{;I3Wpe*2*x8W9=fCeF8%FES*ZWpOM9XEv*~D2r2jPo ze)v8|4YCA}0M?2<1F1&l+b{=jaCFcXfk**avqR?3;zx~9)RfA6`=hkgE#5^aI4obj zSiJjhtK;#j)bY@Na~*|OspH`*>c9!EQy!@k<-j5Wg4}bM4r?QC&@_`R_VtCu|woWFGjk7S#T%T$dYGUQTR@OchN15MlEPJl`9Wcg4^*mU_9^RPrk{CFlfQ&p7C%j~WN&%+>51mStXe#b1;oEz zI8FFDtcT0sBeo2(3RC?--zMk5!ZfnSFKmt$@SMj@bNsUkuhQM=3J+?%DI%1byLzUP zQ8|~`o8jB3r#%3MyjJA{8ZaMx4e~U;FDo_fH)%06o_UkJCv=7{<|SF$r)UwI?^`jy z_-{hCKteWHi;Ich>I@&uLp#R4SUH~jopdrNQP&z$`9<+zH`I%VG5Fzdc^)3Ad0?un z^L->cXRG_ zv3FfF2WA?!F=(v-JSvk*Iw@qL3y_ZG_QL4hi~L<^Q0q-vw#UT}1zX1C8P;{AwMA#T zSKWm6@ROdBo#GtCNf$rb*pDBMmJW%{P?ew0>JEQ(dXfb_S-wF>9?O%^v*#8;T5-pC zJHjtV=bbGC_H+^pgxU^MsBKq=^E0YAo9WSN<^o69VT!g?pp5O4#8ECSG#84b**GzWa_OI`v_ZOsmyOp_v@qhFM+uGt==H z>n0fw)cR9m2UpLDR7m+9rsxKy7&%f&jpbLsPaML757n%)W5izV2_B(vejrZ-68CC( z1v}woe{5z6txbTHmd4Hn~dGX#zbvVpA6zeP{{* z3&4j0jMltS77?^U6^F{&zxTQ2;dhan#5Z24*P{iZO#2$5L1dObsdNV0v2><=28M8d z$VhWK_OXq9QjH5oQWirwQpS+uqX~mqm@MR%6&EBUXFf6lzQPBU+ATnaZ^ zas`t-`6vHCai7j&%Cv_mOYFSPQg_z{H+yWJyATpB7^LU%nrPgE@#Fi*{ZWR9@hdzV zA4SJU8K?1T*$=e#@jk3EBPv{uxyiGT_?}#$!HfF&W+CM18*kAG(tdP8EWC+d$SyF0 zmG@H^;KM^NG01uU34D2E=p{zw>Bl%mj~yL)iLLHOhpvx*a*4tOVRYWdyXpQE-JhiU zlXQQU?mxAgUYyVylL)2WN!KGoew_}+V*Uu9PA+ta39ZN6`#XgLbPNOZv4{qANkjuW zMh~BgcW1@BPt7<%IB*|fMDfM$t^$l{%=RBFz+lB}e+XS?GJig)$9A~U|yXDB|%yeI2jA_#S2f!Ts?|R5M1) zy9JQ-vC9P*Jj+E}nU|)$A%^ig*Uj&zuv)-4EJ1IqgBa|bpmeYUt8JsoJE`(>R5DgB zDrqrY$W!^#Z#gNvprhYmvoa|EfEdQfXCI*M?Y9em#PParpV0c}SUm{USS%fO|2jd4 z>5ZNWr%{Xz9M*Jcat#gIU^forx2Z?*af??K1H4wM;iDnIyY}y$qPG@`DRMmcpPZs3 z!aE)ewx;wP27+x^!2M*O`RUuG^yA zxyOdvId(g+1>STDwu8Dt^gyHa4zTjIbZEqDk2^-=edx<@6Dbr&`d=>eT41d$Uymo5 z-Z8LJBio0LB)E8@EJ>KwR{ zx+Q=lXB*(o3Pkg45QDJ0AO(Kr%{&f#Ylq_O^zCfxy@=A!Nw4vKhTuvEw%G(52aM?R z-+|dJ*?EwZSu|6?up@8Vb`%>H1>tZXaC|PW3Q5x+Jyi-X*bmSTymRRn(#ByIwEcfMDxn*W4vwn@& zIX}bef`5(`H|NnXj@fG@Qj;^cj5`h!mI*WTRLmdQpA+}sG(fL%Lx9XHSj)XmZfMvy|%?)w^#qC zA-~(qvf-3@^CuG*TH)_FWijyq7_-Y{4)&)QPOQ2TCrnhRrMID2^|i4|>P7OAVfX-l z&6NLOESrfout;A%j1_J80&LlmSmVFPYK-B~hCe6P`1;5iH^;KQKeEOc9<99+kJcsD zxI9*447)Zwmsn%j$QoaaWqWyKjWG;cI|9R=j@31*fZ&oa?x+78t75^UiRI6Fl2Vr@ zmi$w~x&?_?w=iDjOe%x0@TYgh%T$Mzc`v0d9#$q{;(|m>oE|Up8&n2i=1;#BFLTPU zGQUr$Im5~%>|BtDofG3_j;1mQTYow(UZ!nWnb%Y5q+w+e#xAhIwFq!j_$IgvqQ_%F z;Il9u%w_b0efxE@#!;XgK-MqFlR|mMd{LIsmaL;&aOs0 z%WOZ5Rx5YDfLMM5VzbLqbdVAU&BXQ^g;$~}F{jv0HDsWSHjoc z(a92Kryi)YtRW#m6v^w%ienFaCMV9OZ8x>5^tanf(Ov%1@VIAXsstI30Y z8cEsWN+u&CPks%mN!%7sG^9E=2~)*SSm}?6eeIvacH}%|HrvB5dmZ?HNF&u69{XKX zMz;C`MgL3YI}_AZ;q$kmGn&eaF$Ky=UXv_LpW})Yq0T{dN+7=3J>sl_Cx1_Z(H<)G zg)#-V({I#HM;yIZn(TF$U0n9vtLjs58i(mrItNXA>Ce+A;TLVYiK(N5g}|hJ3zi-& zVlxE*UxtK7j+93t_s*X`U*XWY@Q*OUgH+R=YUXMGiWaf6)xqZTFtb=n_!pTton#F! zOxcSoNw7c10+No%&J4`N|ry_s7An z@xKjc-o)xA@DdV?)V0EKiYEy+a3@^TCMH?=YU0*Qq6y}Y(DLZ?ku3g8xp zAJ!v4w2^Zxz}Nf+^md4jg!m`0`b@uxPJ&{=H71hyaWAg`pSGCFa@^x{t-SeRMYow7 zh~5U~e?H|36Ip+qOMf+u0FN++m7}L3+(MG7VmQD`F@lmJl#;!wxiNu z2X4fBc>~)20Dh#nC5-rz>SF3%xuh2pUCg`B#cdjb^7PM3RyB+Yl;n{b6lc)4*+h!M zOchA`4EXSNm(&U0ZNn;=x3Q!TKi!HKrq)B@GRWm^w^(s}?N=R7AGEC7#XH)^z`%zL zYp%0a=Jf!14R_F{lbd-D$i_W`&U!YxQ8x0DT+T4GGE6HyEB_rSVTfA6^lH8KCEr!` zX)~S#NnHxvZPH6MQm3K?yA|hdrXf#x7ruAyW!fr228HjHyQ`(bn-wFQ!{4uz zkm5uBrJX?n>indn*a@RqmDT7S%Y@6Y4wvs%)*_VW#Qset=+a=ks=5g!W zY`c67-MZekV)SHa#`PZO1>eVQ$B7ighzuQOY}-dWhlj=4849_8hmL?e9t)mJE;%Jk zcu$SGBEFZKZJv1zTqfW*b;xz4*Gl;EdU2nG_cx3CWW4_YTxL(f+>P|MfpX`Gd&=z< z_sJOf7NbkwjE=4_1VNKhuv&Wt?WDm7`fZc zZO_ew74_DuC#ug`Nk=ISqMCA>++8CGMVm+3r3f0F2VZBpqz)H+!;b)xI}for%`#7W z&h0oBqnIu}H7n5eL7!<1wsT9tXz~FfY&VEo(bv!vH@Y3Wgc!P_-DmH?N zg0(*ok4L{=@I%#dGys68knj+)Dq@91m7OY1E>e^=-7t^uSxr)C3L&bqT^B27>3HN` z1L!xOl0}J}zEg!cV9|3PjPKwQ`_JuDhRH7=NNJ%os5cENguh*B${p(kQ#Chg0uw8r zj6^IBAU}{02+%QplRJ6dc#AtZW2`GVjk%IDCg2hPMAYM~_MM<xGn3P{D#>)3aAtDG77zary`q)36VV#-10pvACbZD5?oJo0jKo5q zMbKBMY|KHi1Tbs3z)?TUOUQoHOGtw!c;-e~J05X48i$D-ZU>YW>0ql+kMvfjD>G>B zhbP*N%x;jK0X#a`e7ybFaegQ(a0dYgTlH`7)V6+5DENo?DI7E$7v?{SbQ}-K3{MC2 zM8GkNDZd|~XQ6D&y-_X>Fc%MAA;?g6CBI%La0f~@4UaM=TBe#nZ&Hp}$r4E^~AXD9WwOnq5+dqSlK}*0^oB0itz3!Ky8u;A%OEm3!;I%x%M(1V_42+!F7(A^9 z51CNZi2U6#mj^T&XwD(?YK(V81cYL1phr^oI*%b=L~mCry5cbIlPGvZ6QH5+f%8Cd z`}8Z~k5`I#3>5EFA*TUVD0w75P^;cSK{#0HN!pY!V==hjoXxgRnG5SAYW{&rs&ggo z-HBm#sD$iUYJRKgI0gSf2y{En%TZK@DAa;_Mfbku<`EZ$EU1`Boky~V?Q`A!i=AxM z23id+%UX2IQ`cqzUFmKA#W4S&5e%|x*ruajrJf^q8QHH=KZ2)|6vv>F?0Y2(0~xG~ z#UE#nxwdz})waC=W5Obrf{~P$DIIvVc(gz|5T6J}4Q+NgBZXi01WzU@!PX>?V~w*z~ij(edhKbH6v31NnS`S?+6cK$27%Aq01sbyh*+Eu1O|WA!kPww)jHLOs!W}+&^4Hxfp*X7@7PlF?>o0b6^u=Az*>T6p z`WGnM`dr(Et9Yauaa!<|FC|$%AHU|md37ASb2yIu;pYVWFb?Bi`FRY-J_WiDK>Jx7 zvlRW#+ab06wxKk_rVSo#vh&AOO z5Z-(OfFk+ON7X;zXCv|Dm4xT4Mo(ft0k-m|(sjd|7i9d9|WzrugpTt0ul!Y_FEVDS!e z)ZqKc4h!&($?u_Gm6(GF=k-Z3&z+W{@AVwQl{Bj^#=qsKfrNJun%sd=(4>PX@kN6% zXW`Dqr5}5Ze@Fk}UgIwqG{cj>9DO=$-Trl)`k?Yi?{DUb1I*#viAf1w##uRsM*LRp z*)ANGc4S3Oc0-&4CmV|Pe;nzrzp@G#BST4PuF{S#drgwx!{g#^<~l5ni?;(wYJQ|D zeq8*}ox;vxMpdxI&aLlZ(B@d%KV-&el95Z_s}M)rDHg9@_TDQLJ+0_ktyl7W2OWKD zEs|8*{;h(F?!%@M7FI;L`Gm8|&1M6?eyDwdIDa-+*G%W3)%;2CdD%w5)Wg7IrAt8p zzw|LdF92^z-ynM4T7}9gXdSw8kfmy%_C=X8tE`TjTK@pZ{IaLMW+-7041}>Yc64lW z`;A$FU`t9Hs=>(O+RZ460OUMmRQd7n!s|s|1hJeXUxuugqYtuR zZ%VR`fL79r`}|9~b2AH9c03##$#ZM(^`qQ)bfm%K-wAWEuU zs-xT=l4)Qku!2(1u#Ow`0Z4*eTPg`r_G&TT04f$6vLy3bHw;LxRQRKx7&b4R3gK0< z5FtH1Mo6x4RJ!CEbR1ccMe@+wg4Bo%I-M)$45b?ZKzG541bu zKG5#OO|GJyE@y4wcZT%Y?A^3kZr)rV_Z%$%3Vy`R-fea}_X8RoCnM0H?;`;Io$=GF zi0)e8yzC5t>=AP5Uvu$lz$=COR_cp?KOj4g%52V2HUB7p!$l9L1=$JAx>=D&g?qdE`znp~#)w*>mNBYRf!DPHKWFnfCr zs>TK}YS5Qa%xvqyt{YEEyi;z5WwLDy#+BuDLiq(v42F{@3yoLHDDubNJ>-v_O`kmP zdpL8GC*kS1$q#>TBK|!3J&XWRIIRM%OQ*e3z?O8|3+NE3i#;CdrX!;`x6=+{{7#tQ zB|Vsa8ZM82f`|7q@yOb=(|Gr(2;oUqcQOk#tQ3ykg(2R4I}4q_H+w_`*bYW~#OKEi zbUEt^CnuiIc@FS&sZn7@7`Fpwg2s4tzLW7GksUPz;;7i=zET%|rWjoiq3mM(DykYY z((a0BIRqDjk5*GL7Ns~bB9o`AL+-tB zLkIrkZ213XlB-h`RB7ga(InTY&O?Yltn}r<;qw38`PO%EfHzf`0nuU2KJ1Bxyv|PF ziMC$~EgsISOMj+V$GProKJAjCMd@;JD2ez5d==(>a6Rs%XU_A_9$Un9@A%^rRUnDBt^ov6>Z4|h4qy~TNkb*y0{ff^=$t#v2<9Ct1woiOLtEMi8nobY#hZ4L!Z&;5fFE~ud!i>wQsxk%;2s| z7LT#aYNoEk)U(&1h)hMsGg)CeP;3S8P609f71imh@T~xGz+Z=|1en)Ak&oAEy^O8E z(RBy1ypXim@AZ^;PU1-ks_-VIHo2~@Jzag{Hfqt zp3dR>vn%t$0%#TJ5x#_*k{|$t~^bqskh$jH5N{-{X zMIH>cC6No!n?Q)SW_4d+MWcT3Ej*$7*!X3cWTSxbpYhD_~oQUN3foe$J3zxy&XeN<%o3L4MLp4X>`j{xwiXR0riEIcgEuuO%JBsn( z2Xfg8Pvz zn4XI*dA#{jLU?I>|M*dSbNhrNd+^n?W_s0)1$0sYd$0gB?3kXr4=LibUc6@ZVZOR< zDrPqjv)+$cDvUawH%CgjbehGCUq6W9eI213{rCZBgyz~Prsq;!d5dM(K*!v{P zU~6z&mp7@#`V?9i>)!hjHGGXYMsu+JYa{(#?ezf3_c|L{X)&#D`#jNLo?d!6Md&(Y z^y@D(PpwOTgDQ2lF#}!8vC2;7x)3``(*k7RQy>E;WbGo50W|~Oed^+E!}Zv;WFpNQ z&|#b>f0rj1v75e>Zq!;LWS}gplgKjIwtYU4@OT!rFr+<)(G{T(r!hV}d2`~b8q0MbReEH${fb+^=nG+lTT zNSC_!YRXG2%wAux?fsF0JoA?m+tY-SS5|lU8Vl0r_E|6$va|7-n-t!Nu>_2q6>?9b zOR58kMyw2EZHY=2-N539s|Iql-Xvkz=?y9+I}s!k_s;_^9@)AoDHPg|kr zG~rO=((?BI9sY*p3iu}IZom=X4T%7LNFU8A8-zPMcm16>r0m&$Cn*njG+}ti<^-Yv z;BS+cK29JHeISrW2k`E!x+~y)0>S$P6W+<=0YbUg9tZlNcj*NALEyLReO2iD;1TF~ zf`9O|^2R=x1XS+Bk9`RIKW+OF>hmcf8j;HN8htyRRfRXf`u!p-%r0JbKuBLaIR~&;YmF z$O*=K6a|S_w|^y}JyDc?Q;@8Zduk8gs}T9PL}u4j=nJXArB_m2&OT}GJ%{cz9a&VF`WVl))y*>+-BPzVzZ+h5nBH4m{2j_peQfqswkgyrmoxaxR|pel^)$*-?OjHiJ4qno zf&oOtv=NAS$TxjBA~u+a7)Qg7ZAyv7vVBVtqG4A7&z=R9Ed5>k+u>KCL{s0u$`_Dt za9*$VrXa7vBOXVGM~m2zNs&`(3(-RuZc6Y*^LRwuKbT>?R@PbwVo5;EwV>ddF>iv6 zeiu)mfZZ7x6X@sB3BYw+L}x24!p{nNWD%aeOs>Cx%mvX->KxfJoinYaZl7?NUXX~;x2u~JEFcA4-Q=;l z8IwyChiT{-dp?{JS4poPsR|#(RIP#ruOW=|ur(=~qB1h4zuVYLHE8 z)hpU}Mf`4Q1pgQJhJcq>@KS-7V)WtqI4u&($TO(vD*+#K&DT@S-*ofWZvI-#Up$?F zzb)o(gZUdUf8R8JO$TklaZwFzBob-NI!ffbk6J1$zSs`nv+97DV+_G8JWt90NSmeJ z{GDn(ZKA+9R3js%N~Isr>5*ua$JqsvJro1joNoIkr}96%t&(cez)ViH~(}3X;*o*$63qDegk>hzE4|0>&3yOL%=SkE5gLhmj=S=?k$3Q z36iQYy?p|!tWA6snXna|rz zql_A2tvC6JEFdOm2|~$uFmCHD^p$M~~p0(?4Lvxt;!mb$ECdYv)$Up*p zd92Kh9l^WRZZWqP0CX>)4QU-(+&01G(R!r6wm5~T0S>tIe_5{Z-=*m993Z%8J{2Pu z4Zbt&zkybu4{M7GOEXPGbHv51@8aXS*rV%RD?Yz6^qhjvuMIuh=%|mg%eMth^1cCF z)Vt6xDo~NmWkm}rRNXo#%H9^U#~aSfUJDB+B2}W=X2m5b9v|co$ttEh~(H5fychxeIJDWJ|TK zGC8_6ZEKP-p4m05tSs4VQ1{kx@dmAcmjxC7mG)puX4~VbVN6Ynr=G;riSg7HOq~=@ zJw$!^2>X&A>r2Mx^@aK8BUbH7!v2h9W$!1M{fTT%iTB4k>VLLBE!1M{)x8sAEuQrG zEk=KoZC|1mk7s4$VlBQ%8~dWgPmZ$um*C;QGX6O7v(w|LhcGoGp4vct-H(0EjP-TO z=lAso*w=3&HR?@eWfWQl``WS99`Eb5qlNtOe`kNOr?WHTso39{Q{t(6Fm-A?wH8ye z?m^Z8@dkJL&WSkLOd|A98WLjo$YAX7Catx$|f&w89k*z?t@^GGgx zewX6xL8YPUJO)42xuxyQe>ZRdh)?jC(91u~q6yTX@jaMul4FTmd=_`3*yT~uVIn=L*#lcmnvkm=E? ztzH;8|6lA&o8SfSaW+ z+1a9Awr~*cvtMG1E7|m4IhV(d0 zJRO6HPjHNu4bT`({d>pgzStQ3c-R=({znlGZ|?>8hJx_+!6|MmPmJfoFHLd9&?FP? z+Mx5F(R;*89EwjV4RNzQo;JxGGLeS!a5kQX^KdGjhVyVfo@PUojHjizMSbE4(e;e^ zGzGD^J)Sfb<^c*weRRXr_&nss=3xxZLr)*gL+;-@4^_S9JZ$YvoCo{o&4a_05vIv- zs2PX;r)C6i3!tx9BHaEm+f8M{?UU2pR4UvS<+x33AbQ|>DTghB#^oW0yf0nPXQ7Ud!NY`eGq$}!4`#L@6*|$eX;kGWI(uB0+5VFb+HtXKZdHX z%L#SZc(A=BQ#B$Xmo0{GQ`HT*@j0Fpo8vDqBr8?rW1QUCciI_%*nPl^K+^!6a6eD#iTP;ZYoP zShrURo=R6x4KMtGN?q{Pb+5x$Ec3=suj`M(zb zr7ob%kf*PUEgpn>kkm6f;P+wyyY(O=X99}xD=Y;nc(8}>Ydr7a4el&)9^wMhd7~JL z0vN6G1pB^r0qN3p#jt$c7jU!8+Act^4_qws!!AIPMtnNxW((@z>Arf%o2nYcP}^ll zSX_gtA+{jeCA0e?P;~0j>xwgd5)+hl8=`NC8_AfKKgxYCPO@vHPmpt@tWY9t!)$D%#G+ z@0TA0F{n5!iMU&&d{DV7BjUkHT$<6K7iLUHQ`-|1Ug`r+esA)l8M#XSE}3_DjK`xM z-brCYr8ZTcm(eiCussJjUr95~HuivU<}tG0_8Lz_R9=`7_8Qs$+oOe2@Sz2yecUBg zxth`8b?(N^R5`uVAI)X!Lavm(xho4Z_R0J+x#T1&Y2pW_;Zw4QaA(1!jyn0Jeh|e! zY`=MgJnE5-dZN1&smANv#Y!`v`Tny-XB6lUjzFWY!P z)v=e-K{a_^MvK?6*XyWPoiH%iIrWahj1HuJ?(}-sxOo}-Sm_p0pEP-R4|H$s93%S$ z4{!528c_p3V*hRiv*)~spaR!f0C{UuF5oH)mxVP5Ue*^e%hI*Tc=unB*}}+BX6sta zyYq^?z^^a5JR+}aZ3HuiL}s`PiZ49TD#Cw~e|}HrDS*JuHgYBaO0iI%1wg;r#X{A1 zvj=YW;LQQJIe<6E;N}?Kw8BlRMeNoj^bcyDqC-VfRv~j(jx0eBGKTOf8(%$Ij*C(l zUY(2O$}WdDox?K?m@C)0p`s%2%H%N@U8J07A>?)L@#$WETy}oOO84Q0?609uS39W} z(;pUI6I$^W9XkxPyY&^t*JHf2lxvaHd<$fSMay?4;r*gfn@06V-%Mh+j(XGDAAJfS zq>kNM`y`$chPjy%SV^VO@kD+6*&Ho-z3hPtLp&5&I)3XiDu~_B@Ec1lo3@$N-4KaY0x87e@v~a}1<7N@VPUd^i zz5#Jw$SZdn*|(~^haU-dox^qHz5sx9s`b0LFSHA0_cPzepLg`~s`}Qat#k&a1*8kU zTjn4*ruw(^qeWP!!Ygb0qr=1K@ndWw8r{E1S@wkuYDB6((2r^~B)<+*&FV1Kt`1WT z>oC=_4$`0E??dLV(Be8!rRzZDt^?J)4pjI$+yOK+S7B={VExk#Z-SWU^)K2IY4$p*mhJ~-!Yb;pJp3Y)&K<) zdX2h4y-{NdRDC_owqeuItch;#9zeS$x^-!A?=QyM0q@2`ox;hRuuJDOKzP0~^SYpo zc$OPGlZa=T{1!YH0A=h{`6e7G3@B+)qV$Ca{XcBN*09otQplzC&+s+xQjD1x&4ysK zF|242Nzodxq6&DURU&{`Sh+ume8kd}p?&qK!C0>x4QSqvU!(0B{#)j`Ie3?mBLGAf zJ#_R!e(c~~hP476FVoAu*~X+702kZ8ALq9)WUEfiSEWu>>QJRFuXMo#8n5%RIeM)0 zf+UW7vyGJmD@M+HxRmCsWed*2@Sj(8Y87!;D+bttZdjl0>+t%USm}Bi$!a(1dy)UX z2Dz;ZsANDUtO=C|FJC4Y0uJm5Jf@0AEGuoR8z~!PK7yH#V&*?yMsUK+Cor@5NTqaO z6CfY5Z;N&#D77Iohp+16uR1-^8bzxf@H+dL|L26g3(mk_m!ehTh)Yk6Mb|S=p?<83 zU5l*WwIPMqpg#?SRfYmcE7k(jzi3~pl1FJFW;*=X{uT2F9vbSa){-=%b4_6FaN~jh|V^wm*_Jo42hSp0!4xme-%IS zIxbSUNpn4rnf*t|?Gn!l*qPIVn3>Z4BNsb~iBHwq7_Ss_(A2p_$3iL!!_KywQ> zuV12!@DvuNI8}pKGkR&Lkh`m-!m04t`&22N#cdFcpkh$XfnTD8@%}CpGy^&#klmsm~?=MMlB&VXkk?Nd!(h}AdV6%T^^gC4?2hrH7Jf+YA?aC zm>k3~diTMW#CM%EVIQnC>x)2{?Y9ejO5LtV z$isGrNEm~+y->sA2(BHF=hhnP(-N7aM43HEqa8%)4FtDtc=O{9KueIXT&!xBR3HNv zRKVt+1N6TL5A!+-*u9tV6Lj^QDd^YN$R4E6`oy&W;6VcLM1MaO!-%nRb1(+%xE>Q! zcbKzUdI0aR^u2M={s1niWhMqJI!933g)lY@CxH5>Sv9KkAr*7^bBl?LEQYeiumEGj z`2QJN*-fjBR5&Z%Jd7yb)2K>~Llge@wNqq;aThp(PfQyt(eg!09!BXK3j>FaPYNnj zUWfER={bxZ&bJMX8c4#|o>Po9uOXHgXcfYZ`HBHj>8*mP)_-HL}gLjeg}`GFv` z(8_&q=SV34+E;*JSHMD1sQ`#vAMS$!#T_L7pdLJdbx(wtf=fcXktXa#9gT|Arj!H` zB!}ita3FSP9PCgL089#uNU50!BZ72-r8+S7oED6rZAVqx*&Y!@3I@W7IFY&uNp{iq zfQ%oYd6mxdT9>0i3k+f#QfMPMJm!#OF~5rQMhQoWwBF=p4`tOUI`Ox3^aol;gB9nG zif#hVg88!~N*h0|uP=(WE}Q6!&F0&i;4OCNJEwZZ4WjbGaNAzohz<(JtcfSzhz}a& z0G7?Hy8zhT`^tO*1nyhyv*0Sn(&_=5YGK%IKZu1TI^+x%X2G{-fxj9~|B-YEpt`RP zz5@Q;7TYmBVX{L-(}%4WsWcuKZdKoeoekF>mn=g(Bw%`2l`eRq`+QBWw@k)1?P0#79BMwG-q$_^1^g>)>%WTW}nuwim5v zd=3EhuieDq0&RBFnrB}fTRLCfW!8NJzN=;npiWXYHNj&&TX17x5=p0%mbYC4UxwWW z%^h@OW>gzYS~`)36yx3~@6=k8(V^874EtN#{shZxX!|4dv*Z+c>BX1B?U^zggb=&P zwif4ZEo6X)dia}%I*nAg4&_nTrl3fcsPjI z?JoTn@0k6CqCs$R2NW71=r`)kY;S?!Mxjpb@U2vJUoA6!iAA{^09Qb$zm#Y#+U@G| zUr}`5Yl{BNIyVo)=vH|6kt@&V+ika#?(s)(U6l4iyxN>N!dH&F7<9Vc?v;+CLdz|6 z5JbQLpbJ76(*da7_ouewQSt1X@a-yXVw0_1QVZ?wp`rA&)_ZAn@UCEyvA zq0(R($=6G-w^&w|UQ>K663zz`P++8fX5#m8x(sVQ#dBI%j}GZ7>M5Ml)AX~cp$_HZ zVH*z(Ks%YkZ`lpAu{I2C)mPZA6%Aa4HyOz*SNs)~arqlMD6pAzgE9(ZQGsYYwVu2R z**(T)8k=i?(#iUAj2nk`{_8yX8flc3r7B_ZHqBKmKK8|6Y~4m68w!<|@E=PVe6v z`4TN(7udIIXiZU!Uqhe!B!%}qbu8GMtmHQWUeQPxw&kc`oYvWgyAk-Xkk(jyYx~#O z)4!t5@xXb?lLo|fVIvaJMi|(8@J&hVO$jvTc@X`Z;BT2CeTYGaF~c}<&?1#?tZ!kX z-ooP+L2)W0m2PZKkFngGr9*Y7v`Mu&0R?|nk3I>vD-F*{Vf z0^ZPs=j$yr?`h;eA$U9~T-YE1WNPnGDLxluL>qOz)W7pH068rH4{S3*`%0}c72$dM zwU!tMkC4bJ<#gRMQR}!#s?`FggOOxNK-lEc2Bvzy1jWP};13jDBWv%kD5ja3BD2ja(8++ja>e@;+i^1#pS0rS z4LD(eoF56x617%dY0K~!YcWq&omPosfX)b@w)6%Zxl=eI$M#b_!ox&>mE*z6Smez` zvB;aeDtIQDf@U*l@2IS9;7fhL;nDif?k}5$1+x2BPn(Lri)z`T29SKFVQ*OK>S-6- zX}L;Awzd(Nvcm5mf~{`H0&*>=Y+~OkGq4O)-~*7C88qL5r~M>84?v89Eu(>)i~(9Q zZz8^0F95^9@kg^wa<2e#6v9UM9}i*Q)>7YSm^a}tZ~cti%6<~g--C4S-ID@gaKgeZ zFd8y2b|!$^C1&^7FR;gcoDRhG6@29|-mIsy!D|8ttGYZDz_x+FR!+wt+XT-WMuG)8 zh&ZuZvbcE%P9KuumHxH$uDE}7mbqhQdY_Xscb#6WS1zcG#gh-{vuhhpp^ZKV#T48<6 zQ{Ch7yO(2`YW(54*vm5WrTjh?RpVbbl&PaG?hiYq9(~{@uK!WW>AHhj;k((TS7Jq4 zCB7Za{AXZx+K#0R#fx@3TYO)ITrVf)!X2o#Eh$LfUHsMbdORk*Q>0o7m%vcal!$3m zxIC3*is{QVr|&VEz6)#_%xtR1&Qc|Fnmz$2!C4DWtCs+s#2ksWlf6q*R3LMjF`ecJ z9&p0QWSG!b(bo_aR`BcuK8gF<~9n3v4X9$|eH4a^ldOEKi)1q8Kdb`g#E# z$A$*$rwue%o6W&`Q4H4duz(Gf_lCWYL!U8`Ro8GMa#C=l#Kd0Xw-O|_F#jq% zMj(dIr4xX%@&yr;|6?#*+B*svz5Z{n$4o^}1K=P`_?rNuMV=(~LNGm8+zSK0LK0gy z>(0>`iSb8IA?L9On1r7qw?OaFWg>hPDj}CKLzqlHf}%StmSSki8)XDVn~)H+eH9v6 z0j)%G!Hg|EakONm{)Ba&$lyrG{4q@NCION&hd^?)36hGQwYA?$G?11yzGga&OelOd zBv}NmiFF7{K^hU>6(Z^gGmGSnZAOZObVLmhuX!7^mzCuKtoni`#wt?CP1UM zqe-R#K!W~~8suU-&iR`tJ1gB5A8;(5RKd&W90^e^XopGV+HWqX7~TRJ?!-!6ybTBk z_S5AEnl#(Yk^*c;L$Eg~&&ZaXT_!)pR!yeL&pGxwyOc4hLs9$HR+yaFaNs0GweoeM8ijm8obO0a0vm??qG zQj4pP))E2Ahf5Dy&15yTf78_e&NUD5*(m^uT|9RB=|b(sbG~`vfG*X6FqWF&mz++iZkVm`1XE|>6Ofi_Nh+lP2n9d>qvU;GIG8z^AFsPOKw!{ zA^(%dHfR4p<`?C_z!=xmQ?ATYxomT{EcJj)?si`CeG*%iZk&0?`|De9e#psop7uVC zgN6Cq(4#3Ai6(dzOS<`wal_~(^mXx|H}yc;i9nuieQPwwE&h_Vs7;HV)4Xfn3|xk zL2{rQ$l-)<-aW*z0R#6veB^;4_7y%)p=I5E-S=Dj@YLMCv+z^SdxpZvKlJ%bt$-hP zf9W(k@v|mhyQmt^7^aq*{A}43{46WY>=}wjFT~7J!!Gz))M1tm<7eCYx`znu>Og#v z4QiFdjj_^OyQ0yQT2}gvE~KgEaw}+YsywvZN-#RK-2Qk{d~pRP)LeOi>rr$kZ!+25 z@WVj~L)2lH1pU=3iSVjH5?Jg4?~&xLOlf<)6H@L8qRLYnv@U(vn0Xlwdt}v-eTogw z$jrl&G4}!QeqWQ&nvw#xo>EeV-l7<6S6A7DUPt6X{SK+ZoWUh;Iy;wssZE3t!U~+7 z%)l*CUe7xmdllZOok|{N|7g}9FD;1U8r|zVtvE2Uz`#@gFhbunyuIdEwKPrlRl#GR z%QS?bX2&z2r35uQ5X&Q)TC{{Ik9Sdf+Z z$&1fGEh6L2!J13!4HS;5^NpjlJ#<-F?eHY)C{CN z7=8}A6Yq2-woK*c;By*j0C^}ZJhKjEMKe#1+5Gb^e##LFzf>oVJmDC{^5_Qk3$m7o zJWo=dg_MVM;O6kLTWi^hFC&q;m3qULe-#h>6mO@A_uUX1Qo~na4r_Qf6m_LVAA(h) zMc<}Mb3bCF{(QR1-L+^wS>A~&EGq81J^ZqwyK9ZPqtL(QIv1~3_T1@gKZwqph1W3@=Fyl;B6T@QQEEqSzBO@3yJ;(Q>${4(jqcTOLs03L~-DP!pT z8^J1WXskIDA+2f${qzq*SS5eeMPhPDr>aQxO8yE`r%C9mwgta{QSvwfP+&NMPGxf{ zAj?kDIAB5z0Gy>)MEgdDCp-aQRn7kcmO9&*@+zizq_cC3oIk1gZ{v3}3~Pq6&f^Hh z&XJoy{OWs>C3en5mCoO7WWNinF*bTzl0}4N`YW)|`U_Zyu4e*JMT4q=e%??t#frlJ z79zZM7o!NV^P@gf-?DK0#3M8ji0Iy8{e?`zH`6=)X{`OJ8v4=;U;cE|nZtc_9256> z$^%e)?2F3W=#YNyRUFswZ1_-rJChLHJ}M3Bg!J%2XoySK=qsuKurVAF8^8F~mR-O} zyhob$!m-+WMB)nX0+J0iEKZC;UHS_d*W%Cb zgdaFDDIQmFkn;o*{#w@f46`SnJGvm z_IYT3WW;i{N-&3YMdM_xH|2@zfkpI*c}ZfWXr|qEUK(-%>Ed<4wq$bc1uRvo&=Y-$3o|Zf3s7} z1t|==^)*<3cyu)eU)6}#^Puovz7NV1#p-6`0zD{1QKtYYP?gEspy=?HDk{=$q5>;@ zTQD}G{s3dr)@vbq`Jw_P{{>M&gGqfvuK|^@U^-HqYmusO@2&I=Q3+x9VbM6ut5XSv zElJ6*^BNgtK+#s*B|!LxzzBY|ChCQfsd-X_Qx~H1cN5-2_}fwaE)!W z6N{8wv1JrA12ZsVXCTxF35LgQXo@Wrs~NNwkT@COI2@o#1bc~cub{M*LMlBaa!ourQO|00f*#O z_S;TRef}p;s7deG-PfaDFu-y!tTzYS>%6zZuI!=EE_=a-g!l2A^b=t`DfJApPM{7c z?@LJy8cZ0g`3VwbzOQ#EryXf~&{2Qukzwll`cMwa@He}Adn9p2txnTf*D~|fa-ArI z=|x$jQ#oV5c8Cl9=XV$U&!)5At|HQN{a6sqMyc$|0pc>dtsk_$&4*=kyK>Dte|jHi zkxY|!(5<}6K7c(|;;I@Da=a&nrP8w@tP+=5%d2i&3_s&lBge%r+7v0Ly-FEg)Q`in zO0j09@7HFJ+dh=)b}t-ghZrOw&ZJ5yS_GH8@$)hNnB657W-9vd=7+%;icn1RIG~eS4(Bk#&+;)cLP&U@_r$N=aD{gkR?5c^f zc5$Ec(-3XtXHKa+C8n*^bv{J(V8T;l=hF0UR`#0xU*2Qa?Io1P86O1t%w7-p4V^Ef zrxWjKV71}XN|__Kfi)S1cBMnM{3)74`fy61jeY-S4Tjhh31Q( z{m$T6$8(1hMA}IH^qT+{lrY!H$pE-O1I)5eIot&zGwyGa^{R2|PMZV!T<FIq(7S`K7+XCLtr1FL7oJG?v`!A@@$-2-$EFOL(k+UR)5_CuN4yZRK1 zj+_#UjKysR_#6zzysbFFn>1=S`UI_&L@ zJRBz?8-MqAKHkE&Mb`iqZd*LaL3GH?(4X@n9&9u{c0)N4z59f!|D8mgMq8AJJ6iE92ezn|lmIJNtz_Kkh!jRi zO-4ygMy4iVe6ZzUdtlb1RL*i9CUvEp-1Q5n@50)<9nB%85RxSGZK^H6N>EDWizjW~ z5;TbEglCe0b{Ci_B6n&<_W* z7BR93r#$TY&D7%BX}-E$m){q8U)A!8QH{t+%pzv9UsSd8*_m5Kjd)YN7ql^44q)6R z#6z+*E3u4&vTMw$RVPzc%5#lLtSP#3ms|+7u2ME)*}xjMf1@tK%U{8U8xtoJ&p?~z z&RYdL>T435wt=^|*r%3PVut9x%dPDpZ)-@ngBb8T3{)jz1BjivL&Vdk&0U%dJ9uAM zObz+T-T7g+CinuPpuOyN*Et@+Kea=QgOJIhoX~tNDxSHFXyrEdnO03RZb5rjC7~9F z6^eK?|Ai=X!D&8wD*-j z<9kpHtg$E;<@oZMKE`*C7`W4-K8NwWNZ~srgI*mrz6tZSykb-fatb38;`0?4-x}dw z+oqAoLU6}?IZB_spU(>FgK+VN94gH))ol=l>FxxP{RJ^wBKs?mbza0LxkdbRH}$VC z#%3ya?IUtRkJwBb(N*~addi;yAocel5LE&bRlS^xM!>+_TshuJQB<`k19oQF4OE{fV$7pZpJcmfhwBR{yvXU$Vmi)!qIYW-L;BEo zTNGxBG#-W3qsLb=sz={~u7c)sR$?6oWmlVN;JXy5wX;)0^u|hw>cH-=KZJA}rXHSi zWFuh6y&=c%JWZSVNz+ymwk z4biy17w$=`qD3AiKiRl&noo4h7+@wBT=`*re^6Tr2!{mU3@Cc$=I;?HDdaBva5tc)?Y zOxry5AGMu1<@ja!%t>d};p3e`-nn6}0849C8rU$0+t;aFBDP~2OBA?Uqr|HVBG?RM z2kXyL&ZXO4ij+M!zfiu!7j9diy)E4~>*mOXp~$&6M=l9P7Tz4W6yk;M3cbzL=_2$4 zOA)<_nga)3g=WZ=s)M)L3d)g#>nGwfx5EF3VR4~68H?S@h8^2or<&q*a-%>nrVxy# zpTjcUA*os-j7LQI5u%^turrYcvYWdTQ^muZdBzy_A+eZwORU0BxD7|4R`IkCs$W*x z4_idmE-Njy^enfc*63%b0a`^41!nKYlOqblnzO-lDE~JY#%dle!+rUs{Cu#^-yU}V z4MpY$Hhg@FiTgSYcPEWtyD(m(#}#zahL2I4Fn&!#PJ!8N{NxBOE>I5i{|IFICYO*M zy|YYCh7-#8s2&J897r??CDW2(7CDQz$T7%XERP9tz~LnhE0rjpS|;~i+FpBX74W;z zm;WrSoYF|dZ_D_68HtLMl2E_n1O(?KCkVhv)NeJy@v>_1DsAM8lR0U_O-YkONf{&L z<^TWOd{&6_lkl^`qyMdo_^$Joegox(Gox0X3oNN+H@d z+usUH=Os3nI)_>*b^8ePL@2Nfc&}x%VHx|E4Jw?{$qLBC69GOD8m{5B$*#1s_q}?`Bl(LF)c!2letLCe zeV0B?s{SphkCWwl3p zg-etcTJSul_`2w6s&S<{C*VHYNr5E@glZX8T^LV@(uQb0ae2B?l$J${%r^vzG^XmtmTOYt z<^E53b&QKFX%95t-^HH}xGuH@_>A&_&v0x%4}bzY;o#`vxUm1^mamXd@;Vqmo} z*1cFo8!cfx{Gy*${pKwWrDRAg6z`I4N|`OU3Yu63jFcCN=WyS5YI8Lp!31^V2W{QXXYA@k;;S~Y4|$vw6|HXy1U6&AFmE#RIf&EP z_~P4qGV57?P9D`lJZ25AKwoV|(#E{^q|D3|UV*-lfn(p3LNW-1y_)~rg7aL1x<-Ci z;iUc&-w9=@4GcM+pElKV>Kr^uX!ds(svpR5ae``Cx@d}awV1RB7hBpjQoWQvgVDIG z(jL&EQyXIoRPnu+yUpkNtG!?c);@D!yS<V0-aYzXK5bBQzI=qV4=zOZe0O zAs`)b@W%Ukm*ee(Fh;K{#T)MfvkntiFUso)U5X`uQ}nu;0T|Q`T_9I=fgH3W2=acv z1bH*84PCHSNmxs}U=?=3O7R6@9Vz)XSWCNL6-rnZeiUW$qj)MmR_6zkA(!#NKW_)T z^TcRYb1jMOX%yKl*oZ3)5}x8>Vl4eo*^fv9>3E1^5A%xytfx9<2hSP5OQBoGyNNwBV* z>jul_3&Q&MZ*8N@!mv)AlY3K%^M<(1ABVPJan?=kFb5^0qc?pT)8O06x}w3LT8!daeH#fwR3KVHaH76bh= zQ@l%6)K^V-3TkeO{L+}?gUvCNuQ|%B?4=g>YUC;GlRmtgY`pJHY}2pK=|iogWe<)3 zI`uEymR1V~7gFK=xKz0BX|F$bLlFwet0;7}oQ2#uLQ-xDG2Rq1pnC{w+t;|(K_71r z?2B>LbxpFwu<{1~DJ+-fA3%KU=>u@$S$K$Pzx}e9p+<7RjOb7N815Oxz2x`tJaOYN zJ)3C3iD*Q^OO5E{85^%Qa)LHWa1oJkrE)Y9Ze&i(rLd!y#VZ<+7rx-&8wjQy)+?Nq zKop&j&*C!@)Cr0|LeF!-vWjZ8vJS}v?UVouVMKMPtmC1wB#@iRI;55Hp?s-`) z`CMBrL0_Tt=kwT6o|ti+K6c@wjsPBGfR_O9O9H56(8}pR@IDF-!c||_+V-Ya&i`oT zyw|OqN5pZ;f1@VYak7xhp+aQp8wcn;2&w3KttB%x$ggUU=k?R5MVhFNK zExSyEr2ayKJWhlBOAT_~H`V!Y_d0iVOkNmd4N}p6Xpn!ZK_0F_9{GQ&^T;sB8YH!k z2Dz^W`3D;0`$Le!D|kK#N=A1zMFQ>1p>ry|s)62gm3cZ(0=>{2vrX|#r&)z4=JrN- zxYF&+(-om?n^(o$qb5`-Mbq#yv?{Pqe5}R-l+K^0V^C?n=6>Di5l7O)aI~2JNS=F( z6_6t4#5;3OsW!uKW#MX&F|GTEOW?RY92XxJ*ZfIO`lXj*gCw=<^f6;~1b;|mR$Ch7 z2aVYb?rbu@Pp2kwjg_&TW7f%~CctHVb<>rmd< zqaQxf>aa4rAC`O{{qUYvhc|oH;k};y@TOLWx#9ir-1pHB^Rzn5>{*9-J^Nv%R);C! z{gCl}^h2IjhjBgYkk_*x#%Xm(5ATOT-$y@;)avllo^=@6vma!N8AZuqO)oa^$Btb( zntkemH9HaNkY>D{!`r299kL5wJA)l^3Ck5`+xx@j#9(DEp)&V}b;vGs2vUfTi!i6@ z{JI*NtuWg}6=C+$Rnzo`i779t=vJQ1iVV#D1UAJ-Kq{rje1^H|%d?oP`Z#j~s_jM| zO;)qPahL-Ywqb8zvbzU~S~$^ePShEmsCkn^S^XSu&PrnNkbA~Yv<>6O3$bCWhvC&? zuy|t8LwuNJaJR8#!-CIUFP>2KY{c8Cd~HwN!NT@eKj8x1 z%XAU!Ok50%9z})D+!);51~;~ibCdL zAeQ>~pR*}kj67p0<$Ep5aw(1FSlg~?q)0egcc=Nh&SV^_EcLtM9p1!=C&6D|cqOr^ zJqo7i5-l=7EILQu3o0$@&>hg;;WLMFcZ;6*aQDR+?^FFewc?%9-hQ#vr`^*h^JV1r zze*KlVN6WLCM4BUU4t4{i`1fq8oA-o&al-h{4E955N1_6EJ_n$L^6FW@Yv9_k1GAg zqa5lpQFe6%BrT?Jx?`G<0$Tx6)Jg(m)dkY3Smzm*6Kdr?=+(c-n=UF~p;jomu)@yqVNqqJY>A|7V=g_J7`(%P-@a@E#9Y+p|Xg4Vf&rCH56VULy^Qv=y05BWAoJKXK0jXS1O@`m>%I2TVw1x|K;H#OTT!G*czLxeHAWd z6nB?LI(4nKf{HbIA|}&S*Ix70GF|?H=05U#KJ6;LMWM7$S;7>l`^@o7Ida>@x+^xZ zb@0z@x^lZZl!&FmCwG?mkDG?WuBS^)wYg&s6U z3@jC+AnCZ!Q^?%PFL>Ou~!Rid(d{?*qRIm!Z_puR<<#Kt~6}&Gtmg3OtPXIR9AVwWyU;*d?M!8N%ir_($ z;?MC#m|Gg}FXDZ2aeA#3ExiA)e6IwPQ;^{b;bT^ry9AUKG3OzO92(?yw@TYF69fZ& z7FEmixTB9-{ytxE*5D#uqcw+@aZ8)>#6S;FPt1EAlH>!6Gx)(JN6bh`aHe znx4f#+@^^)SaJ(-ME${80h*x5>N4a%2Evj!KFI^Z3e`0}!rd7&=LwpEq+Sek>Q#NZ z;;(6QH|TVgO|5)==M(W!-+dxL>r#`cH_rtgpT%3!k5`DlVRFx{u*0Q8$rOo0|9uNq zhQrb@;0QG#CC#@L8~<%fc*?C7_e59;->tffW_Me>ML71F3)=W*T{!nylvwvD5Wu%Z zJKTD#hIU;wgu*{!Ixxbc;Rwq01B>n=W%>lg31>|~JMwK_!l%kR39{K;-qMr@O98s8 z7Nvz|LyOni&*D|5aetOzt6^$2EA19BrrKhvba+)PlvaXeRVQ?sP1nszhgFO@X;z0v zQashJlIBx~!pUel!4SF9EeemDJ=aIbD@|rb#ibw9VP0wA(TRJ6zQk6&Zu1ZL$X>N^ z1%}msu8U}X7)IV1r1W#sDxJxb%xHNs_Sc6mmem<QyB&JK$*S3zXk@mqt>F1cGGoHm>9`My(J+qg^dAZy+TW~^rWJAK!Uc^a%*a*IZ&?6$TnpkZb)E&PMyw+K?|Qv*UzVgYsgu# z_)|U;YaueZ<;C7~Om=i_iSGat^dV(0T|U%BsYvq-hK(2vgU_9xrjKDJW*naQ6&lKpl>o?j> zKG#RQti++zXLj-EDo1Q$T@hoUv;ix}ZZ6a#y^?7myf!#=t=^>`hM4&e6z-7)>HpER z?FQdsxLftEM~V6%o*N>PyXzUPCBnod)^{h8xspgeIzmJ;5Yp>oh&$;<9Wq7S?jvDf zhRr#`hGIe7X2ow+cA7Q8I`2kFA{xpO(c~6^;B2NEn;28W1DRS412;?&Nindo7{qWey?T8u_p(=Nc^Jkt-8FU=Dmh34U*`(Q0T1RC_TLn92k zuFS5hAP!d&7vmMXec8u`GuBG7VEmvQXfn}Q$~0{@hx~JH9fZ_18U_WWe&O8+>TxPJ zoXQuCe_se;;OkD}gS`w6EVq4CXmMo5DG&lbTx@kr(^-ZbpYwZ5>hXYI8aGL;wC5f` zkHIEnY7G&dE%k^^-3XQ>t*3tk$ZLJlrFai#gNE46;R^m;Mc-}sNd zRC#A76OCvqt(*om8d=9Qa+FpNovRlS$y0etY|7!A=%W|W#|dwK(Oiu<&_YQZ3B4$$ zD~M<#lTw8fMyadBeAQ2sF@h;$kED!qpp3a8%3#_KQAS1=Wi)s*J#&L(AlGKHb}uGKbtp!mwQlIEMAs6v#JZNoaj(&& z%<^-*h?+XYqWg6k2jMNARvTh!tO^b6YB_p-$x(E%6LC?Xk0Ok8kZiS8XRvOBIy8kQ zd2f;2Tor@F!B1Pn)?QX!i`7(TRSwg7U1>Je(UQK)s>HrVtB(HuZ;o`lM!bbT(x&UQ znRdE9xytCJ%TFWr;<#P6oknq`#D9IVMb|==Vt%2Tcm-3P7z0NksE3lU!1CjbS&|-^ ziLxkzU{;zXbSw)@Q*GW@O&-WuEj;OwtgZ|hpD>vq(BSHKz*as`6x`4xzRhgrPHXB} z+Rl45yx{v7$!(=U2PW~g72ujT%9*(YXh>UH@YDCDPz;So^Zh;VO9G|lz;>oG_DRw% z3deQAY|t8JgB$Ehn_am?i~y{~Sl|z%%}>!+ zTT-K8XzjlW62eP-gtF#6Q&yOLS zJ`=NPRVXYM=A0JOPvf8e7%oB9(>{dJrd;l6m}KrY)=g8M+T!{p;f7mkL$$^$Vk#1Xo$BA0Xu%PG z*4n#o9u7gMp_8Q`CJp}si=Y@p4fW$xg_=MOi+T~Jh5j>W0bXklEDS}mFz`Jv`^v(= zlUB6{dg==#JtPdP9)dzL-xSx2tfOLVy_k4J#3wxzZV!G02)}xHdrx`i1TX!p@K#E# zvpQ+U8iKWB={b?@(0a8y)*5cjpCTYg8@Pq5o%3C0K99Y9bA4l}zL^ghq@mBt;^``u zS4nj|>=UV(4_61pmk&{me~go$z02CW^SiNLIX(kwluXs9)$FWI{j?586uF-<>!8l| zK{tKvOR&iI(L9Xa0(WC18#A4~1(r;$-g(j*!dIHkx2}K{w=`=|b6yGBe0uV`TKK`c z-TPBKjfOl;4GCH@rS`^CdxPyd1{0<~M8j&{Y!57vhh7fV7sR<>SD!((L03Z~U1>qY z(+opI4_wBG$CM@pMlT~sGy9s$=dl(dRryZhJr?1kIZg}JZ3b^u-OB#dV4Ec>`tV*I ze5%>Pe5$?urZi;%JWY^g#hsI7#U0@|mJokallJc4`6g5Neu&EEEoXV9ccu7j_I;OR zz8cmN7TNzL75B5C?Hc_&=gTUi2D>*z_SAtFHurX8xKQgYFNUThPwkvJ>TsP>LyM2Q zU6-GwCOidMFSAl*mMa(AhxW0FXj)&KxwWiky^O^lpsBrVq?%-K=GJrYJE7or%(_|# zgF8)r*G}`bI%3N56U<_r4_5A3=Ukd@#!D}H=_S>gyY$8>RbQo&GEjodd2kt+xNm8D zz^>d-%{sHP)0W$2RX#Jj&qi65YT6=$)Zs(2!K&;x7o0@ZN2`NbfQ@#QZz8IZUfA-x z=4-M;O`46y!eAZkq#Y^M4kah0h5mOp|65Q$Hmla=ot07-sBi~v6@}+mB9p#pR>5Jw zQ1%o;ImP%>X;sA8>YvX-`i7u>IkojQ`kDTk9>UNLm1Ux*t9k;wo5J&7i$OrX3NPhN z(7b;w76DUL_$mLI{?AB5SNmKWXpYbS8S!!|Z!C3R;RL}d4Hk5_=U%`z+|jeg?E#;Y zX52=4KP44)ce6F3LV4l?YNgsY694dcNS|SPafDb`XUpAn8O{!=<57)MsEYK;m$lmw z0tg(ZR&4z)CmsL>Dix{Kh?>-FYJ@0qBaA78K)h(i^QNfO?9rb z%|%ikao{d7e-UrAs1rTJG136g|8B9U0DKPZ6k@GJflp}tnHFFdj$!g0Ad=XvQjIV^ zcB@6z4}tOe;hMGZos5n1h#`v9ec?3ia_y1MfqvKttC8zyZ`9TuKvQp&OCx-%3!Vhy zUV9zVjQ;M=`cKOZLxSd^=cFVEqd29NZfGy&Z0 za@IYJ*I`fEA%*j-`kvYCx!avehh5p{R0kBYV)@m^fjwYBf_{|VU4rK6g@+xZc{g%x z-R0>PuV+h#JY4gt>!lx@0p+i&Z69pX3KIb!^_2J zjEKEC2iLUln{ve7EZ>ju37-l(e&1cbU#S7p0HZFiM@Wg@q?u_r=w^R~?&B>ERWH~X zkM1H7CNTs1;9}KOZWgcmEQNki_)ADgRfVswi-Kzr4Bt7}nPnw=rquIyOz6AIb7f_h z=SrC6+ia?tC7Nzw$fg>h-jFl2Qf*U89jTDqjS+A7?MkaXwZ-mkO*V@+P#IS=kzn&U zNK?BTT7Ry_#%R3IXf(Y@;uG)$J?2>h_Jv8!jHXH?yz7z%My{6AbKUw{NWIyPt!m_U$L*ruR)Uh`y(} zCyuj*&(Q^<{J`flrDWb|px>lBsT<)LzSC9Ygdeh4nmn3DcZS?lD=qf{o$~L%E<7W^ z*C4m2RW<#Y8H=WW05^#*MMv={p&6cFGh`cRT-&?+OB3z(!OsM&Cx&0f3Roa`|lc>z`m0qVKK5ntxZG5 z^QMcl=a`jBOl*XGxZ1hpMZz?H^Dk)5f+j4rp;rjg^z%V3L4Vf3uLX=>3%c-Y0pr&K zsz8jxwAWQnd|iH0aQ$U_k=flGE1m1^uY;!DmAY`IN3pi1Re5UJowpcM8X(RlYSyoC z)Z7ttg+X220{w#FVuJp5`f=PIL%m&{ClY-$@~MUDbJ+IL+fsE3yDIAAr?@JFEL4|h zafRw!oH7vsLD66XF{0^qrPA(i(Bq&Qo(6T5)cBYl_JkH)mRr~O2>hhCLSGOw3c+ZA z{Tx_QwyBN(m|Byc%g*wIk(jn$4gMbb_@_rx9arn)9vmj zgQgM9m}{{G@?j_Hf@)?N|rZXzigxu`=&kMCyqAX!j_g6JC&W^;7`Y~tOL@u6&ic&&3Ut9q>@BgcRJMx zI=lP2?v=Xc_tJc(n%OToa{WYhw%l5WYS+=oHUFDAuuF2&@s@Y=BbCgziODLpn`-kN zT;{UjVxVPy$INqPWmlmu{{YQ#HMym8m?D>TJMIHGbe**a@e=PPhkl!cnjjE`wFgf3T|g6IM0?<2$)Nx zfhOUIZFqNmXTsk`p$?v>mHpPdpuTs{@^K;LbI|;8+NQeJ zH*WcDcXP3=u3ohJ&Y*61{nsITkK=IiH^@wjWp^8ojW%_*uW|ElS+-s*0y`sbOnV%h zs0k4~khCQ1Uflh>1PW7*hgmNy^WjQphCN*yIyrfP*4h^g!Erqh^!@VO{4jmtZJAC@ za%gAIBb1Zj4Qr$!c5ffV-m@A%$_oy0SNk$*x-KU-W51z^ zO^YzacWd8D*}gU3-9DTQ4L}1I^XSbDwr}OPwGXvXBD!jm`3<#sdQms~0pHc%ju+BV zp^RXK_J=Ce>ER|>zOla@-gK=(CvNWW4B6qxrmTI=9oigLF z=;6F2IQVzG*AXO$ro55L^xRH_#=aLGI3O4I0Z!9Nx&O|8f3!LPyBOR8{4T<&I}gHI zxbg~5*j?E^_!W#aF@J&D@8RmyQi={fJ8M8r3jA2QO!Hh z$?D*bpoa6QriOFOtQ;}BPq3oQ>{M&oRkpHDkmhQTq7S+yq-qUP({4&oa8k~ISHtWT zE?mDPtee`Xo3^;BgN<6h^rpDlP~7sH;tqu3R@@YKh&_Fm)8u&BHQ(u}&426(HHk~I z>6%BYgV);&%9lul>Dr%+kcTM}&T)d1@<+lUC+sIUa+k5Cbxij@R({^^K1m%y(|rs& z1oIt^^a{>uZN@mi=N6>&p0!m&P~dqiX?qF?t2eJCQSdql4rvdvD@ zHP;)E2z+F;I+P2r>%QVP6EPG1qiRQAZ4dOs^ij%yU$b=UN!KYW8v0qp%?(9U%mpXH zTKT6}!`2ta`i$-aksPXRX7`96gd(!KN2G)za=J&{A8OyFQ2P?N0K4v-*1imF2Iwfz z+7~vSqm-1FX*?aK>#kEk_0O1fzXZ&N2G}iW^-Z%QLhe>^wHZC}m9Y66`?=*oa2GAg zhT?Q-3dXTq-!0e@436m*Yzqd*b_;f}jp8!epB{6q0ICkRse>6bE0!lM*ZR3;P7nLX zyq^Cv)JG$gp&!`;<>;N$X;I z$tN58Ol4cW8Qss|X2JY-4|m03HIYN67R#D!lr<@l07F2$zcpdgf>f{nT}_^RGFX|$ zmvVIxO~0q8#@i?L*nZw@f1Bk+%7}5XcxNYfJFbN?lttCgwSrC9#{*uWw6gAFtH!YY zAvkq0(`cEc(N94TziLJ*OFf28HsWV+6zIE!CxpUdx`p=(g~xUaPvWx&@U9iE<=|au zdLGKqZMv_XVGA0GORFNba~00+ueaQgR<}O>0~_v*`UaEPk=>8U4^Gesr{Weq zHF+_{=B|kSWYtgEw$z;yLSz*l96`Zd_j%sM{rcm+W>k#h8Q(pg=CKaN2ki>Gzct0I z^Qq71((2terV2~5#k)9xk}8=g9Trom7~N!5D$S{t!qQ|`ZBxy{UP%OIG3_zKUr*|e zPjWG-+0HrC`Rn_`pUdY?<*?mAa3Mr>fO6w#XKtt6f3_F!By)(K zuh=H9t)|mfG3K;I$xAi|%8CD*s(}-pX6okkIsol>31?aXi~6g8 z=YmCyK4CTO7KPBZ_Kr(-=AO6vPxl(-oo-CFn9flr{5n2&fLa8mLOH{-qq%N!%92(j5sPV8{!5h2k$d@@vRMlQu59(mfTun@!>Di1Q0Q}|sw2A6*CG9#-jT_+ z+~1s6N>Q!R7WJwBcHfw6 z5sqrrs#7NxyKm$QM;#UJ%sql}MICsW7?c8wSugXDo+e<&?Nk~MWx80~dTM|ud=)g6TLwp|IG3Llk-`Qwb1G=^ z?L(y4r_>^{%Pnliy3cltnB+4HBW)%hi7kt?2;;6iE4@}mnnkH!muD7366%C;2i3BI zn&05et#z<(0l?0-L?pn8u{ls0tS{>6nI2%(g9Fq@;}V3i46sXe!U%o8drufd>2{qc z-;enw;fOHqM`+?QS9`!fQw7)Kc2QcYqn0L?zRD-93H#k`y~Sdj?Re+M_QysiBj9c| zm^TYvDI7b6@d#fr8`%d^o6eOlZG2Yf!Qsk@pj_LLtBi6%b2zqH%XO4<9W}d4J6#JQ zaGF1iOO|udm}=;dX;7v{{HwCrWO<(|F`jl64;-gGWyQ-Z26A7320~=v8os|ipCvp_ zvgI4D(D-_QI!xnJrURI`dx>3Vm+gJFL(wa%THG2K|Uz)kXTS$5@$&FdP% z_WDl91C31<#d-j8>c=~O+cMdfd)4twxm-}nZT>H{1*OFdB@n0I;@xJzs-ReX7R7H# zZ4-;;@ji1efgME9mUvx8dMx+4`q5*h*JZH5=hAZ9GcB^$T4b+vrs&EkX1z^$YJp9e zUFq=tZaw_nHmB|~3&);o^)@dBDrc-pt5vxea4iYAmIqub1FkiVqfl=zWup{e1+2QS zl*69-S7?o}9{?9Z`t&0Rg6#n2K4 z+B8E{#@%-7wp)Rgj#hfV%lye*yj(b{>1(Qa>s0yG;vIpz#AiYEKk(~@&3%jpxMT@2 zDEapcoBL@*gzKkyVn$`2c)sQy>7w!ZM{}6Gj2eoR-V67o5 zu`v67-^AyFzKhf)oa6y+8&misn!LBeI4hJdVf%`|O6kkTCaU6BALT5tTo}JP9}bw* zPtjgmt{eP_YSXD1#Wr^+Sqi zc-w-$nng}W$L*{(sb?M0f=*u)p%&bm^|I;eY|&uXG&glN^`E;5*e6KQm^l_cs{Y19 zL*t{x`A%pff^l!)rY{V!w z{`KA<9?0>hZQj7>WZb>-h}*m+K1Lxpx$nnsml|h2DTQ!)u|oz7@IaqW$~vAli~YNIRXIW=K1oD=F9t#ZxtL$M2!hS%MLAcs&@DrZtewULsuz zM8CyZvMPuvI7GZdcH*gL`LHnSGkjHFwuYWF#@k=Nz5?-g~eL-pJ zDJts-{lw?QVAIDfU3lA)Tez?QrTR>^-3oa~bcgag?vCsOXj%WyrqdV2?WZz#(aR*% zHfs<4#5;tZvhPFM-AICJp-PhgC+i@RER8FaPChKR(S9I)gVxCSEnEQ`ErBcvQP8#} z3UWdxVIa~7&KM%7-!8$L^VjqOv#PBYbP1K+NiS#da@Gn z4Ev3irGv{u6R)FUgFnP*Tx)_=0@nAWsR{6YrX~M^LauO`1zHI_P?v*n@9U!_dx1X8;wkzccO6~Pfy6CHd<Ka86_#E^JipXM| zI|uNWxZenu+sv>}OD|t*6p-~9`=1-QQ_Rn0#ocRSUNyUXr~th@Go9(6Ttk)Y#0|fd z^l~VT&~xAp?yhjk8TP2m(aEKOa@sNVeF$c~Nhc2-i(trHl-I&KwlVrYUI0O**HZEeX z#E9~%lq7L)f& z$|6JCf*MVk(SxPq`a= zhd9eje0kU3&3?q5KLzvjxXYWcN{MIADUjE0~plCY@Pn=LY`R!d)s zz;OxP3b+y~0KSZQ-nj=u^Mu zb@3GcT@d4`D6A=1D{2|`bgYj!mSF$KYq97H<3`Do8A{SyxzlMqv#{C)ye>`h!wtxn zL5CC;*^Vo?z$>370`R+FCV%hNYM~Du0dXdtil_=ZvG$srO63(Bfs|676N} zT*<|+*S=T#w3ng`?M3a7{IVPynB#6`r$!nZ%_Hbk^HX%h>c;eEcdbzlb%2kEm@$W4 z7r!KdWer>ZYl&ke4I%D(LWNV=*Erd(Llq6Oio{{fnc5z3Ds4{P4Jh3rkcMvPNHK*+G*--Js>71~xyc-N$Po1FL7Ic6EoA-;fJxn^+@rO29i2M|{M! z%VI3`5som3S%V3rgSn_Q->b@Zdu}pvbB`dI^ge+QVG?NhKJQfS zl(Urf6#r9ae5DoDmuKJeG{BQD0DGseo(t~lwB6m&TfBOsopCy6)#@B5A%58|>8-dy zbMu{;%XaozGsv@~O`C6}UzxUFYV)n*mtFEd!@Ze9^va>sz0xA`AuMK?_^LrOSfE!# z50-^Y1m7ONBKnkcOOBQ0uw**o3!|5|u}OHnQbYSyo9i$C=h=YS87gQKmha6zIrFIp z+F1V?+b|7(&EAChsWq~)HC>hEm2P68NA?3SYVtyEKO6j(q46>VRrCd(|4115Qy5i{ zzR2+tP%)V(H+tH1aC+RQ@P(N}Q)<1Kd>Hm)jfr>9kj7;voD_ZP#f17iVkUmggq?Mq z{$XXX_o?1_;tb9Yd)MQQ(j+wJi6LqI@1MeFswVn+_P#$&O#3 zQTA2(r?`Z#QS4j%FU!g3QBFkr_gKzg0_ch~-Zsa8C$V$%JM{orG)qk>H?Geg#TYc|K zV!*C~HprT8!jEuPE_h-nz^BL};Z|k!0a@hQe67gSTx2s9d780^cjr7G7-d0~U;*oQ403K5>4#H#l#ld*wUmSwRtcyeWkojV|1Y^Nt z@Wr%RzH_wE4_*L*x z*sRkedq)a?XT`mm!?*#RxZ}dOhtnGG3h~LzHp;C4Wf~wOI59BNCnFZ#F({bB0^9Af z&vOdz7%U6H@dUAW7BK9TYi=;tkYFye#Ol>cf&~rjQP9d@mUJx(e9Xu)EH{;5W>;X8 z*cJ}o%&ut(wj&HS1@7jg8C`I9NVu8b1XuQDRu8zcM{~m925T@yOKAan%3!D7+$Cl) zv99T{PR(FV@69oiNap%m+3Lyuh9Db{qm0*hybS$^=L8)dW!Hn^$DeYS;3PV77O;cQ zTq*seheuO7TdwygFTRF{XUO5coOadx3D!@Wnz(QxLmc+O)9ryYA5QVGJE0k}hkr6+ z-)*M7X0gIY%d{csRa6K1v=%Y2&McPsEK1?Vbl%Qp?}Lptv(oNR_S)TTw5j+JTLm-L zn!U3=*5kNCX|<+S&za`P-RI1$9jy*Nh!Py$Vr(azsh3&*is<3z9r(RN9<+_3QV-yF z>H$4M_-ySk-oR$iyko__f!+>aU`nBDz>XtoTpQq?h_^~Mcjs--CsI|KD5$nMA*_7} zK$>cYI$8%N1s!f!@z0jKd9ug`b33_?IRy^0fG@={(u}9E$s5=p<{gIB1+c|d@jwH` zH`2@GW*iWqZbz-6^m9PYK4}r94LYkBcQVf^<}{eE`gCSt^*6mm`3wFSgF6c0*$npP zs=JNm9mB@wm;kaL9y6gmkS>;%+tguG9i}9DGYJdMTU zx8LmOEm@4$-iZEkrrVv(ckGZ$*1`;lFO6q@NZwlH;FRVKy_URfJa49nQ7rZAL}s0M zs#J_)wRD-n;29KGKYGwC;h{I`7_GdCGekm*A$kjcaS9ed8>?-#fxdnXV z1Ab7s1$?yVUaw`DCd11l7jAfCgA9LTdMLf&-Ayv= z-RI=9TGAOT8yMVMG&uQPaJB=EPlNMPk8-zb>1Tz~8{Vzd;LnxMQVo6s((l&N7xsW( zt-)W=1O5Rm{lYN#bsGF7-vIxpmVRk>`0qAod6vuYQ2J9^`W0d6iBhqy<6bU$!RKymGX%R(RzwVM@wk3!q*(ew5fE6K5^-YU2fL*~QkB+-(Ne--(sv z+p3M}+Bw3oS4&!8f}*qj*NP2uA~b+&T-gT_tko zIceDMWLFl+t>&b}iJ3U&!rcM;Xs^J;E8`f2i{u&Lq{ONx+48EKjN?ZFrn^pV(W5W? zf~QKL^1~*EAOD7oi=P?e<#R2*!j17%344wEMC-L9y6S*W;TSV5pXP#2@gv$z7=FZj zxx#1G5Zuudat=*GTMXV=yiR#Qzl0-vM@^`;f(fH2IQZ`u}+%Vu;YQcEAN8ExgfK;~=CYQnlOc zQj17c(I;l?le42gqPoT}9rMJt&T>02#~Ej0c-Ox^}cW3Y(6-c#uQY{Hc6ec)dR zF6S57J7{dq%QvhgxV?&(0mheOt8esWNr4EaFc0 z8v2f+cKuBwrj@~nX>-U4tD!kB+S(X5p?U`DRGQ ztGnerm;1b|ledMRV|{RXi8lH%hj_2ZP#MB)XGU!h;yScAkrh2@96)=HcCx{8J|u#6 zzf&)@{GA5Qx^EEG;*CrsRnK?I&9yZKG0cS-2+-GH;qLHsurah@*XHXY9O^~`3tcdB zIFzf6@k0^%X$p%a4TBw5Htck%F=i-?cnqtd5SzMjIVW##Jn^XXb+kg37;lASrDu8Y zE5WM#D%qmEtL>>gJi8(y)?8tYz?QA2(IWL%b>Sk&zZe+OpA3@^>t|Q?c3JIHoYmNg zd3>VqFHsPTs<0{hZAz=xX@pF+#rwN{7O&M%(8dByF;-o`s;g>#jJ_X(fD?V&bN4x( zX@l{1htg*AH$86mf0b<3?W1(|+$zU2RWhndMm13sg@0yOPQe9{-~)x@E%wyikjHVK zkFhD&9LgD+@>Hcosj@0f=3fM=ZSL&`v-?J;E0MOY(m%T4z7HI{Gd#7si~yaB*xF zsR@JWhrWU)wCYX%{Ck4;0fZ~D3oqm93vxaSHBH1uuhlqA?yz9Tmnsn{ooU2;(Pk7PDOs`vQZv=Zd2G;6feRMmI;Whq%e z920%*!xUF3vO3M;j>m-YYcU=YCWbjNnycDNoSF?2b_rv=>sE1&tliYz!Qt<9a)+ti`Z3UMmOd)N!0ak)zvcZu>{ z>B6{QlH*l?Sbz0MyR*ON4`-FFMad(WDgZ6@54%*}=q<*C^Os&z= zx;3B%qxDl^ydiQrSw@1!r~I;%HOgwiR|PGva-hzO@l6ouc{b)U8<7;*=V*kmjfv+V z;&EwY^lxbXxc>YvNPqkTA^n1a0Okr&>@Vn)FQsX|n4PYEVqK>$P!KqO1G0MVid)=g zTiq1~rM0ZPy3cN#I?50pru5n|im-)jCW5FGn=>}qZ9 z9g%ijna$Mfs+2~(pyWkWv59q-lvamWcVReXoT0(9dC?)N9@_f}YXG3hOzFED_Uh_C zB{SuGu;(n8CR7??3T(CI);M#U%u1b`R78XcyCqVOoX*;6&A$)+??BiNO~Jnt=N z&Ur^}iP2A&Tbg#Hz`oD#we`Y69cz#z z#ExJ_NSX_3PnyeeQ9mDcdS$;Q54 zEL_9hJQ{37(V`CSZFZlbaT?VJp_Q6Gaivvy;GHX zd&2t@y2eqMjUFY$5QQ(YW%X9sIRsE3HM)k)uC1j_uDwYrD zicDKV!{^AL^>f|s0zU(25_uzZu^AK|o6$3v)(gA4G}2+(@2Y6J)XCibv*+`~a~C4m zlp3??h$#Fc44O4|ej{>WJA&8Fx2izjTL9Qg*@gdA_(jdWk&u324MOP); zjGqyJ<^5|k?5)G829HWW`$K?sHaRJU`;~qXqVR`=iD>uGNKM*1`a!cq@9cDm-X8hv z|G9kL_j@zu{|Eo-8Vqcu>dpDKH*J zDIob;Z4t(rnY2Y8xSihi&xC+>rA3V213_zCp;Xh$5u}D7ZdM(1$!@LhO=CUaIz>NS zkAFme!P3?+q)G5oKA~I(aPe}0pf*WKT>5I}yBLcEox?)h2j#7i6-WIjTS|9a1oUnGAloUOtEYHl4d@ZY_0D zau5B4ZoRZFBG^mRLDU(>>Y2x=gK$z)JM)0nLA!72pwH1k$Ns-`P~*)VRQ<0y=s>80 z9_Kl``+VI)zo)K;VXkV6_mw1O;v;)Q<6&~Y5o#>0eYe<@l0kedFv_5)4xH~ORd)5~ ztP27~adixFLL0|Ie4-iT@m?Neta8Bl^Gn z7?0a$PsnlmLZ*B8)5joox5J^7Kp(n!VE6ILK8)@@3}L1ftHD#y0&FKX$kZVUZ;ym- zE}RTWi}Mduxl80d)*{$7v3b=&d}Q$lnhws|+$CsQME(=Rt6Qaga*Nn%VmVPO7T%FW zxUaL8z(}!mXm18;#mY+&j@NM?idleWSlvY@O|qrUQ3U3y2PNtpUao!qJv0*%uTVv` z5}E|+12tWcX$3Ed;TjgE^WxN$7U*_Ax-~vnA7sr=*cz-)La07aPsny`)obu7P#>G= zx~tTDwL+Jl@^dvd*{-xZ#kv~wO?+1^Lhu*@{=Do_- ztkA{$GWG5N3+_xl!D^|e4=2^4UODF9dPPoKhnP|QE;Bx_*}Yu_u8V8CL^{5kOa zbBokr$hnNKEBHCh$^aB}R4OIl2o;2|6NFGhgs`8gag+#wYB26d)0rCq%^p%(0BshY z7>Kj|D8Nny;k^N;e~Uyfpg;^f(J>aJ(?YXR;xfCPvns8cucVP`68jM%;&_~rLzvy7 zrr52_i4Xhu5e<)rKiVF!Ds9a_$@>=kTwBeS-*%V-WoB6ZyCDjHPAK|wb?8qB7r7Qn z2}IawTue9P><8U(HiWR*d>TY6S1t7^tu!IT+8>6p&RjUONP0X7c2`fVwd&4VbjO{h zL$1xuw`%((FD_qXxgAJTBq# zZx5`t>P}F7?WR`OW=8A>WX;67ec=_0`QM`V*ly@O;(J2xyLW&8=>7hUki23LzjeUx z`+MN`Pol!{+u?oAK#2CZ!@E}E_-Q#`x88^6C^cytp_asKy`A~1VaI-^A@(yv+JTuy zUI#jwZQ=NBSXhi%msMk^30MlVc^qTk4)%3CCBc9I>*;U111`g!k7Q&9Oq)9Nw*Q~_ ze>{Z$mJl{tyI~^;fTqdV$Ai;k)y)J@VbKv)9&~ML?wuF3*ZIBRe?D6(Ibl%_cgKH1 zayY^3+f`qQr1ZPA1d|2VoQkkgs@uD2u8{&g7M2{)k}*+G1FX7IxKLWfMDg&=L}AuI zb)$-I1xotpB8|S?NF(NZB8`Zu@1HcLW9AR{FR9VT)y^*ZnDcK>s0q?7oa$k`RIb^S z^2X%8x`>-imw;mmKRo=mzTq~1Y@RAn_(?RZ;RQtDKj;Nkto{-m5oW2xtQ^2u;A<49 zT(&4D#JWT7_DB{h6V6h1`z^1;Skrt>u4tIP^3jUJR$OX{5&H6RX7S&Dtjpe@Vem3x zn6=D6-9mhNX}OM`^oYi(ed#`W91^7mbr`kM&vhcRRAla=q`UJhVo1UsdOHA(e|#L2 zIdo<*u38vN*&M`0G43#o3*fz7#8Xl6RFutK8e=zAyWD1zPk8p~F_SmAJ0m|~Q*_)8 zi*T012|}IO-4SUP&I35vYH+AYn&-45?2j-rrNyZnvtK**|2TUa_$I6CalB8PN`T@G zQWd3)GK;o~NVhtPg7$$Y@I(?s1uPC&EoHf>{|&Em4%qGA+GSpaw@nh=Ofh32ql35A63s> zf*q~HR#Boa@Sw1~*TFjy@t<#3&=M1)z;>Ph4$irgMX!hoad0@YMPSMjNych{_bMa+S#a% zd6{Z_n?w2vRkD%DH1zd^_Au;3UZZSnVWl#K`maY_0A>Y)Igse_g3NMb(1g9XTH%d> zj$z91Mp=JCQuX<9;3LmLk9O|wq9-UIEg?k*o^KYE(`Sa_pE#s8m~H6Uub``izY)3p z>Kzi^ky!fKgp@H8cJ!$FXJ#mD8W7P@=+ycM$b*yW)wmy^g1(cgwZ^@j@AaK*L?*43_4t}4CaMyvc3~8jI~1o6FF0nNg;a6LgO7EyQ7IwQHMe1 zszbQP0}7iSWDi`B0S17pG3!>QPoz*F=OtENtrnkk6o(!BTQ#pu^~TMG?4_-R=Fi+N zm0wW3U&k;jqp&-u4W>BtoDX`&tF__v2AT!Gk6y@o#r1q{6Z}17T-EOr-;3b?z$~ab z@Ui#trpv5a&$>ZM{8rHb7v1IvMcL*pyf$yQ%0DY?t(9E)7;1p6bpftx3ykIbfZT;~ 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

    {M~0T?&4cs$1ASbQ+X2e7yW;{#cI7{=pS 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 G>i+<`NAcJI diff --git a/apps/client/local_plugins/jl_opus/ios/jl_opus/Package.swift b/apps/client/local_plugins/jl_opus/ios/jl_opus/Package.swift deleted file mode 100644 index ab0c6c8e..00000000 --- a/apps/client/local_plugins/jl_opus/ios/jl_opus/Package.swift +++ /dev/null @@ -1,22 +0,0 @@ -// swift-tools-version: 5.9 -import PackageDescription - -let package = Package( - name: "jl_opus", - platforms: [.iOS("16.0")], - products: [ - .library(name: "jl-opus", targets: ["jl_opus"]) - ], - dependencies: [ - .package(path: "../../../ble_service/ios/ble_service") - ], - targets: [ - .target( - name: "jl_opus", - dependencies: [ - .product(name: "ble-service", package: "ble_service") - ], - path: "Sources/jl_opus" - ) - ] -) diff --git a/apps/client/local_plugins/jl_opus/ios/jl_opus/Sources/jl_opus/JlOpusPlugin.swift b/apps/client/local_plugins/jl_opus/ios/jl_opus/Sources/jl_opus/JlOpusPlugin.swift deleted file mode 100644 index 09734dab..00000000 --- a/apps/client/local_plugins/jl_opus/ios/jl_opus/Sources/jl_opus/JlOpusPlugin.swift +++ /dev/null @@ -1,450 +0,0 @@ -import Flutter -import Foundation -import opus - -@objc(JlOpusPlugin) -public class JlOpusPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { - private var eventSink: FlutterEventSink? - private var isInitialized: Bool = false - - private var isDecodingStream: Bool = false - private var streamHasHeader: Bool = false - private var streamChannels: Int32 = 1 - private var streamSampleRate: Int32 = 16000 - private var streamDecoder: OpaquePointer? - - private let decodeQueue = DispatchQueue(label: "com.yunqiinnovation.jl_opus.decode", qos: .userInitiated) - - /// Flutter 插件注册入口 - /// - Parameter registrar: FlutterPluginRegistrar - /// - Returns: 无 - /// - Throws: 不抛出 - public static func register(with registrar: FlutterPluginRegistrar) { - let methodChannel = FlutterMethodChannel( - name: "com.yunqiinnovation.jl_opus", - binaryMessenger: registrar.messenger() - ) - - let eventChannel = FlutterEventChannel( - name: "com.yunqiinnovation.jl_opus/events", - binaryMessenger: registrar.messenger() - ) - - let instance = JlOpusPlugin() - registrar.addMethodCallDelegate(instance, channel: methodChannel) - eventChannel.setStreamHandler(instance) - } - - /// 处理 Flutter 侧方法调用 - /// - Parameters: - /// - call: FlutterMethodCall - /// - result: FlutterResult - /// - Returns: 无 - /// - Throws: 不抛出(错误通过 FlutterError 返回) - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "initOpusDecoder": - isInitialized = true - result(true) - - case "decodeOpusFile": - decodeOpusFile(arguments: call.arguments, result: result) - - case "startDecodeStream": - startDecodeStream(arguments: call.arguments, result: result) - - case "stopDecodeStream": - stopDecodeStream(result: result) - - case "writeAudioStream": - writeAudioStream(arguments: call.arguments, result: result) - - case "isDecoding": - result(isDecodingStream) - - case "dispose": - dispose(result: result) - - default: - result(FlutterMethodNotImplemented) - } - } - - /// 事件通道开始监听 - /// - Parameters: - /// - arguments: Flutter 传入参数 - /// - eventSink: 事件回调 - /// - Returns: FlutterError?(通常为 nil) - /// - Throws: 不抛出 - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - eventSink = events - return nil - } - - /// 事件通道取消监听 - /// - Parameter arguments: Flutter 传入参数 - /// - Returns: FlutterError?(通常为 nil) - /// - Throws: 不抛出 - public func onCancel(withArguments arguments: Any?) -> FlutterError? { - eventSink = nil - return nil - } - - /// 解码 Opus 文件为 PCM 文件(16-bit little-endian 原始 PCM) - /// - Parameters: - /// - arguments: Flutter 传入参数(需包含 inPath/outPath/hasHeader/channel/sampleRate/packetSize) - /// - result: FlutterResult,成功返回 outPath,失败返回 FlutterError - /// - Returns: 无 - /// - Throws: 不直接抛出;内部错误会通过 FlutterError 与事件通道 onError 返回 - private func decodeOpusFile(arguments: Any?, result: @escaping FlutterResult) { - guard isInitialized else { - result(FlutterError(code: "NOT_INITIALIZED", message: "Opus解码器未初始化", details: nil)) - return - } - - guard let args = arguments as? [String: Any], - let inPath = args["inPath"] as? String, - let outPath = args["outPath"] as? String else { - result(FlutterError(code: "INVALID_ARGS", message: "输入或输出路径不能为空", details: nil)) - return - } - - let hasHeader = (args["hasHeader"] as? Bool) ?? false - let channel = Int32((args["channel"] as? Int) ?? 1) - let sampleRate = Int32((args["sampleRate"] as? Int) ?? 16000) - let packetSize = (args["packetSize"] as? Int) ?? 40 - - sendEvent([ - "event": "onStart", - "type": "file" - ]) - - decodeQueue.async { [weak self] in - guard let self = self else { return } - - do { - let inputUrl = URL(fileURLWithPath: inPath) - let outputUrl = URL(fileURLWithPath: outPath) - - let inputData = try Data(contentsOf: inputUrl) - if inputData.isEmpty { - throw NSError(domain: "jl_opus", code: -2, userInfo: [NSLocalizedDescriptionKey: "输入文件为空"]) - } - - let packets = try self.splitPackets(inputData: inputData, hasHeader: hasHeader, packetSize: packetSize) - let pcmData = try self.decodePacketsToPcm(packets: packets, sampleRate: sampleRate, channels: channel) - - try self.ensureParentDirectoryExists(for: outputUrl) - try pcmData.write(to: outputUrl, options: .atomic) - - self.sendEvent([ - "event": "onComplete", - "type": "file", - "filePath": outPath - ]) - - DispatchQueue.main.async { - result(outPath) - } - } catch { - let message = error.localizedDescription - self.sendEvent([ - "event": "onError", - "type": "file", - "code": -1, - "message": message - ]) - - DispatchQueue.main.async { - result(FlutterError(code: "DECODE_ERROR", message: "解码出错: \(message)", details: nil)) - } - } - } - } - - /// 开始 Opus 数据流解码 - /// - Parameters: - /// - arguments: Flutter 传入参数(需包含 hasHeader/channel/sampleRate) - /// - result: FlutterResult,成功返回 true,失败返回 FlutterError - /// - Returns: 无 - /// - Throws: 不抛出;失败通过 FlutterError/事件 onError 返回 - private func startDecodeStream(arguments: Any?, result: @escaping FlutterResult) { - guard isInitialized else { - result(FlutterError(code: "NOT_INITIALIZED", message: "Opus解码器未初始化", details: nil)) - return - } - - guard let args = arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGS", message: "缺少解码参数", details: nil)) - return - } - - let hasHeader = (args["hasHeader"] as? Bool) ?? false - let channel = Int32((args["channel"] as? Int) ?? 1) - let sampleRate = Int32((args["sampleRate"] as? Int) ?? 16000) - - stopDecodeStreamInternal() - - var error: Int32 = 0 - guard let decoder = opus_decoder_create(sampleRate, channel, &error), error == OPUS_OK else { - sendEvent([ - "event": "onError", - "type": "stream", - "code": Int(error), - "message": "Opus解码器创建失败: \(error)" - ]) - result(FlutterError(code: "DECODE_STREAM_ERROR", message: "数据流解码器创建失败", details: nil)) - return - } - - streamHasHeader = hasHeader - streamChannels = channel - streamSampleRate = sampleRate - streamDecoder = decoder - isDecodingStream = true - - sendEvent([ - "event": "onStart", - "type": "stream" - ]) - result(true) - } - - /// 停止 Opus 数据流解码 - /// - Parameter result: FlutterResult,已停止返回 true,未处于解码返回 false - /// - Returns: 无 - /// - Throws: 不抛出 - private func stopDecodeStream(result: @escaping FlutterResult) { - if isDecodingStream { - stopDecodeStreamInternal() - result(true) - } else { - result(false) - } - } - - /// 停止并释放当前数据流解码器(内部方法) - /// - Returns: 无 - /// - Throws: 不抛出 - private func stopDecodeStreamInternal() { - if let decoder = streamDecoder { - opus_decoder_destroy(decoder) - } - streamDecoder = nil - isDecodingStream = false - } - - /// 写入 Opus 数据到解码流,并通过事件通道返回 PCM 数据 - /// - Parameters: - /// - arguments: Flutter 传入参数(需包含 data) - /// - result: FlutterResult,成功返回 true;不在解码状态返回 FlutterError - /// - Returns: 无 - /// - Throws: 不直接抛出;解码错误通过事件 onError 返回 - private func writeAudioStream(arguments: Any?, result: @escaping FlutterResult) { - guard isDecodingStream, let decoder = streamDecoder else { - result(FlutterError(code: "NOT_DECODING", message: "当前没有处于解码状态", details: nil)) - return - } - - guard let args = arguments as? [String: Any], - let data = args["data"] as? FlutterStandardTypedData else { - result(FlutterError(code: "INVALID_ARGS", message: "音频数据不能为空", details: nil)) - return - } - - let opusData = data.data - if opusData.isEmpty { - result(true) - return - } - - let hasHeader = streamHasHeader - let channels = streamChannels - let sampleRate = streamSampleRate - - decodeQueue.async { [weak self] in - guard let self = self else { return } - - do { - let packets: [Data] - if hasHeader { - packets = try self.splitPacketsByLengthPrefix(inputData: opusData) - } else { - packets = [opusData] - } - - for packet in packets { - let pcm = try self.decodeSinglePacketToPcm(decoder: decoder, packet: packet, sampleRate: sampleRate, channels: channels) - if !pcm.isEmpty { - self.sendEvent([ - "event": "onDecodeStream", - "data": FlutterStandardTypedData(bytes: pcm) - ]) - } - } - } catch { - let message = error.localizedDescription - self.sendEvent([ - "event": "onError", - "type": "stream", - "code": -1, - "message": message - ]) - } - } - - result(true) - } - - /// 释放插件资源 - /// - Parameter result: FlutterResult - /// - Returns: 无 - /// - Throws: 不抛出 - private func dispose(result: @escaping FlutterResult) { - stopDecodeStreamInternal() - isInitialized = false - result(true) - } - - /// 拆分输入文件数据为 Opus 包列表 - /// - Parameters: - /// - inputData: 输入文件二进制数据 - /// - hasHeader: 是否包含 2 字节小端长度前缀 - /// - packetSize: 无协议头时按固定长度切包 - /// - Returns: Opus 包数组 - /// - Throws: 参数错误或解析失败时抛出 NSError - private func splitPackets(inputData: Data, hasHeader: Bool, packetSize: Int) throws -> [Data] { - if hasHeader { - return try splitPacketsByLengthPrefix(inputData: inputData) - } - - guard packetSize > 0 else { - throw NSError(domain: "jl_opus", code: -5, userInfo: [NSLocalizedDescriptionKey: "packetSize 必须大于 0"]) - } - - var packets: [Data] = [] - var offset = 0 - while offset < inputData.count { - let end = min(offset + packetSize, inputData.count) - packets.append(inputData.subdata(in: offset.. [Data] { - var packets: [Data] = [] - var cursor = 0 - while cursor + 2 <= inputData.count { - let lo = Int(inputData[cursor]) - let hi = Int(inputData[cursor + 1]) << 8 - let length = lo | hi - cursor += 2 - - if length <= 0 || cursor + length > inputData.count { - throw NSError(domain: "jl_opus", code: -3, userInfo: [NSLocalizedDescriptionKey: "协议头解析失败"]) - } - - packets.append(inputData.subdata(in: cursor..<(cursor + length))) - cursor += length - } - - if packets.isEmpty { - throw NSError(domain: "jl_opus", code: -4, userInfo: [NSLocalizedDescriptionKey: "未解析到任何Opus数据包"]) - } - - return packets - } - - /// 将 Opus 包列表解码为 PCM 数据 - /// - Parameters: - /// - packets: Opus 包数组 - /// - sampleRate: 采样率 - /// - channels: 通道数 - /// - Returns: PCM 数据(16-bit little-endian) - /// - Throws: 解码器创建失败或任意包解码失败时抛出 NSError - private func decodePacketsToPcm(packets: [Data], sampleRate: Int32, channels: Int32) throws -> Data { - var error: Int32 = 0 - guard let decoder = opus_decoder_create(sampleRate, channels, &error), error == OPUS_OK else { - throw NSError(domain: "jl_opus", code: Int(error), userInfo: [NSLocalizedDescriptionKey: "Opus解码器创建失败: \(error)"]) - } - defer { opus_decoder_destroy(decoder) } - - var pcmOutput = Data() - for packet in packets { - let pcmChunk = try decodeSinglePacketToPcm(decoder: decoder, packet: packet, sampleRate: sampleRate, channels: channels) - pcmOutput.append(pcmChunk) - } - return pcmOutput - } - - /// 解码单个 Opus 包为 PCM - /// - Parameters: - /// - decoder: Opus 解码器指针 - /// - packet: 单个 Opus 包 - /// - sampleRate: 采样率 - /// - channels: 通道数 - /// - Returns: PCM 数据(16-bit little-endian) - /// - Throws: 解码失败时抛出 NSError - private func decodeSinglePacketToPcm(decoder: OpaquePointer, packet: Data, sampleRate: Int32, channels: Int32) throws -> Data { - let maxSamplesPerChannel = Int(sampleRate * 120 / 1000) - var pcmBuffer = [opus_int16](repeating: 0, count: maxSamplesPerChannel * Int(channels)) - - let decodedSamples: Int32 = packet.withUnsafeBytes { packetPtr in - guard let inPtr = packetPtr.bindMemory(to: UInt8.self).baseAddress else { - return OPUS_BAD_ARG - } - - return opus_decode( - decoder, - inPtr, - Int32(packet.count), - &pcmBuffer, - Int32(maxSamplesPerChannel), - 0 - ) - } - - if decodedSamples < 0 { - throw NSError(domain: "jl_opus", code: Int(decodedSamples), userInfo: [NSLocalizedDescriptionKey: "Opus解码失败: \(decodedSamples)"]) - } - - let sampleCount = Int(decodedSamples) * Int(channels) - return pcmBuffer.withUnsafeBytes { rawPtr in - Data(rawPtr.bindMemory(to: UInt8.self).prefix(sampleCount * MemoryLayout.size)) - } - } - - /// 确保输出路径的父目录存在 - /// - Parameter url: 输出文件 URL - /// - Returns: 无 - /// - Throws: 创建目录失败时抛出错误 - private func ensureParentDirectoryExists(for url: URL) throws { - let dir = url.deletingLastPathComponent() - var isDirectory: ObjCBool = false - if FileManager.default.fileExists(atPath: dir.path, isDirectory: &isDirectory), isDirectory.boolValue { - return - } - try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil) - } - - /// 通过事件通道向 Flutter 发送事件 - /// - Parameter event: 事件字典 - /// - Returns: 无 - /// - Throws: 不抛出 - private func sendEvent(_ event: [String: Any]) { - guard let sink = eventSink else { return } - DispatchQueue.main.async { - sink(event) - } - } -} diff --git a/apps/client/local_plugins/jl_opus/lib/jl_opus.dart b/apps/client/local_plugins/jl_opus/lib/jl_opus.dart deleted file mode 100644 index dc63a0cd..00000000 --- a/apps/client/local_plugins/jl_opus/lib/jl_opus.dart +++ /dev/null @@ -1,214 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; -import 'package:flutter/services.dart'; - -/// OPUS解码参数选项 -class OpusOption { - /// 是否具有协议头 - final bool hasHeader; - - /// 通道数。取值范围:[1, 2] - final int channel; - - /// 采样率。参考值:{8000, 16000, 24000, 32000, 44100, 48000} - final int sampleRate; - - /// 数据包长度。仅hasHeader = false时生效。 - final int packetSize; - - /// 构造函数 - OpusOption({ - this.hasHeader = false, - this.channel = 1, - this.sampleRate = 16000, - this.packetSize = 40, - }); - - /// 转换为参数Map - Map toMap() { - return { - 'hasHeader': hasHeader, - 'channel': channel, - 'sampleRate': sampleRate, - 'packetSize': packetSize, - }; - } -} - -/// 杰理OPUS编解码器事件 -class OpusEvent { - /// 事件类型 - final String event; - - /// 数据类型 (file或stream) - final String? type; - - /// 解码后的数据 (仅在流解码时有效) - final Uint8List? data; - - /// 输出文件路径 (仅在文件解码完成时有效) - final String? filePath; - - /// 错误码 (仅在发生错误时有效) - final int? errorCode; - - /// 错误信息 (仅在发生错误时有效) - final String? errorMessage; - - /// 构造函数 - OpusEvent({ - required this.event, - this.type, - this.data, - this.filePath, - this.errorCode, - this.errorMessage, - }); - - /// 从Map构造对象 - factory OpusEvent.fromMap(Map map) { - return OpusEvent( - event: map['event'], - type: map['type'], - data: map['data'], - filePath: map['filePath'], - errorCode: map['code'], - errorMessage: map['message'], - ); - } -} - -/// 杰理OPUS编解码插件 -class JlOpus { - static const MethodChannel _methodChannel = MethodChannel('com.yunqiinnovation.jl_opus'); - static const EventChannel _eventChannel = EventChannel('com.yunqiinnovation.jl_opus/events'); - - /// 事件流 - Stream? _eventStream; - - /// 获取事件流 - Stream get eventStream { - _eventStream ??= _eventChannel.receiveBroadcastStream().map((event) { - if (event is Map) { - return OpusEvent.fromMap(event); - } - throw PlatformException( - code: 'INVALID_EVENT', - message: '无效的事件数据', - ); - }); - return _eventStream!; - } - - /// 解码后的数据流 - final StreamController _decodedDataController = StreamController.broadcast(); - - /// 获取解码数据流 - Stream get decodedDataStream => _decodedDataController.stream; - - /// 构造函数,设置事件监听 - JlOpus() { - _setupEventListener(); - } - - /// 设置事件监听 - void _setupEventListener() { - eventStream.listen((event) { - if (event.event == 'onDecodeStream' && event.data != null) { - _decodedDataController.add(event.data!); - } - }); - } - - /// 初始化OPUS解码器 - Future initOpusDecoder() async { - try { - final result = await _methodChannel.invokeMethod('initOpusDecoder'); - return result ?? false; - } catch (e) { - return false; - } - } - - /// 解码OPUS文件 - /// - /// [inPath] 输入文件路径 - /// [outPath] 输出文件路径 - /// [option] 解码参数 - Future decodeOpusFile(String inPath, String outPath, OpusOption option) async { - try { - final result = await _methodChannel.invokeMethod( - 'decodeOpusFile', - { - 'inPath': inPath, - 'outPath': outPath, - ...option.toMap(), - }, - ); - return result; - } catch (e) { - rethrow; - } - } - - /// 开始解码OPUS数据流 - /// - /// [option] 解码参数 - Future startDecodeStream(OpusOption option) async { - try { - final result = await _methodChannel.invokeMethod( - 'startDecodeStream', - option.toMap(), - ); - return result ?? false; - } catch (e) { - rethrow; - } - } - - /// 停止解码OPUS数据流 - Future stopDecodeStream() async { - try { - final result = await _methodChannel.invokeMethod('stopDecodeStream'); - return result ?? false; - } catch (e) { - return false; - } - } - - /// 写入OPUS音频数据进行解码 - /// - /// [data] OPUS音频数据 - Future writeAudioStream(Uint8List data) async { - try { - final result = await _methodChannel.invokeMethod( - 'writeAudioStream', - {'data': data}, - ); - return result ?? false; - } catch (e) { - rethrow; - } - } - - /// 判断是否正在解码 - Future isDecoding() async { - try { - final result = await _methodChannel.invokeMethod('isDecoding'); - return result ?? false; - } catch (e) { - return false; - } - } - - /// 释放资源 - Future dispose() async { - try { - _decodedDataController.close(); - final result = await _methodChannel.invokeMethod('dispose'); - return result ?? false; - } catch (e) { - return false; - } - } -} diff --git a/apps/client/local_plugins/jl_opus/pubspec.yaml b/apps/client/local_plugins/jl_opus/pubspec.yaml deleted file mode 100644 index b28aaeb0..00000000 --- a/apps/client/local_plugins/jl_opus/pubspec.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: jl_opus -description: 杰理OPUS编解码库Flutter插件 -version: 0.0.1 -homepage: - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" - -dependencies: - flutter: - sdk: flutter - path_provider: ^2.0.0 - permission_handler: ^12.0.1 - -dev_dependencies: - flutter_test: - sdk: flutter - -flutter: - plugin: - platforms: - android: - package: com.yunqiinnovation.jl_opus - pluginClass: JlOpusPlugin diff --git a/apps/client/local_plugins/open_ai_service/LICENSE b/apps/client/local_plugins/open_ai_service/LICENSE deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/client/local_plugins/open_ai_service/android/AndroidManifest.xml b/apps/client/local_plugins/open_ai_service/android/AndroidManifest.xml deleted file mode 100644 index e71b124e..00000000 --- a/apps/client/local_plugins/open_ai_service/android/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/apps/client/local_plugins/open_ai_service/android/build.gradle.kts b/apps/client/local_plugins/open_ai_service/android/build.gradle.kts deleted file mode 100644 index a93b5d5a..00000000 --- a/apps/client/local_plugins/open_ai_service/android/build.gradle.kts +++ /dev/null @@ -1,64 +0,0 @@ -plugins { - // Android Library 插件 - id("com.android.library") - // Kotlin Android 插件 - id("org.jetbrains.kotlin.android") - id("com.github.johnrengelman.shadow") version "8.1.1" - kotlin("plugin.serialization") version "1.9.24" -} - -android { - // 命名空间,对应你插件的包名(需与代码内包名保持一致) - namespace = "com.yunqiinnovation.open_ai_service" - - // 目标 SDK 版本 - compileSdk = 35 - - defaultConfig { - // 最低 SDK 版本 - minSdk = 21 - targetSdk = 35 - } - - // Java 语言级别兼容配置 - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - // Kotlin 语言级别 - kotlinOptions { - jvmTarget = "11" - } - - // 添加packaging配置,排除冲突文件 - packaging { - resources { - excludes.add("META-INF/DEPENDENCIES") - excludes.add("META-INF/LICENSE") - excludes.add("META-INF/LICENSE.txt") - excludes.add("META-INF/license.txt") - excludes.add("META-INF/NOTICE") - excludes.add("META-INF/NOTICE.txt") - excludes.add("META-INF/notice.txt") - excludes.add("META-INF/*.kotlin_module") - } - } -} - -dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") - implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") - implementation("com.squareup.okhttp3:okhttp:4.9.3") - implementation("com.squareup.okhttp3:logging-interceptor:4.9.3") - implementation("com.anthropic:anthropic-java:0.8.0") - implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0") - implementation("io.ktor:ktor-client-core:3.1.2") - implementation("io.ktor:ktor-client-cio:3.1.2") - implementation("io.ktor:ktor-client-okhttp:3.1.2") - implementation("io.ktor:ktor-client-logging:3.1.2") - implementation("com.google.code.gson:gson:2.10.1") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") - implementation("androidx.media:media:1.6.0") -} \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/android/settings.gradle.kts b/apps/client/local_plugins/open_ai_service/android/settings.gradle.kts deleted file mode 100644 index 1c33f497..00000000 --- a/apps/client/local_plugins/open_ai_service/android/settings.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = "open_ai_service" \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/android/src/main/AndroidManifest.xml b/apps/client/local_plugins/open_ai_service/android/src/main/AndroidManifest.xml deleted file mode 100644 index 69c9202a..00000000 --- a/apps/client/local_plugins/open_ai_service/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - diff --git a/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt b/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt deleted file mode 100644 index 370e16a5..00000000 --- a/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt +++ /dev/null @@ -1,1070 +0,0 @@ -package com.yunqiinnovation.open_ai_service - -import android.content.Context -import android.util.Log -import com.anthropic.core.JsonValue -import com.anthropic.models.messages.Tool -import com.anthropic.models.messages.ToolUnion -import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import io.ktor.client.HttpClient -import io.ktor.client.plugins.sse.* -import io.modelcontextprotocol.kotlin.sdk.Implementation -import io.modelcontextprotocol.kotlin.sdk.ListToolsResult -import io.modelcontextprotocol.kotlin.sdk.TextContent -import io.modelcontextprotocol.kotlin.sdk.client.Client -import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.json.JsonObject -import org.json.JSONObject -import kotlin.jvm.optionals.getOrNull -import java.io.File -import io.ktor.client.engine.okhttp.* -import okhttp3.OkHttpClient -import java.security.cert.X509Certificate -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager -import java.security.SecureRandom -import java.util.concurrent.TimeUnit -import okhttp3.logging.HttpLoggingInterceptor -import okhttp3.Interceptor -import io.ktor.client.request.* -import io.ktor.http.* -import com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import okio.Buffer - -/** - * 工具类型枚举 - */ -enum class ToolType { - LOCAL_FUNCTION, // 本地函数 - MCP_TOOL // MCP工具 -} - -/** - * 函数处理器接口 - */ -interface FunctionHandler { - /** - * 处理函数调用 - * @param arguments 函数参数,Map格式 - * @return 函数执行结果,字符串 - */ - suspend fun handle(arguments: Map): String -} - -/** - * 单个MCP子客户端 - * 负责连接到一个具体的MCP服务器并管理其工具 - */ -class MCPSubClient( - private val serverId: String, - serverUrl: String, - private val customClient: HttpClient? = null -) : AutoCloseable { - private val TAG = "MCPSubClient[$serverId]" - - // 保存服务器URL - private val serverUrl: String = serverUrl - - // 重连配置 - private val maxRetryAttempts = 3 - private val initialReconnectDelay = 1000L // 初始重连延迟1秒 - private val maxReconnectDelay = 30000L // 最大重连延迟30秒 - private var currentReconnectDelay = initialReconnectDelay - private var retryCount = 0 - - // 连接状态锁 - private val connectionLock = Mutex() - - // 初始化MCP客户端 - private val mcp: Client = Client( - clientInfo = Implementation( - name = "deep-voice-mcp-client-$serverId", - version = "1.0.0" - ) - ) - - // 服务器提供的工具列表 - private var tools: List = emptyList() - - // 工具Map列表,用于OpenAI请求 - private val toolMaps = mutableListOf>() - - // 是否已连接 - private var isConnected = false - - // SSE传输层,使用自定义的传输层 - private var sseTransport: CustomSseClientTransport? = null - - // 工具名称到工具的映射,用于快速查找 - private val toolMapping = mutableMapOf() - - // 心跳检测 - private var heartbeatJob: Job? = null - private val heartbeatInterval = 30000L // 30秒 - - // 网络状态监听 - private var networkStateListener: NetworkStateListener? = null - - init { - Log.d(TAG, "MCPSubClient初始化,URL: $serverUrl") - } - - /** - * 将JsonObject转换为JsonValue - */ - private fun JsonObject.toJsonValue(): JsonValue { - val mapper = ObjectMapper() - val node = mapper.readTree(this.toString()) - return JsonValue.fromJsonNode(node) - } - - /** - * 连接到MCP服务器(通过SSE) - * @return 是否连接成功 - */ - suspend fun connect(): Boolean { - if (isConnected) { - Log.d(TAG, "已经连接到服务器,跳过重复连接") - return true - } - try { - Log.d(TAG, "开始连接服务器: $serverUrl") - - // 使用提供的客户端或创建带SSL配置的客户端 - val client = customClient ?: createSslTrustAllClient() - - // 使用自定义传输层 - sseTransport = CustomSseClientTransport( - client = client, - urlString = serverUrl - ) - - // 连接MCP客户端到服务器 - mcp.connect(sseTransport!!) - - // 获取工具列表并处理 - val toolsResult = mcp.listTools() - processToolsList(toolsResult) - - isConnected = true - retryCount = 0 - currentReconnectDelay = initialReconnectDelay - // 启动心跳检测 - startHeartbeat() - - Log.d(TAG, "连接成功,可用工具: ${tools.size}个") - return true - } catch (e: Exception) { - Log.e(TAG, "连接失败: ${e.message}") - return false - } - } - - /** - * 处理工具列表 - */ - private fun processToolsList(toolsResult: ListToolsResult?) { - try { - // 将MCP工具列表转换为ToolUnion列表 - tools = toolsResult?.tools?.map { tool -> - // 转换为ToolUnion格式 - ToolUnion.ofTool( - Tool.builder() - .name(tool.name) - .description(tool.description ?: "") - .inputSchema( - Tool.InputSchema.builder() - .type(JsonValue.from(tool.inputSchema?.type ?: "object")) - .properties(if (tool.inputSchema?.properties is JsonObject) - (tool.inputSchema.properties as JsonObject).toJsonValue() - else JsonValue.from(emptyMap())) - .putAdditionalProperty("required", JsonValue.from(tool.inputSchema?.required ?: emptyList())) - .build() - ) - .build() - ) - } ?: emptyList() - - // 更新工具映射 - toolMapping.clear() - tools.forEach { toolUnion -> - val tool = toolUnion.tool().get() - toolMapping[tool.name()] = toolUnion - } - - // 更新工具Map列表 - updateToolMaps() - - Log.d(TAG, "成功处理 ${tools.size} 个工具") - } catch (e: Exception) { - Log.e(TAG, "处理工具列表失败: ${e.message}", e) - } - } - - /** - * 更新工具Map列表,用于OpenAI请求 - * 将MCP工具转换为OpenAI函数调用格式 - */ - private fun updateToolMaps() { - toolMaps.clear() - - try { - tools.forEach { toolUnion -> - val tool = toolUnion.tool().getOrNull() ?: return@forEach - - try { - val inputSchema = tool.inputSchema() ?: return@forEach - val schemaStr = inputSchema.toString() - Log.d(TAG, "工具 ${tool.name()} 的Schema: $schemaStr") - - val propertiesMap = HashMap() - val requiredList = ArrayList() - - try { - // ✅ 替换原来的正则方式,使用括号匹配方式提取 properties 内容 - val propertiesContent = extractPropertiesContent(schemaStr) - if (propertiesContent != null) { - val fieldPattern = "([a-zA-Z0-9_]+)=\\{([^{}]+?)\\}".toRegex() - val fieldMatches = fieldPattern.findAll(propertiesContent) - - fieldMatches.forEach { fieldMatch -> - val fieldName = fieldMatch.groupValues[1] - val fieldContent = fieldMatch.groupValues[2] - - val fieldMap = HashMap() - val keyValuePattern = "([a-zA-Z0-9_]+)=(\"[^\"]*\"|'[^']*'|[^,}\\s]+)".toRegex() - val keyValueMatches = keyValuePattern.findAll(fieldContent) - - keyValueMatches.forEach { kvMatch -> - val key = kvMatch.groupValues[1] - var value = kvMatch.groupValues[2].trim() - - if ((value.startsWith("\"") && value.endsWith("\"")) || - (value.startsWith("'") && value.endsWith("'"))) { - value = value.substring(1, value.length - 1) - } - - val processedValue: Any = when { - value.equals("true", ignoreCase = true) -> true - value.equals("false", ignoreCase = true) -> false - value.matches("\\d+".toRegex()) -> value.toIntOrNull() ?: value - value.matches("\\d+\\.\\d+".toRegex()) -> value.toDoubleOrNull() ?: value - else -> value - } - - fieldMap[key] = processedValue - } - - propertiesMap[fieldName] = fieldMap - } - - Log.d(TAG, "成功解析属性: $propertiesMap") - } - - // ✅ required 部分保持不变 - val requiredPattern = "required=\\[(.*?)\\]".toRegex() - val requiredMatch = requiredPattern.find(schemaStr) - if (requiredMatch != null) { - val requiredStr = requiredMatch.groupValues[1] - if (requiredStr.isNotEmpty()) { - val fieldNames = requiredStr.split(",").map { - val trimmed = it.trim() - if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) || - (trimmed.startsWith("'") && trimmed.endsWith("'"))) { - trimmed.substring(1, trimmed.length - 1) - } else { - trimmed - } - } - requiredList.addAll(fieldNames) - Log.d(TAG, "解析到required字段: $requiredList") - } - } - } catch (e: Exception) { - Log.e(TAG, "解析Schema字符串失败: ${e.message}", e) - Log.e(TAG, "原始Schema: $schemaStr") - } - - val functionMap = mapOf( - "name" to tool.name(), - "description" to (tool.description() ?: ""), - "parameters" to mapOf( - "type" to "object", - "properties" to propertiesMap, - "required" to requiredList - ) - ) - - toolMaps.add(mapOf( - "type" to "function", - "function" to functionMap - )) - - Log.d(TAG, "成功转换工具 ${tool.name()} 到OpenAI函数格式 $functionMap") - } catch (e: Exception) { - Log.e(TAG, "转换工具 ${tool.name()} 失败: ${e.message}") - } - } - } catch (e: Exception) { - Log.e(TAG, "更新工具映射失败: ${e.message}") - } - - Log.d(TAG, "更新工具映射完成,共 ${toolMaps.size} 个工具") - } - - private fun extractPropertiesContent(schemaStr: String): String? { - val startKey = "properties={" - val startIndex = schemaStr.indexOf(startKey) - if (startIndex == -1) return null - - var braceCount = 0 - var endIndex = -1 - val contentStart = startIndex + startKey.length - - for (i in contentStart until schemaStr.length) { - when (schemaStr[i]) { - '{' -> braceCount++ - '}' -> { - if (braceCount == 0) { - endIndex = i - break - } else { - braceCount-- - } - } - } - } - - return if (endIndex != -1) schemaStr.substring(contentStart, endIndex) else null - } - - /** - * 将JsonNode转换为Map - */ - private fun convertJsonNodeToMap(node: JsonNode): Any { - return when { - node.isTextual() -> node.asText() - node.isNumber() -> node.asDouble() - node.isBoolean() -> node.asBoolean() - node.isObject() -> { - val result = mutableMapOf() - node.fieldNames().forEach { fieldName -> - result[fieldName] = convertJsonNodeToMap(node.get(fieldName)) - } - result - } - node.isArray() -> { - val result = mutableListOf() - for (i in 0 until node.size()) { - result.add(convertJsonNodeToMap(node.get(i))) - } - result - } - node.isNull() -> "null" - else -> node.toString() - } - } - - /** - * 获取工具Map列表,用于OpenAI请求 - */ - fun getToolMaps(): List> { - return toolMaps - } - - /** - * 获取可用的工具列表 - */ - fun getTools(): List { - return tools - } - - /** - * 检查是否包含指定工具 - */ - fun containsTool(name: String): Boolean { - return toolMapping.containsKey(name) - } - - /** - * 调用工具 - */ - suspend fun callTool(name: String, arguments: Map): JSONObject? { - - // 先检查连接状态 - if (!checkConnection()) { - Log.e(TAG, "无法连接到服务器,工具调用失败") - return JSONObject().apply { put("context", "无法连接到MCP服务器") } - } - - if (!containsTool(name)) { - Log.w(TAG, "此客户端不包含工具: $name") - return null - } - - return try { - val result = mcp.callTool(name = name, arguments = arguments) - - // 处理工具调用结果 - val context = result?.content?.joinToString("\n") { content -> - when (content) { - is TextContent -> content.text ?: "" - else -> "" - } - } - JSONObject().apply { - put("name", name) - put("meta", result?._meta) - put("context", context) - } - } catch (e: Exception) { - Log.e(TAG, "调用工具失败: ${e.message}") - isConnected = false - JSONObject().apply { put("context", "调用工具失败: ${e.message}") } - } - } - - /** - * 设置网络状态监听器 - */ - fun setNetworkStateListener(listener: NetworkStateListener) { - this.networkStateListener = listener - } - - /** - * 检查连接状态并自动重连 - */ - suspend fun checkConnection(): Boolean { - if (!isConnected) { - Log.d(TAG, "当前未连接,尝试重新连接...") - return connect() - } - - try { - // 简单ping测试连接状态 - mcp.ping() - return true - } catch (e: Exception) { - Log.e(TAG, "连接检查失败: ${e.message}") - isConnected = false - return false - } - } - /** - * 检查是否已连接 - */ - fun isConnected(): Boolean { - return isConnected - } - /** - * 处理连接错误并尝试重连 - */ - private suspend fun handleConnectionError(e: Exception) { - isConnected = false - stopHeartbeat() - - if (retryCount < maxRetryAttempts) { - retryCount++ - currentReconnectDelay = minOf(currentReconnectDelay * 2, maxReconnectDelay) - - // Log.w(TAG, "连接失败,将在 ${currentReconnectDelay}ms 后尝试重连 (尝试 $retryCount/$maxRetryAttempts)") - - delay(currentReconnectDelay) - connect() - } else { - Log.e(TAG, "已达到最大重试次数($maxRetryAttempts),停止重连") - // 可以在这里添加通知或回调,告知上层连接彻底失败 - } - } - - /** - * 启动心跳检测 - */ - private fun startHeartbeat() { - heartbeatJob?.cancel() - heartbeatJob = CoroutineScope(Dispatchers.IO).launch { - while (isActive && isConnected) { - try { - delay(heartbeatInterval) - - // 发送心跳请求 - val startTime = System.currentTimeMillis() - val response = mcp.ping() - val latency = System.currentTimeMillis() - startTime - - Log.d(TAG, "心跳检测成功,延迟: ${latency}ms") - } catch (e: Exception) { - Log.e(TAG, "心跳检测失败: ${e.message}") - handleConnectionError(e) - break - } - } - } - } - - /** - * 停止心跳检测 - */ - private fun stopHeartbeat() { - heartbeatJob?.cancel() - heartbeatJob = null - } - - /** - * 关闭连接 - */ - override fun close() { - runBlocking { - connectionLock.withLock { - try { - isConnected = false - stopHeartbeat() - mcp.close() - sseTransport?.close() - Log.d(TAG, "已关闭MCP服务器连接") - } catch (e: Exception) { - Log.e(TAG, "关闭MCP服务器连接失败: ${e.message}", e) - } - } - } - } - - /** - * 网络状态监听接口 - */ - interface NetworkStateListener { - fun onNetworkAvailable() - fun onNetworkLost() - } -} - -/** - * MCP客户端,用于连接Model Context Protocol服务器和管理本地函数 - */ -class MCPClient(private val context: Context? = null) : AutoCloseable { - private val TAG = "MCPClient" - - // 本地函数Map,函数名 -> 处理器 - private val localFunctions = mutableMapOf() - - // 本地函数定义Map,函数名 -> 定义 - private val localFunctionDefs = mutableMapOf() - - // 子客户端列表,每个连接一个MCP服务器 - private val subClients = mutableMapOf() - - // 是否已连接 - private var isConnected = false - - // 系统功能处理器 - private val systemFunctionHandler = SystemFunctionHandler(context!!) - - // 共享的HttpClient,用于所有子客户端 - private val sharedHttpClient by lazy { createSslTrustAllClient() } - - init { - // 如果提供了Context,自动注册语音功能处理器 - context?.let { initializeVoiceFunctions(it) } - } - - /** - * 初始化语音功能处理器 - */ - private fun initializeVoiceFunctions(context: Context) { - try { - // 使用SystemFunctionHandler注册所有功能 - systemFunctionHandler.registerAllFunctions(this) - - Log.d(TAG, "语音功能初始化完成") - } catch (e: Exception) { - Log.e(TAG, "语音功能初始化失败: ${e.message}", e) - } - } - - /** - * 连接到MCP服务器(通过SSE) - * 直接接收完整的JSON配置字符串 - * - * @param mcpConfigJson 包含mcpServers字段的JSON配置字符串 - * @return 是否连接成功 - */ - suspend fun connectToSSE(mcpConfigJson: String): Boolean { - // 清除现有连接 - closeAllConnections() - - val serverMap = mutableMapOf() - var configLoaded = false - - // 尝试解析传入的JSON配置 - try { - if (mcpConfigJson.isNotEmpty()) { - val mapper = ObjectMapper() - try { - val config = mapper.readValue(mcpConfigJson, Map::class.java) - - // 处理mcpServers格式 - @Suppress("UNCHECKED_CAST") - val mcpServers = config["mcpServers"] as? Map> - if (mcpServers != null && mcpServers.isNotEmpty()) { - // 从服务器配置中提取URL - mcpServers.forEach { (serverId, serverConfig) -> - val url = serverConfig["url"] - if (!url.isNullOrEmpty()) { - serverMap[serverId] = url - Log.d(TAG, "从配置中添加服务器 [$serverId]: $url") - } - } - configLoaded = serverMap.isNotEmpty() - Log.d(TAG, "从传入的JSON配置加载了 ${serverMap.size} 个服务器") - } - } catch (e: Exception) { - Log.e(TAG, "解析传入的JSON配置失败: ${e.message}") - } - } - } catch (e: Exception) { - Log.e(TAG, "处理传入的配置失败: ${e.message}") - } - - - // 创建并连接所有子客户端 - val connectionResults = mutableMapOf() - - for ((serverId, serverUrl) in serverMap) { - try { - Log.d(TAG, "为服务器 [$serverId],[$serverUrl] 创建子客户端") - val subClient = MCPSubClient(serverId, serverUrl, sharedHttpClient) - val connected = subClient.connect() - - if (connected) { - subClients[serverId] = subClient - connectionResults[serverId] = true - Log.d(TAG, "子客户端 [$serverId] 连接成功") - } else { - subClient.close() - connectionResults[serverId] = false - Log.w(TAG, "子客户端 [$serverId] 连接失败") - } - } catch (e: Exception) { - Log.e(TAG, "创建子客户端 [$serverId] 失败: ${e.message}", e) - connectionResults[serverId] = false - } - } - - // 检查是否有任何客户端连接成功 - val successCount = connectionResults.count { it.value } - isConnected = successCount > 0 - - if (isConnected) { - Log.d(TAG, "已成功连接 $successCount/${serverMap.size} 个MCP服务器") - - // 记录所有可用工具 - val allToolNames = getAllToolNames() - Log.d(TAG, "所有可用工具: ${allToolNames.joinToString(", ")}") - - return true - } else { - Log.e(TAG, "所有MCP服务器连接均失败") - return false - } - } - - /** - * 关闭所有连接 - */ - private fun closeAllConnections() { - subClients.forEach { (serverId, client) -> - try { - client.close() - Log.d(TAG, "已关闭子客户端 [$serverId]") - } catch (e: Exception) { - Log.e(TAG, "关闭子客户端 [$serverId] 失败: ${e.message}") - } - } - subClients.clear() - } - - /** - * 获取所有工具名称 - */ - private fun getAllToolNames(): List { - val toolNames = mutableSetOf() - - // 添加本地函数 - toolNames.addAll(localFunctions.keys) - - // 添加所有子客户端的工具 - subClients.forEach { (_, client) -> - client.getTools().forEach { toolUnion -> - val tool = toolUnion.tool().get() - toolNames.add(tool.name()) - } - } - - return toolNames.toList() - } - - /** - * 注册本地函数 - * @param name 函数名称 - * @param description 函数描述 - * @param parameters 函数参数定义(Map或JSONObject) - * @param handler 函数处理器 - * @return 是否注册成功 - */ - fun registerLocalFunction( - name: String, - description: String, - parameters: Any, - handler: FunctionHandler - ): Boolean { - try { - // 将参数统一转换为Map格式 - val parametersMap: Map = when (parameters) { - is Map<*, *> -> { - @Suppress("UNCHECKED_CAST") - parameters as Map - } - is JSONObject -> { - val mapper = ObjectMapper() - mapper.readValue( - parameters.toString(), - object : TypeReference>() {} - ) - } - else -> { - Log.e(TAG, "参数类型不支持: ${parameters.javaClass.name}") - return false - } - } - - // 检查参数是否包含必要字段 - if (!parametersMap.containsKey("type") || (parametersMap["type"] != "object")) { - Log.e(TAG, "参数必须是object类型") - return false - } - - // 注册函数处理器 - localFunctions[name] = handler - - // 创建函数定义 - val functionDef = mapOf( - "name" to name, - "description" to description, - "parameters" to parametersMap - ) - - // 保存函数定义 - localFunctionDefs[name] = ObjectMapper().writeValueAsString(functionDef) - - // Log.d(TAG, "注册本地函数成功: $name") - return true - } catch (e: Exception) { - Log.e(TAG, "注册本地函数失败: ${e.message}", e) - return false - } - } - - /** - * 取消注册本地函数 - * @param name 函数名称 - * @return 是否取消成功 - */ - fun unregisterLocalFunction(name: String): Boolean { - val removed = localFunctions.remove(name) != null - if (removed) { - localFunctionDefs.remove(name) - Log.d(TAG, "取消注册本地函数成功: $name") - } - return removed - } - - /** - * 获取工具Map列表,用于OpenAI请求 - */ - fun getToolMaps(): List> { - val allToolMaps = mutableListOf>() - - // 首先添加本地函数 - localFunctionDefs.forEach { (name, functionDefJson) -> - try { - val mapper = ObjectMapper() - val functionDef = mapper.readValue(functionDefJson, object : TypeReference>() {}) - allToolMaps.add(mapOf( - "type" to "function", - "function" to functionDef - )) - } catch (e: Exception) { - Log.e(TAG, "解析本地函数定义失败: $name, ${e.message}") - } - } - - // 然后添加所有子客户端的工具 - subClients.forEach { (serverId, client) -> - val clientTools = client.getToolMaps() - allToolMaps.addAll(clientTools) - // Log.d(TAG, "添加子客户端 [$serverId] 的 ${clientTools.size} 个工具") - } - - return allToolMaps - } - - /** - * 获取工具类型 - * @param name 工具名称 - * @return 工具类型,如果不存在则返回null - */ - fun getToolType(name: String): ToolType? { - if (localFunctions.containsKey(name)) { - return ToolType.LOCAL_FUNCTION - } - - if (subClients.any { it.value.containsTool(name) }) { - return ToolType.MCP_TOOL - } - - return null - } - - /** - * 调用工具(统一入口) - * @param name 工具名称 - * @param arguments 参数 - * @return 调用结果 - */ - suspend fun callTool(name: String, arguments: Map): JSONObject? { - val requestId = "${System.currentTimeMillis()}-${(0..1000).random()}" - Log.d(TAG, "[$requestId] 调用工具: $name 参数:$arguments") - - val toolType = getToolType(name) - - val result = when (toolType) { - ToolType.LOCAL_FUNCTION -> { - callLocalFunction(name, arguments) - } - ToolType.MCP_TOOL -> { - callMcpTool(name, arguments, requestId) - } - null -> { - Log.e(TAG, "未找到工具: $name") - JSONObject().apply { put("context","未找到工具: $name") } - } - } - - return result - } - - /** - * 调用本地函数 - */ - private suspend fun callLocalFunction(name: String, arguments: Map): JSONObject? { - return try { - val handler = localFunctions[name] - if (handler != null) { - val context = handler.handle(arguments) - JSONObject().apply { put("context", context) } - } else { - Log.e(TAG, "未找到本地函数: $name") - JSONObject().apply { put("context","未找到本地函数: $name") } - } - } catch (e: Exception) { - Log.e(TAG, "调用本地函数失败: ${e.message}") - JSONObject().apply { put("context","调用本地函数失败: ${e.message}") } - } - } - - /** - * 调用MCP工具 - */ - private suspend fun callMcpTool(name: String, arguments: Map, requestId: String): JSONObject? { - if (!isConnected) { - Log.e(TAG, "未连接到服务器") - return JSONObject().apply { put("context","未连接到MCP服务器") } - } - - // 查找包含此工具的子客户端 - val clientEntry = subClients.entries.find { it.value.containsTool(name) } - - return if (clientEntry != null) { - val (serverId, client) = clientEntry - try { - val startTime = System.currentTimeMillis() - val result = client.callTool(name, arguments) - val duration = System.currentTimeMillis() - startTime - - Log.d(TAG, "[$requestId] 工具调用成功: $name (${duration}ms)") - result - } catch (e: Exception) { - Log.e(TAG, "[$requestId] 工具调用失败: $name, ${e.message}") - JSONObject().apply { put("context", "工具调用失败: ${e.message}") } - } - } else { - Log.e(TAG, "[$requestId] 无可用服务器提供工具: $name") - JSONObject().apply { put("context", "无可用的服务器提供工具: $name") } - } - } - - /** - * 获取可用的工具列表 - */ - fun getTools(): List { - val allTools = mutableListOf() - - // 收集所有子客户端的工具 - subClients.forEach { (_, client) -> - allTools.addAll(client.getTools()) - } - - return allTools - } - - /** - * 检查工具是否存在 - */ - fun hasToolWithName(name: String): Boolean { - return localFunctions.containsKey(name) || - subClients.any { it.value.containsTool(name) } - } - - /** - * 从字符串解析JSON参数 - */ - fun parseJsonArguments(json: String): Map { - return try { - ObjectMapper().readValue(json, object : TypeReference>() {}) - } catch (e: Exception) { - Log.e(TAG, "解析JSON参数失败: ${e.message}", e) - emptyMap() - } - } - - /** - * 检查是否已连接到MCP服务器 - */ - fun isConnected(): Boolean { - return isConnected - } - - /** - * 关闭连接 - */ - override fun close() { - runBlocking { - try { - closeAllConnections() - isConnected = false - Log.d(TAG, "已关闭所有MCP服务器连接") - } catch (e: Exception) { - Log.e(TAG, "关闭MCP服务器连接失败: ${e.message}", e) - } - } - } -} - -/** - * 创建一个信任所有SSL证书的HttpClient - */ -private fun createSslTrustAllClient(): HttpClient { - // 创建信任所有证书的TrustManager - val trustAllCerts = arrayOf( - object : X509TrustManager { - override fun checkClientTrusted(chain: Array?, authType: String?) {} - override fun checkServerTrusted(chain: Array?, authType: String?) {} - override fun getAcceptedIssuers(): Array = arrayOf() - } - ) - - // 创建SSL上下文并初始化它 - val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, trustAllCerts, SecureRandom()) - - // 创建HTTP日志拦截器 - val loggingInterceptor = HttpLoggingInterceptor { message -> - Log.d("OkHttp", message) - }.apply { - level = HttpLoggingInterceptor.Level.HEADERS - } - - // 创建自定义拦截器显示实际请求URL - val urlLoggingInterceptor = Interceptor { chain -> - val request = chain.request() - Log.d("OkHttp-URL", "实际请求URL: ${request.url}") - Log.d("OkHttp-Headers", "请求头: ${request.headers}") - - // 生成curl命令用于调试 - logAsCurlCommand(request) - - val response = chain.proceed(request) - - Log.d("OkHttp-Response", "状态码: ${response.code} - ${response.message}") - Log.d("OkHttp-Response", "响应头: ${response.headers}") - - response - } - - // 创建OkHttpClient并配置信任所有证书 - val okHttpClient = OkHttpClient.Builder() - .sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) - .hostnameVerifier { _, _ -> true } - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - // .addInterceptor(loggingInterceptor) // 添加HTTP日志拦截器 - // .addInterceptor(urlLoggingInterceptor) // 添加URL日志拦截器 - .build() - - // 创建使用OkHttp引擎的HttpClient - return HttpClient(OkHttp) { - engine { - preconfigured = okHttpClient - } - - // 安装SSE插件 - install(SSE) - } -} - -/** - * 将OkHttp请求转换为curl命令并输出到日志 - */ -private fun logAsCurlCommand(request: okhttp3.Request) { - try { - val curlCmd = StringBuilder("curl -X ${request.method}") - - // 添加URL - curlCmd.append(" \"${request.url}\"") - - // 添加请求头 - request.headers.forEach { (name, value) -> - curlCmd.append(" \\\n -H \"$name: $value\"") - } - - // 添加请求体 - val requestBody = request.body - if (requestBody != null) { - val buffer = okio.Buffer() - requestBody.writeTo(buffer) - val bodyContent = buffer.readUtf8() - - // 请求体不为空时添加 - if (bodyContent.isNotEmpty()) { - curlCmd.append(" \\\n -d '${bodyContent.replace("'", "\\'")}'") - } - } - - // 输出到日志 - Log.d("CURL-Command", curlCmd.toString()) - } catch (e: Exception) { - Log.e("CURL-Command", "生成curl命令失败: ${e.message}", e) - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt b/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt deleted file mode 100644 index b04b9a0a..00000000 --- a/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt +++ /dev/null @@ -1,929 +0,0 @@ -package com.yunqiinnovation.open_ai_service - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.os.Handler -import android.os.Looper -import android.util.Base64 -import android.util.Log -import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.databind.ObjectMapper -import kotlinx.coroutines.* -import okhttp3.* -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.RequestBody.Companion.toRequestBody -import okio.Buffer -import org.json.JSONArray -import org.json.JSONObject -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.IOException -import java.util.concurrent.TimeUnit -import kotlin.coroutines.CoroutineContext -import kotlin.math.min -import kotlin.math.sqrt - - -/** - * OpenAI服务的原生实现 - */ -class OpenAIService(private val context: Context? = null) : CoroutineScope { - private val TAG = "OpenAIService" - - /* ---------- 协程与并发 ---------- */ - - // 创建协程Job,用于生命周期管理 - private val job = SupervisorJob() - - // 实现CoroutineScope接口 - override val coroutineContext: CoroutineContext - get() = Dispatchers.IO + job - - // 主线程Handler,用于确保回调在主线程执行 - private val mainHandler = Handler(Looper.getMainLooper()) - - private var baseUrl = "" - private val client = OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .build() - - private var apiKey: String = "" - private var isInitialized = false - private var model: String = "" // 默认模型 - private var visionModel: String = "doubao-1-5-vision-pro-32k-250115" // 默认模型 - - // MCP客户端 - private var mcpClient: MCPClient? = null - private var isMcpInitialized = false - - // 是否自动处理MCP工具调用 - private var autoHandleMcpTools = true - - // 添加成员变量以跟踪当前请求 - private var currentStreamCall: Call? = null - @Volatile private var isCanceled = false - - /** - * 构建curl命令用于测试 - */ - private fun buildCurlCommand(request: Request, body: String): String { - val command = StringBuilder("curl -v -X ${request.method}") - - // 添加请求头 - request.headers.forEach { header -> - // 敏感信息处理:不显示真实的API Key - if (header.first == "Authorization") { - command.append(" -H '${header.first}: Bearer $apiKey'") - } else { - command.append(" -H '${header.first}: ${header.second}'") - } - } - - // 添加请求体 - if (request.method == "POST" || request.method == "PUT") { - // 转义JSON中的单引号,确保curl命令正确 - val escapedBody = body.replace("'", "\\'") - command.append(" -d '${escapedBody}'") - } - - // 添加URL - command.append(" '${request.url}'") - - return command.toString() - } - - /** - * 将文件转换为Base64字符串 - */ - fun fileToBase64(filePath: String, maxSizeKB: Int = 20480): String? { - try { - val file = File(filePath) - if (!file.exists() || !file.isFile) { - Log.e(TAG, "文件不存在: $filePath") - return null - } - - // 读取文件 - var bitmap = BitmapFactory.decodeFile(filePath) ?: return null - - // 检查图片尺寸,限制最大为1024*1024 - val maxDimension = 1024 - if (bitmap.width > maxDimension || bitmap.height > maxDimension) { - // 计算缩放比例,保持纵横比 - val ratio = min(maxDimension.toFloat() / bitmap.width, maxDimension.toFloat() / bitmap.height) - - val newWidth = (bitmap.width * ratio).toInt() - val newHeight = (bitmap.height * ratio).toInt() - - bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) - } - - // 压缩图片 - val outputStream = ByteArrayOutputStream() - var quality = 100 - bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream) - - // 如果原始图片太大,继续优化文件大小 - var fileSize = file.length() / 1024 // 转为KB - if (fileSize > maxSizeKB) { - val scale = sqrt(maxSizeKB.toDouble() / fileSize) - val newWidth = (bitmap.width * scale).toInt() - val newHeight = (bitmap.height * scale).toInt() - bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) - outputStream.reset() - quality = 85 - bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream) - } - - // 转为Base64 - return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) - } catch (e: Exception) { - Log.e(TAG, "转换Base64失败: ${e.message}") - return null - } - } - - /** - * 创建带图片的用户消息 - */ - fun createUserMessageWithImage(text: String, imageBase64: String): JSONObject { - return JSONObject().apply { - put("role", "user") - put("content", JSONArray().apply { - // 添加文本部分 - if (text.isNotEmpty()) { - put(JSONObject().apply { - put("type", "text") - put("text", text) - }) - } - - // 添加图片部分 - put(JSONObject().apply { - put("type", "image_url") - put("image_url", JSONObject().apply { - put("url", "data:image/jpeg;base64,$imageBase64") - }) - }) - }) - } - } - - /** - * 创建用户消息 - */ - fun createUserMessage(content: String): JSONObject { - return JSONObject().apply { - put("role", "user") - put("content", content) - } - } - - /** - * 创建系统消息 - */ - fun createSystemMessage(content: String): JSONObject { - return JSONObject().apply { - put("role", "system") - put("content", content) - } - } - - /** - * 创建助手消息 - */ - fun createAssistantMessage(content: String): JSONObject { - return JSONObject().apply { - put("role", "assistant") - put("content", content) - } - } - - /** - * 初始化OpenAI服务 - */ - fun initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String): Boolean { - this.apiKey = apiKey - if (baseUrl.isNotEmpty()) { - this.baseUrl = baseUrl - } - if (model.isNotEmpty()) { - this.model = model - } - - // 初始化MCPClient - if (mcpClient == null) { - mcpClient = MCPClient(context) - } - initializeMcpClient(mcpServer) - isInitialized = apiKey.isNotEmpty() - return isInitialized - } - - /** - * MCP客户端是否已初始化 - */ - fun isMcpInitialized(): Boolean { - return isMcpInitialized && mcpClient?.isConnected() == true - } - - /** - * 关闭MCP客户端 - */ - fun closeMcpClient() { - mcpClient?.close() - mcpClient = null - isMcpInitialized = false - } - - /** - * 初始化MCP客户端 - */ - fun initializeMcpClient(mcpServer: String): Boolean { - if (mcpClient != null) { - mcpClient?.close() - } - - mcpClient = MCPClient(context) - - // 直接使用类的CoroutineScope启动协程 - launch { - try { - val result = mcpClient?.connectToSSE(mcpServer) ?: false - isMcpInitialized = result - Log.d(TAG, "MCP客户端初始化${if (result) "成功" else "失败"}") - } catch (e: Exception) { - Log.e(TAG, "MCP客户端初始化失败: ${e.message}", e) - isMcpInitialized = false - } - } - - return true // 立即返回,实际连接在后台进行 - } - - /** - * 处理MCP工具调用 - * - * @param functionCall 函数调用JSON对象 - * @return 处理结果,如果处理失败则返回null - */ - suspend fun handleMcpToolCall(functionCall: JSONObject): JSONObject? { - if (mcpClient == null || !isMcpInitialized) { - return JSONObject().apply { put("context","MCP客户端未初始化") } - } - - try { - // 获取函数名称 - val name = functionCall.getString("name") - - // 获取参数 - val argumentsJson = functionCall.getString("arguments") - val arguments = mcpClient?.parseJsonArguments(argumentsJson) ?: mapOf() - - // 调用工具 - return mcpClient?.callTool(name, arguments) - } catch (e: Exception) { - Log.e(TAG, "处理MCP工具调用失败: ${e.message}", e) - return JSONObject().apply { put("context","处理MCP工具调用失败: ${e.message}") } - } - } - - /** - * 注册函数 - */ - fun registerFunction(name: String, description: String, parameters: JSONObject): Boolean { - try { - // 确保MCPClient已初始化 - if (mcpClient == null) { - mcpClient = MCPClient() - } - - // 创建函数处理器 - val handler = object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - // 由于本地函数的实际处理是在Flutter端完成的 - // 这里只需返回一个标记,表示该函数是本地函数 - return "LOCAL_FUNCTION:$name" - } - } - - // 直接委托给MCPClient处理 - return mcpClient?.registerLocalFunction(name, description, parameters, handler) ?: false - } catch (e: Exception) { - Log.e(TAG, "注册函数失败: ${e.message}", e) - return false - } - } - - /** - * 自动处理MCP工具调用 - */ - private suspend fun autoHandleMcpToolCall(functionCall: JSONObject, messages: JSONArray, callback: StreamCallback) { - try { - // 获取函数名称 - val name = functionCall.getString("name") - - // 获取参数 - val argumentsJson = functionCall.getString("arguments") - - Log.d(TAG, "自动处理MCP工具调用: name=${name}, arguments=${argumentsJson}") - val arguments = mcpClient?.parseJsonArguments(argumentsJson) ?: mapOf() - - // 调用工具(统一通过MCPClient处理) - val result = mcpClient?.callTool(name, arguments) - Log.d(TAG, "自动处理MCP工具调用结果: ${result?.toString()}") - // 检查是否已取消 - if (isCanceled) { - Log.d(TAG, "工具调用已被取消,不处理结果") - return - } - - // 回调结果 - val resultObj = result ?: JSONObject().apply { put("context", "工具调用失败") } - callback.onFunctionCallResult(functionCall, resultObj) - - // 发送结果到OpenAI继续对话 - val resultContent = resultObj.optString("context", "工具调用失败") - sendFunctionCallResult( - messages = messages, - functionCall = functionCall, - functionResult = resultContent, - callback = callback - ) - } catch (e: Exception) { - if (!isCanceled) { - Log.e(TAG, "自动处理工具调用失败: ${e.message}", e) - // 失败时返回错误给回调函数 - val errorMessage = "工具调用失败: ${e.message}" - sendFunctionCallResult( - messages = messages, - functionCall = functionCall, - functionResult = errorMessage, - callback = callback - ) - } - } - } - - /** - * 发送消息(非流式输出) - */ - @Throws(OpenAIException::class) - fun sendMessage(messages: JSONArray): String { - if (!isInitialized || apiKey.isEmpty()) { - throw OpenAIException("OpenAI服务未初始化") - } - - val requestBody = JSONObject().apply { - put("model", model) - put("messages", messages) - put("temperature", 0.7) - put("max_tokens", 2000) - put("stream", false) - - // 添加工具列表 - val tools = JSONArray() - - // 使用MCPClient提供的所有工具 - mcpClient?.getToolMaps()?.forEach { toolMap -> - try { - val tool = JSONObject(toolMap) - tools.put(tool) - } catch (e: Exception) { - Log.e(TAG, "转换工具失败: ${e.message}", e) - } - } - - // 如果有工具,则添加到请求中 - if (tools.length() > 0) { - put("tools", tools) - } - } - - val mediaType = "application/json".toMediaTypeOrNull() - val request = Request.Builder() - .url(baseUrl) - .addHeader("Content-Type", "application/json") - .addHeader("Authorization", "Bearer $apiKey") - .post(requestBody.toString().toRequestBody(mediaType)) - .build() - - try { - // 输出用于测试的curl命令 - // val curlCommand = buildCurlCommand(request, requestBody.toString()) - // Log.d(TAG, "curl command: \n$curlCommand") - - client.newCall(request).execute().use { response -> - if (!response.isSuccessful) { - throw OpenAIException("API调用失败: ${response.code}") - } - - val responseBody = response.body?.string() ?: throw OpenAIException("Empty response") - val jsonResponse = JSONObject(responseBody) - - // 检查是否有函数调用 - if (jsonResponse.has("choices") && - jsonResponse.getJSONArray("choices").length() > 0) { - - val choice = jsonResponse.getJSONArray("choices").getJSONObject(0) - - // 检查是否是函数调用 - if (choice.has("message")) { - val message = choice.getJSONObject("message") - - // 检查是否有工具调用 - if (message.has("tool_calls")) { - val toolCalls = message.getJSONArray("tool_calls") - if (toolCalls.length() > 0) { - val toolCall = toolCalls.getJSONObject(0) - if (toolCall.has("function")) { - val function = toolCall.getJSONObject("function") - val functionCall = JSONObject().apply { - put("name", function.getString("name")) - put("arguments", function.getString("arguments")) - put("id", toolCall.getString("id")) - } - return functionCall.toString() - } - } - } - - // 如果没有工具调用,返回消息内容 - if (message.has("content")) { - return message.getString("content") - } - } - } - - throw OpenAIException("Invalid response format") - } - } catch (e: Exception) { - if (e is OpenAIException) throw e - throw OpenAIException("Failed to communicate with AI service: ${e.message}") - } - } - - /** - * 发送消息(流式输出) - */ - fun sendMessageStream(messages: JSONArray, callback: StreamCallback) { - if (!isInitialized || apiKey.isEmpty()) { - callback.onError(OpenAIException("OpenAI服务未初始化")) - return - } - - // 重置取消状态 - isCanceled = false - - // 检查最后一条消息内容中是否包含 "image_url",决定使用哪个模型 - var currentModel = model - if (messages.length() > 0) { - val lastMessage = messages.getJSONObject(messages.length() - 1) - if (lastMessage.has("content")) { - val content = lastMessage.optString("content", "") - if (content.contains("image_url")) { - currentModel = visionModel - } - } - } - - val requestBody = JSONObject().apply { - put("model", currentModel) - put("messages", messages) - put("temperature", 0.7) - put("max_tokens", 2000) - put("stream", true) - - // 添加工具列表 - val tools = JSONArray() - - // 使用MCPClient提供的所有工具 - mcpClient?.getToolMaps()?.forEach { toolMap -> - try { - val tool = JSONObject(toolMap) - tools.put(tool) - } catch (e: Exception) { - Log.e(TAG, "转换工具失败: ${e.message}", e) - } - } - - // 如果有工具,则添加到请求中 - if (tools.length() > 0) { - put("tools", tools) - } - Log.i(TAG, "liwei------------------- 请求Ai对话: context:${messages.getJSONObject(messages.length() - 1).get("content").toString()} 携带工具数据 ${tools.length()}") - } - - val mediaType = "application/json".toMediaTypeOrNull() - val request = Request.Builder() - .url(baseUrl) - .addHeader("Content-Type", "application/json") - .addHeader("Authorization", "Bearer $apiKey") - .addHeader("Accept", "text/event-stream") - .post(requestBody.toString().toRequestBody(mediaType)) - .build() - - val call = client.newCall(request) - // 保存当前Call对象以便后续取消 - currentStreamCall = call - // Log.d(TAG, "发送消息流式输出: request=$request") - call.enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - Log.d(TAG, "发送消息流式输出失败: e=$e") - // 如果是由于取消导致的失败,不报告错误 - if (isCanceled) { - return - } - callback.onError(OpenAIException(e.message ?: "请求失败")) - } - - override fun onResponse(call: Call, response: Response) { - // Log.d(TAG, "发送消息流式输出成功: response=$response") - // 检查请求是否被取消 - if (isCanceled) { - response.body?.close() - return - } - - if (!response.isSuccessful) { - callback.onError(OpenAIException("API调用失败: ${response.code}")) - return - } - - val responseBody = response.body ?: return - val source = responseBody.source() - val buffer = Buffer() - - try { - // 用于存储函数调用的各个部分 - val toolCalls = mutableMapOf() - - // 逐块读取数据,而不是一次性预取 - val BUFFER_SIZE = 8192L // 8KB的块大小 - - while (!Thread.currentThread().isInterrupted && !isCanceled) { - // 读取一小块数据 - val bytesRead = source.read(buffer, BUFFER_SIZE) - if (bytesRead == -1L) break // 流结束 - - // 处理接收到的数据 - while (true) { - val line = buffer.readUtf8Line() ?: break - if (line.trim().isEmpty()) continue - - if (line.startsWith("data:")) { - val data = line.substring(5).trim() - - // 处理[DONE]消息 - if (data == "[DONE]" || data == "[\"DONE\"]") { - val hasToolCalls = processToolCalls(toolCalls, callback, messages) - // 如果没有工具调用,这是对话的真正结束 - if (!hasToolCalls) { - callback.onComplete() - } - return - } - - // 验证JSON是否完整(至少检查大括号匹配) - if (isValidJson(data)) { - try { - handleStreamJson(JSONObject(data), toolCalls, callback, messages) - } catch (e: Exception) { - // 忽略解析错误,但记录日志 - Log.e(TAG, "解析JSON出错: ${e.message}", e) - } - } else { - // 记录不完整的JSON,但不尝试解析它 - Log.d(TAG, "收到不完整的JSON数据,跳过处理, data=$data") - } - } - } - } - - // 如果是因为取消导致循环结束,直接返回 - if (isCanceled) { - return - } - - // 如果没有明确的完成信号但数据流结束了,处理最后可能的工具调用 - val hasToolCalls = processToolCalls(toolCalls, callback, messages) - if (!hasToolCalls) { - callback.onComplete() - } - } catch (e: Exception) { - // 如果是由于取消导致的异常,不报告错误 - if (isCanceled) { - return - } - callback.onError(OpenAIException("处理响应流时出错: ${e.message}")) - } finally { - responseBody.close() - } - } - }) - } - - /** - * 处理流式JSON数据 - */ - private fun handleStreamJson( - json: JSONObject, - toolCalls: MutableMap, - callback: StreamCallback, - messages: JSONArray - ) { - if (json.has("choices") && json.getJSONArray("choices").length() > 0) { - val choice = json.getJSONArray("choices").getJSONObject(0) - - // 处理delta内容 - if (choice.has("delta")) { - val delta = choice.getJSONObject("delta") - - // 处理普通文本内容 - if (delta.has("content")) { - val content = delta.getString("content") - // Log.d(TAG, "处理流式JSON数据: content=$content") - if (!isCanceled) { - callback.onToken(content) - } - } - - // 收集工具调用信息 - if (delta.has("tool_calls")) { - val toolCallsArray = delta.getJSONArray("tool_calls") - for (i in 0 until toolCallsArray.length()) { - val toolCall = toolCallsArray.getJSONObject(i) - val index = toolCall.getInt("index") - - // 创建或获取现有的工具调用信息 - val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() } - - // 更新ID - if (toolCall.has("id")) { - toolCallInfo.id = toolCall.getString("id") - } - - // 更新函数信息 - if (toolCall.has("function")) { - val function = toolCall.getJSONObject("function") - - if (function.has("name")) { - toolCallInfo.name = function.getString("name") - } - - if (function.has("arguments")) { - toolCallInfo.arguments += function.getString("arguments") - } - } - } - Log.i(TAG, "liwei---------------------- AI发起工具调用: tool=$toolCallsArray") - } - } - - } - } - - /** - * 处理工具调用 - */ - private fun processToolCalls( - toolCalls: Map, - callback: StreamCallback, - messages: JSONArray? = null - ): Boolean { - if (toolCalls.isEmpty()) return false - - // 只处理第一个工具调用 - val firstToolCall = toolCalls.entries.firstOrNull()?.value ?: return false - - if (firstToolCall.isValid()) { - // 创建函数调用JSON对象 - val functionCall = JSONObject().apply { - put("name", firstToolCall.name) - put("arguments", firstToolCall.arguments) - put("id", firstToolCall.id) - } - Log.i(TAG, "liwei---------------------- 工具调用: id=${firstToolCall.id}, name=${firstToolCall.name} arguments=${firstToolCall.arguments}") - - // 通知上层工具调用事件 - callback.onFunctionCall(functionCall) - - // 在协程中处理工具调用 - if (messages != null) { - launch { - try { - if (!isCanceled) { - // 调用工具 - val result = try { - val args = mcpClient?.parseJsonArguments(firstToolCall.arguments) ?: mapOf() - mcpClient?.callTool(firstToolCall.name, args) - } catch (e: Exception) { - Log.e(TAG, "liwei----------------------- 工具调用错误: $e") - JSONObject().put("context", "工具调用失败: ${e.message}") - } - - if (!isCanceled) { - - // 处理结果 - val resultObj = result ?: JSONObject().put("context", "工具调用失败") - callback.onFunctionCallResult(functionCall, resultObj) - - val context = resultObj.optString("context", "工具调用失败").let { - if (it.isNullOrEmpty()) "工具调用失败" else it - } - Log.i(TAG, "liwei----------------------- mcp工具调用完成: name=${firstToolCall.name}, context=$context") - - // 将结果发送回OpenAI继续对话 - sendFunctionCallResult( - messages = messages, - functionCall = functionCall, - functionResult = context, - callback = callback - ) - } - } - } catch (e: Exception) { - if (!isCanceled) { - Log.e(TAG, "liwei----------------------- 处理工具调用异常: $e") - try { - val errorMessage = "工具调用处理失败: ${e.message}" - sendFunctionCallResult( - messages = messages, - functionCall = functionCall, - functionResult = errorMessage, - callback = callback - ) - } catch (e2: Exception) { - Log.e(TAG, "发送工具调用结果失败: ${e2.message}") - } - } - } - } - } - return true - } - return false - } - - /** - * 发送函数调用结果 - */ - fun sendFunctionCallResult( - messages: JSONArray, - functionCall: JSONObject, - functionResult: String, - callback: StreamCallback - ) { - if (isCanceled) { - Log.d(TAG, "请求已取消,不发送函数调用结果") - return - } - - try { - val fullMessages = JSONArray() - - // 添加用户消息 - for (i in 0 until messages.length()) { - fullMessages.put(messages.getJSONObject(i)) - } - - // 添加函数调用消息 - val callId = functionCall.optString("id", "call_${System.currentTimeMillis()}") - fullMessages.put(JSONObject().apply { - put("role", "assistant") - put("content", "") - - // 添加工具调用 - val toolCalls = JSONArray().apply { - val toolCall = JSONObject().apply { - put("id", callId) - put("type", "function") - put("function", JSONObject().apply { - put("name", functionCall.getString("name")) - put("arguments", functionCall.getString("arguments")) - }) - } - put(toolCall) - } - put("tool_calls", toolCalls) - }) - - // 添加函数调用结果 - fullMessages.put(JSONObject().apply { - put("role", "tool") - put("content", functionResult) - put("tool_call_id", callId) - }) - - // 发送完整对话 - sendMessageStream(fullMessages, callback) - - } catch (e: Exception) { - if (!isCanceled) { - Log.e(TAG, "发送函数调用结果失败: ${e.message}", e) - callback.onError(OpenAIException("发送函数调用结果失败: ${e.message}")) - } - } - } - - /** - * 取消当前流式请求 - * - * @return 是否成功取消 - */ - fun cancelCurrentStream(): Boolean { - try { - // 标记已取消 - isCanceled = true - - // 取消当前Call对象 - val call = currentStreamCall - if (call != null && !call.isCanceled()) { - call.cancel() - Log.d(TAG, "已取消当前流式请求") - return true - } - return false - } catch (e: Exception) { - Log.e(TAG, "取消流式请求失败: ${e.message}", e) - return false - } finally { - // 清除当前Call引用 - currentStreamCall = null - } - } - - /** - * 取消所有操作并释放资源 - * 供AgentService.dispose()调用 - */ - fun cancelAll() { - cancelCurrentStream() - job.cancel() // 取消所有协程 - } - - /** - * 工具调用信息类 - */ - private class ToolCallInfo { - var id: String = "" - var name: String = "" - var arguments: String = "" - - fun isValid(): Boolean { - return id.isNotEmpty() && name.isNotEmpty() - } - } - - /** - * 流式输出回调接口 - */ - interface StreamCallback { - fun onToken(token: String) - fun onComplete() - fun onError(e: Exception) - fun onFunctionCall(functionCall: JSONObject) - fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) - } - - /** - * 检查字符串是否为有效的JSON - * 使用简单的括号匹配和引号检查来确定JSON是否完整 - */ - private fun isValidJson(jsonStr: String): Boolean { - if (jsonStr.isEmpty()) return false - if (!jsonStr.startsWith("{") || !jsonStr.endsWith("}")) return false - - var braceCount = 0 - var insideQuotes = false - var escapeNext = false - - for (char in jsonStr) { - if (escapeNext) { - escapeNext = false - continue - } - - when (char) { - '\\' -> escapeNext = true - '"' -> if (!escapeNext) insideQuotes = !insideQuotes - '{' -> if (!insideQuotes) braceCount++ - '}' -> if (!insideQuotes) braceCount-- - } - - // 如果括号不匹配(闭合太多),提前返回 - if (braceCount < 0) return false - } - - // 所有引号都应该闭合,所有括号也应该匹配 - return braceCount == 0 && !insideQuotes - } -} - -/** - * OpenAI服务异常 - */ -class OpenAIException(message: String) : Exception(message) \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt b/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt deleted file mode 100644 index 09a0d065..00000000 --- a/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt +++ /dev/null @@ -1,272 +0,0 @@ -package com.yunqiinnovation.open_ai_service - -import android.content.Context -import android.os.Handler -import android.os.Looper -import android.util.Log -import kotlinx.serialization.json.Json -import androidx.annotation.NonNull -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.EventChannel.EventSink -import io.flutter.plugin.common.EventChannel.StreamHandler -import org.json.JSONArray -import org.json.JSONObject -import java.util.concurrent.CountDownLatch -import java.util.concurrent.Executors -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.JsonObject -import kotlin.math.min - -/** OpenAIServicePlugin */ -class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler { - /// 方法通道名称 - private val methodChannelName = "com.yunqiinnovation.open_ai_service/methods" - - /// 事件通道名称 - private val eventChannelName = "com.yunqiinnovation.open_ai_service/events" - - /// 方法通道 - private lateinit var methodChannel: MethodChannel - - /// 事件通道 - private lateinit var eventChannel: EventChannel - - /// 应用上下文 - private lateinit var context: Context - - /// OpenAI服务实例 - private lateinit var openAIService: OpenAIService - - /// 事件接收器(用于流式输出) - private var eventSink: EventSink? = null - - /// 执行器(用于真正需要在后台线程处理的任务) - private val executor = Executors.newSingleThreadExecutor() - - /// 主线程Handler - private val mainHandler = Handler(Looper.getMainLooper()) - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - // 保存上下文 - context = flutterPluginBinding.applicationContext - - // 初始化OpenAI服务 - openAIService = OpenAIService(context) - - // 初始化方法通道 - methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, methodChannelName) - methodChannel.setMethodCallHandler(this) - - // 初始化事件通道 - eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, eventChannelName) - eventChannel.setStreamHandler(this) - } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - when (call.method) { - "initialize" -> { - val apiKey = call.argument("apiKey") ?: "" - val baseUrl = call.argument("baseUrl") ?: "" - val model = call.argument("model") ?: "" - val mcpServer = call.argument("mcpServer") ?: "" - - val initialized = openAIService.initialize(apiKey, baseUrl, model, mcpServer) - result.success(initialized) - } - - - "sendMessageStream" -> { - val messagesRaw = call.argument>>("messages") ?: emptyList() - // 检查事件接收器 - if (eventSink == null) { - result.error("NO_EVENT_SINK", "没有可用的事件流接收器", null) - return - } - - // 转换消息格式 - val messages = JSONArray() - for (message in messagesRaw) { - messages.put(JSONObject(message)) - } - - // 不再在后台线程包装OkHttp的异步调用 - try { - openAIService.sendMessageStream( - messages = messages, - callback = object : OpenAIService.StreamCallback { - override fun onToken(token: String) { - // 发送token事件 - mainHandler.post { - eventSink?.success(mapOf("type" to "token", "content" to token)) - } - } - - override fun onComplete() { - // 发送完成事件 - mainHandler.post { - eventSink?.success(mapOf("type" to "complete")) - } - } - - override fun onError(e: Exception) { - // 发送错误事件 - mainHandler.post { - eventSink?.success(mapOf("type" to "error", "content" to e.message)) - } - } - - override fun onFunctionCall(functionCall: JSONObject) { - mainHandler.post { - eventSink?.success(mapOf("type" to "function_call", "content" to functionCall.toString())) - } - } - - override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) { - mainHandler.post { - eventSink?.success( - mapOf("type" to "function_call_result" - ,"function_call" to functionCall.toString() - , "result" to functionCallResult.toString())) - } - } - } - ) - - // 请求已开始,直接返回结果 - result.success(true) - } catch (e: Exception) { - // 直接在当前线程处理错误 - result.error("OPENAI_ERROR", e.message, null) - } - } - - - "createUserMessage" -> { - val content = call.argument("content") ?: "" - val message = openAIService.createUserMessage(content) - result.success(message.toMap()) - } - - "createAssistantMessage" -> { - val content = call.argument("content") ?: "" - val message = openAIService.createAssistantMessage(content) - result.success(message.toMap()) - } - - else -> { - result.notImplemented() - } - } - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - methodChannel.setMethodCallHandler(null) - eventChannel.setStreamHandler(null) - executor.shutdown() - } - - // Stream事件处理 - override fun onListen(arguments: Any?, eventSink: EventSink?) { - this.eventSink = eventSink - } - - override fun onCancel(arguments: Any?) { - this.eventSink = null - } - - // 工具方法:JSONObject转Map - private fun JSONObject.toMap(): Map { - val map = mutableMapOf() - val keys = this.keys() - while (keys.hasNext()) { - val key = keys.next() - var value: Any? = this.opt(key) - - value = when (value) { - JSONObject.NULL -> null - is JSONObject -> value.toMap() - is JSONArray -> value.toList() - else -> value - } - - map[key] = value - } - return map - } - - // 工具方法:JSONArray转List - private fun JSONArray.toList(): List { - val list = mutableListOf() - for (i in 0 until this.length()) { - var value: Any? = this.opt(i) - - value = when (value) { - JSONObject.NULL -> null - is JSONObject -> value.toMap() - is JSONArray -> value.toList() - else -> value - } - - list.add(value) - } - return list - } - - private fun initializeMcpClient(call: MethodCall, result: Result) { - val serverUrl = call.argument("serverUrl") - if (serverUrl == null) { - result.error("INVALID_ARGUMENT", "Missing serverUrl", null) - return - } - - val success = openAIService.initializeMcpClient(serverUrl) - result.success(success) - } - - private fun isMcpInitialized(call: MethodCall, result: Result) { - result.success(openAIService.isMcpInitialized()) - } - - private fun closeMcpClient(call: MethodCall, result: Result) { - openAIService.closeMcpClient() - result.success(true) - } - - private fun handleMcpToolCall(call: MethodCall, result: Result) { - val functionCall = call.argument("functionCall") - if (functionCall == null) { - result.error("INVALID_ARGUMENT", "Missing functionCall", null) - return - } - - try { - val jsonFunctionCall = JSONObject(functionCall) - - // 使用协程处理异步操作 - CoroutineScope(Dispatchers.IO).launch { - try { - val toolResult = openAIService.handleMcpToolCall(jsonFunctionCall) - - // 切换到主线程返回结果 - withContext(Dispatchers.Main) { - result.success(toolResult ?: "无法处理MCP工具调用") - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - result.error("MCP_TOOL_ERROR", "处理MCP工具调用失败: ${e.message}", null) - } - } - } - } catch (e: Exception) { - result.error("INVALID_ARGUMENT", "解析functionCall失败: ${e.message}", null) - } - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt b/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt deleted file mode 100644 index 286fa9cb..00000000 --- a/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt +++ /dev/null @@ -1,1168 +0,0 @@ -package com.yunqiinnovation.open_ai_service - -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.location.Location -import android.location.LocationListener -import android.location.LocationManager -import android.net.Uri -import android.os.Bundle -import android.provider.AlarmClock -import android.provider.CalendarContract -import android.provider.MediaStore -import android.provider.Settings -import android.util.Log -import org.json.JSONObject -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.coroutines.resume -import android.view.KeyEvent -import android.os.Handler -import android.os.Looper -import kotlinx.coroutines.withContext -import kotlinx.coroutines.Dispatchers -import android.Manifest -import android.content.ContentValues -import android.content.ContentUris -import androidx.core.content.ContextCompat -import android.app.ActivityManager -import android.app.Application -import android.content.ComponentName -import android.media.AudioManager -import android.os.Process -/** - * 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能 - */ -class SystemFunctionHandler(private val context: Context) { - companion object { - private const val TAG = "SystemFunctionHandler" - const val ACTION_EXIT_INTERACTION = "com.yimai.eaimar.action.EXIT_INTERACTION" - } - - /** - * 在MCPClient中注册所有系统功能 - * @param client MCPClient实例 - */ - fun registerAllFunctions(client: MCPClient) { - try { - // 注册退出交互函数 - registerExitInteractionFunction(client) - - // 注册翻译模式函数 - registerTranslationModeFunction(client) - - // 注册设置提醒函数 - registerSetReminderFunction(client) - - // 注册获取当前时间函数 - registerGetCurrentTimeFunction(client) - - // 注册获取当前位置函数 - registerGetCurrentLocationFunction(client) - - // 注册媒体控制函数 - registerMediaPlayFunction(client) - registerMediaPauseFunction(client) - registerMediaPreviousFunction(client) - registerMediaNextFunction(client) - - // 注册打开录音机功能 - registerOpenRecorderFunction(client) - - // 注册关闭导航服务功能 - registerStopNavigationFunction(client) - - //增加音量 - registerAddVolumeFunction(client) - //减少音量 - registerReduceVolumeFunction(client) - //静音 - registerMuteVolumeFunction(client) - Log.d(TAG, "所有系统功能注册完成") - } catch (e: Exception) { - Log.e(TAG, "注册系统功能失败: ${e.message}", e) - } - } - - - /** - * 注册退出交互函数 - */ - private fun registerExitInteractionFunction(client: MCPClient) { - client.registerLocalFunction( - "exit_interaction", - "结束当前交互", - JSONObject(""" - { - "type": "object", - "properties": {}, - "required": [] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - // 调用发送广播的函数 - // sendExitInteractionBroadcast() - - return "{\"result\": \"已结束当前交互\"}" - } - } - ) - Log.d(TAG, "已注册退出交互功能") - } - - /** - * 注册翻译模式函数 - */ - private fun registerTranslationModeFunction(client: MCPClient) { - client.registerLocalFunction( - "enter_translation_mode", - "用户请求进入实时翻译模式时,启动实时翻译功能", - JSONObject(""" - { - "type": "object", - "properties": {}, - "required": [] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - val result = enterTranslationMode() - return "{\"result\": \"${result.message}\"}" - } - } - ) - Log.d(TAG, "已注册翻译模式功能") - } - - /** - * 注册设置提醒函数 - */ - private fun registerSetReminderFunction(client: MCPClient) { - client.registerLocalFunction( - "set_reminder", - "设置提醒事项", - JSONObject(""" - { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "提醒标题" - }, - "content": { - "type": "string", - "description": "提醒内容" - }, - "time": { - "type": "string", - "description": "提醒时间,格式为'yyyy-MM-dd HH:mm',如'2023-12-31 14:30'" - } - }, - "required": ["title", "time"] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - val title = arguments["title"] as? String ?: "" - val content = arguments["content"] as? String ?: "" - val time = arguments["time"] as? String ?: "" - val result = setReminder(title, content, time) - return "{\"result\": \"${result.message}\"}" - } - } - ) - Log.d(TAG, "已注册设置提醒功能") - } - - /** - * 注册获取当前时间函数 - */ - private fun registerGetCurrentTimeFunction(client: MCPClient) { - client.registerLocalFunction( - "get_current_time", - "获取当前日期和时间,返回值已按当前语言本地化并对TTS友好(不含冒号等易被误读的符号)。播报或回复给用户时必须原样使用 result 字段的文本,不要自行改写为 HH:mm:ss 等格式。", - JSONObject(""" - { - "type": "object", - "properties": { - "format": { - "type": "string", - "enum": ["short", "date", "time", "full", "iso"], - "description": "时间输出粒度,仅可使用枚举值:short(仅时分)、date(仅日期)、time(时分秒)、full(日期+星期+时间)、iso(机器可读)。禁止传入 SimpleDateFormat/strftime 等格式串。" - } - }, - "required": [] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - val format = arguments["format"] as? String - val result = getCurrentTime(format) - return "{\"result\": \"${result.message}\", \"time\": \"${result.message}\"}" - } - } - ) - Log.d(TAG, "已注册获取当前时间功能") - } - - /** - * 注册获取当前位置函数 - */ - private fun registerGetCurrentLocationFunction(client: MCPClient) { - client.registerLocalFunction( - "get_current_location", - "获取当前地理位置", - JSONObject(""" - { - "type": "object", - "properties": {}, - "required": [] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - val result = getCurrentLocation() - return "{\"result\": \"${result.message}\", \"success\": ${result.success}}" - } - } - ) - Log.d(TAG, "已注册获取当前位置功能") - } - - /** - * 注册媒体播放功能 - */ - private fun registerMediaPlayFunction(client: MCPClient) { - client.registerLocalFunction( - "media_resume", - "继续媒体播放", - JSONObject(""" - { - "type": "object", - "properties": {}, - "required": [] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - val result = controlMedia("play") - return "{\"result\": \"${result.message}\"}" - } - } - ) - Log.d(TAG, "已注册媒体播放功能") - } - - /** - * 注册媒体暂停功能 - */ - private fun registerMediaPauseFunction(client: MCPClient) { - client.registerLocalFunction( - "media_pause", - "暂停媒体播放", - JSONObject(""" - { - "type": "object", - "properties": {}, - "required": [] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - val result = controlMedia("pause") - return "{\"result\": \"${result.message}\"}" - } - } - ) - Log.d(TAG, "已注册媒体暂停功能") - } - - /** - * 注册媒体上一首功能 - */ - private fun registerMediaPreviousFunction(client: MCPClient) { - client.registerLocalFunction( - "media_previous", - "播放上一首", - JSONObject(""" - { - "type": "object", - "properties": {}, - "required": [] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - val result = controlMedia("previous") - return "{\"result\": \"${result.message}\"}" - } - } - ) - Log.d(TAG, "已注册媒体上一首功能") - } - - /** - * 注册媒体下一首功能 - */ - private fun registerMediaNextFunction(client: MCPClient) { - client.registerLocalFunction( - "media_next", - "播放下一首", - JSONObject(""" - { - "type": "object", - "properties": {}, - "required": [] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - val result = controlMedia("next") - return "{\"result\": \"${result.message}\"}" - } - } - ) - Log.d(TAG, "已注册媒体下一首功能") - } - - /** - * 注册打开录音机功能 - */ - private fun registerOpenRecorderFunction(client: MCPClient) { - client.registerLocalFunction( - "open_recorder", - "打开系统录音机并开始录音", - JSONObject(""" - { - "type": "object", - "properties": {}, - "required": [] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - val result = openRecorder() - return "{\"result\": \"${result.message}\"}" - } - } - ) - Log.d(TAG, "已注册打开录音机功能") - } - /** - * 注册获取当前时间函数 - */ - private fun registerStopNavigationFunction(client: MCPClient) { - client.registerLocalFunction( - "stop_navigation", - "结束当前导航服务", - JSONObject(""" - { - "type": "object", - "properties": {}, - "required": [] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - val result = stopNavigation() - return "{\"result\": \"${result.message}\"}" - } - } - ) - Log.d(TAG, "已注册获取当前时间功能") - } - /** - * 注册添加音量函数 - */ - private fun registerAddVolumeFunction(client: MCPClient) { - client.registerLocalFunction( - "add_volume", - "增大音量", - JSONObject(""" - { - "type": "object", - "properties": {}, - "required": [] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - val result = adjustVolume(context, AudioManager.ADJUST_RAISE) - return "{\"result\": \"增加音量成功!\"}" - } - } - ) - Log.d(TAG, "已注册音量调节功能") - } - - /** - * 注册减少音量函数 - */ - private fun registerReduceVolumeFunction(client: MCPClient) { - client.registerLocalFunction( - "reduce_volume", - "减少音量", - JSONObject(""" - { - "type": "object", - "properties": {}, - "required": [] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - val result = adjustVolume(context, AudioManager.ADJUST_LOWER) - return "{\"result\": \"减少音量成功!\"}" - } - } - ) - Log.d(TAG, "已注册音量调节功能") - } - - /** - * 注册静音函数 - */ - private fun registerMuteVolumeFunction(client: MCPClient) { - client.registerLocalFunction( - "mute_volume", - "静音设置", - JSONObject(""" - { - "type": "object", - "properties": {}, - "required": [] - } - """), - object : FunctionHandler { - override suspend fun handle(arguments: Map): String { - val result = setVolume(context, AudioManager.STREAM_MUSIC, 0) - return "{\"result\": \"减少音量成功!\"}" - } - } - ) - Log.d(TAG, "已注册音量调节功能") - } - - /** - * 控制媒体播放 - * @param action 操作类型:"play", "pause", "previous", "next" - * @return 操作结果 - */ - private fun controlMedia(action: String): Result { - Log.d(TAG, "媒体控制: $action") - - try { - val intent = Intent() - - when (action) { - "play" -> { - // 尝试发送播放广播 - intent.action = "android.intent.action.MEDIA_PLAY" - context.sendBroadcast(intent) - - // 也尝试使用媒体按键广播 - val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { - putExtra( - Intent.EXTRA_KEY_EVENT, - android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PLAY) - ) - flags = Intent.FLAG_RECEIVER_FOREGROUND - } - keyIntent.setPackage("com.yimai.eaimar") - context.sendBroadcast(keyIntent) - return Result( - success = true, - message = "已开始播放媒体" - ) - } - "pause" -> { - // 发送完整的按键事件序列 - val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE) - val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON) - keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, downEvent) - keyIntent.setPackage("com.yimai.eaimar") - context.sendBroadcast(keyIntent) - - return Result( - success = true, - message = "已暂停媒体播放" - ) - } - "previous" -> { - // 尝试发送上一首广播 - intent.action = "android.intent.action.MEDIA_PREVIOUS" - context.sendBroadcast(intent) - - // 也尝试使用媒体按键广播 - val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { - putExtra( - Intent.EXTRA_KEY_EVENT, - android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS) - ) - flags = Intent.FLAG_RECEIVER_FOREGROUND - } - keyIntent.setPackage("com.yimai.eaimar") - context.sendBroadcast(keyIntent) - return Result( - success = true, - message = "已切换到上一首" - ) - } - "next" -> { - // 尝试发送下一首广播 - intent.action = "android.intent.action.MEDIA_NEXT" - context.sendBroadcast(intent) - - // 也尝试使用媒体按键广播 - val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { - putExtra( - Intent.EXTRA_KEY_EVENT, - android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_NEXT) - ) - flags = Intent.FLAG_RECEIVER_FOREGROUND - } - keyIntent.setPackage("com.yimai.eaimar") - context.sendBroadcast(keyIntent) - return Result( - success = true, - message = "已切换到下一首" - ) - } - else -> return Result( - success = false, - message = "未知的媒体控制操作: $action" - ) - } - } catch (e: Exception) { - Log.e(TAG, "媒体控制失败: ${e.message}", e) - return Result( - success = false, - message = "媒体控制失败: ${e.message}" - ) - } - } - - /** - * 发送退出交互广播 - */ - private fun sendExitInteractionBroadcast() { - // 发送广播通知VoiceInteractionHandler停止语音识别 - val intent = Intent(ACTION_EXIT_INTERACTION).apply { - // 明确指定接收广播的包名 - setPackage(context.packageName) - putExtra("timestamp", System.currentTimeMillis()) - // 添加FLAG_RECEIVER_FOREGROUND确保及时处理 - addFlags(Intent.FLAG_RECEIVER_FOREGROUND) - } - Log.d(TAG, "准备发送停止交互广播,包名: ${context.packageName}") - context.sendBroadcast(intent) - Log.d(TAG, "已发送停止交互广播") - } - - - - /** - * 启动翻译模式 - * @return 操作结果 - */ - fun enterTranslationMode(): Result { - Log.d(TAG, "进入翻译模式") - // 此处应添加实际的翻译模式实现 - // 可能需要启动一个专门的翻译服务或活动 - - // 广播通知APP的其他部分进入翻译模式 - val intent = Intent("com.yimai.eaimar.ACTION_ENTER_TRANSLATION_MODE").apply { - setPackage(context.packageName) - - putExtra("timestamp", System.currentTimeMillis()) - // 添加FLAG_RECEIVER_FOREGROUND确保及时处理 - addFlags(Intent.FLAG_RECEIVER_FOREGROUND) - } - - // 发送广播 - context.sendBroadcast(intent) - Log.d(TAG, "已发送广播: ENTER_TRANSLATION_MODE") - - return Result( - success = true, - message = "已进入实时翻译模式" - ) - } - - /** - * 检查权限并根据应用状态采取不同行为 - * @param permission 需要检查的权限 - * @param permissionName 权限名称(用于显示给用户) - * @param functionName 功能名称(用于日志和提示) - * @return 是否拥有权限 - */ - private fun checkPermissionAndGuide(permission: String, permissionName: String, functionName: String): Boolean { - if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) { - Log.w(TAG, "缺少${permissionName}权限,无法执行${functionName}") - - // 检查应用是否在前台 - if (isAppInForeground()) { - // 应用在前台,尝试直接弹出权限申请框 - try { - // 首先尝试发送广播通知DeepSound应用弹出权限申请对话框 - val intent = Intent("com.yimai.eaimar.action.REQUEST_PERMISSION").apply { - setPackage(context.packageName) - putExtra("permission", permission) - putExtra("permissionName", permissionName) - putExtra("functionName", functionName) - addFlags(Intent.FLAG_RECEIVER_FOREGROUND) - } - context.sendBroadcast(intent) - - // 由于插件无法直接请求权限(需要Activity),上面的广播会让主应用来处理权限请求 - // 这里设置一个短暂延迟,让用户有时间响应权限请求 - Thread.sleep(500) - - // 再次检查权限是否已授予(以防用户立即授予) - if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) { - return true - } - } catch (e: Exception) { - Log.e(TAG, "请求权限失败: ${e.message}", e) - } - - // 如果权限仍未授予,引导用户到权限管理页面 - try { - // 尝试跳转到应用内的权限管理页面 - val intent = Intent("com.yimai.eaimar.action.PERMISSIONS_SETTINGS").apply { - setPackage(context.packageName) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - - if (intent.resolveActivity(context.packageManager) != null) { - context.startActivity(intent) - } else { - // 如果应用内权限页面不可用,则跳转到系统应用设置页面 - val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", context.packageName, null) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - context.startActivity(settingsIntent) - } - } catch (e: Exception) { - Log.e(TAG, "打开权限设置页面失败: ${e.message}", e) - } - } - // 应用在后台或权限请求失败,只返回文字提示 - return false - } - return true - } - - /** - * 检查应用是否在前台运行 - * @return 应用是否在前台 - */ - private fun isAppInForeground(): Boolean { - try { - val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val appProcesses = activityManager.runningAppProcesses ?: return false - val packageName = context.packageName - - for (appProcess in appProcesses) { - if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND - && appProcess.processName == packageName) { - return true - } - } - } catch (e: Exception) { - Log.e(TAG, "检查应用状态失败: ${e.message}", e) - } - return false - } - - /** - * 设置提醒 - * @param title 提醒标题 - * @param content 提醒内容(可选) - * @param timeStr 提醒时间,格式为"yyyy-MM-dd HH:mm" - * @return 操作结果 - */ - fun setReminder(title: String, content: String, timeStr: String): Result { - Log.d(TAG, "设置提醒: $title, 时间: $timeStr") - - try { - // 解析时间字符串 - val time = try { - val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.getDefault()) - dateFormat.parse(timeStr)?.time ?: System.currentTimeMillis() - } catch (e: Exception) { - Log.e(TAG, "解析时间失败: $timeStr, ${e.message}") - return Result( - success = false, - message = "提醒时间格式无效,请使用'yyyy-MM-dd HH:mm'格式" - ) - } - - // 检查日历权限 - if (!checkPermissionAndGuide(Manifest.permission.WRITE_CALENDAR, "日历写入", "设置提醒")) { - return Result( - success = false, - message = "需要日历权限才能创建提醒,请在权限管理中开启日历权限" - ) - } - - // 直接使用ContentProvider写入日历 - val endTime = time + 3600000 // 默认1小时后结束 - - // 获取默认日历ID - val calendarId = getDefaultCalendarId() - if (calendarId == -1L) { - Log.w(TAG, "未找到默认日历") - return tryCreateAlarm(title, content, timeStr) - } - - // 创建事件 - val eventValues = ContentValues().apply { - put(CalendarContract.Events.CALENDAR_ID, calendarId) - put(CalendarContract.Events.TITLE, title) - put(CalendarContract.Events.DESCRIPTION, content) - put(CalendarContract.Events.DTSTART, time) - put(CalendarContract.Events.DTEND, endTime) - put(CalendarContract.Events.HAS_ALARM, 1) - put(CalendarContract.Events.EVENT_TIMEZONE, java.util.TimeZone.getDefault().id) - } - - val eventUri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, eventValues) - if (eventUri == null) { - Log.e(TAG, "插入日历事件失败") - return tryCreateAlarm(title, content, timeStr) - } - - // 获取事件ID - val eventId = ContentUris.parseId(eventUri) - - // 添加提醒(15分钟前提醒) - val reminderValues = ContentValues().apply { - put(CalendarContract.Reminders.EVENT_ID, eventId) - put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) - put(CalendarContract.Reminders.MINUTES, 15) // 提前15分钟提醒 - } - - val reminderUri = context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, reminderValues) - if (reminderUri != null) { - return Result( - success = true, - message = "已成功创建提醒: $title, 时间: $timeStr" - ) - } else { - // 仍然创建了事件,但没有提醒 - return Result( - success = true, - message = "已创建日历事件,但未设置提醒: $title" - ) - } - } catch (e: Exception) { - Log.e(TAG, "设置提醒失败: ${e.message}", e) - return Result( - success = false, - message = "设置提醒失败: ${e.message}" - ) - } - } - - /** - * 获取默认日历ID - */ - private fun getDefaultCalendarId(): Long { - val projection = arrayOf(CalendarContract.Calendars._ID) - var calendarId = -1L - - try { - val uri = CalendarContract.Calendars.CONTENT_URI - val selection = "(${CalendarContract.Calendars.VISIBLE} = 1) AND (${CalendarContract.Calendars.IS_PRIMARY} = 1)" - context.contentResolver.query(uri, projection, selection, null, null)?.use { cursor -> - if (cursor.moveToFirst()) { - calendarId = cursor.getLong(0) - } else { - // 如果没有找到主日历,尝试找任何可见的日历 - context.contentResolver.query(uri, projection, "${CalendarContract.Calendars.VISIBLE} = 1", null, null)?.use { c -> - if (c.moveToFirst()) { - calendarId = c.getLong(0) - } - } - } - } - } catch (e: Exception) { - Log.e(TAG, "获取默认日历ID失败: ${e.message}", e) - } - - return calendarId - } - - /** - * 尝试创建闹钟提醒(备选方案) - */ - private fun tryCreateAlarm(title: String, content: String, timeStr: String): Result { - try { - // 解析小时和分钟 - val timeParts = timeStr.split(" ")[1].split(":") - val hour = timeParts[0].toInt() - val minute = timeParts[1].toInt() - - val alarmIntent = Intent(AlarmClock.ACTION_SET_ALARM) - alarmIntent.putExtra(AlarmClock.EXTRA_MESSAGE, "$title: $content") - alarmIntent.putExtra(AlarmClock.EXTRA_HOUR, hour) - alarmIntent.putExtra(AlarmClock.EXTRA_MINUTES, minute) - alarmIntent.putExtra(AlarmClock.EXTRA_SKIP_UI, true) // 尝试跳过UI - alarmIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - - if (alarmIntent.resolveActivity(context.packageManager) != null) { - context.startActivity(alarmIntent) - return Result( - success = true, - message = "已创建闹钟提醒: $title, 时间: $timeStr" - ) - } - } catch (e: Exception) { - Log.e(TAG, "创建闹钟失败: ${e.message}", e) - } - - return Result( - success = false, - message = "未找到可用的日历或闹钟应用" - ) - } - - /** - * 获取当前时间 - * @param format 时间格式(可选) - * @return 操作结果 - */ - fun getCurrentTime(format: String? = null): Result { - try { - val calendar = java.util.Calendar.getInstance() - val date = calendar.time - val lang = java.util.Locale.getDefault().language - - // TTS友好的时间格式,避免AI将":"分隔的时间误读为数字 - val timeStr = when (format?.lowercase(java.util.Locale.getDefault())) { - "short" -> formatShortTime(date, lang) - "date" -> formatDate(date, lang) - "time" -> formatTime(date, lang) - "full" -> formatFull(date, lang) - "iso" -> java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", java.util.Locale.getDefault()).format(date) - else -> formatDefaultTime(date, lang) - } - - // 获取星期几(使用系统locale自动适配语言) - val weekDay = java.text.SimpleDateFormat("EEEE", java.util.Locale.getDefault()).format(date) - - // 根据格式决定是否添加星期几 - val resultTime = when { - format == "short" || format == "time" || format == "iso" -> timeStr - format == "full" -> timeStr // full格式已经包含了星期 - else -> "$timeStr $weekDay" - } - - Log.d(TAG, "获取当前时间成功: $resultTime") - return Result( - success = true, - message = resultTime - ) - } catch (e: Exception) { - Log.e(TAG, "获取当前时间失败: ${e.message}", e) - return Result( - success = false, - message = "获取当前时间失败: ${e.message}" - ) - } - } - - // TTS友好的时间格式辅助方法 - // CJK 使用单位数 m/s(去掉前导零),其他语言使用英文单词避免冒号 - private fun formatShortTime(date: java.util.Date, lang: String): String = when (lang) { - "zh" -> java.text.SimpleDateFormat("H时m分", java.util.Locale.CHINESE).format(date) - "ja" -> java.text.SimpleDateFormat("H時m分", java.util.Locale.JAPANESE).format(date) - "ko" -> java.text.SimpleDateFormat("H시 m분", java.util.Locale.KOREAN).format(date) - else -> formatTimeWestern(date, withSeconds = false) - } - - private fun formatDate(date: java.util.Date, lang: String): String = when (lang) { - "zh" -> java.text.SimpleDateFormat("yyyy年M月d日", java.util.Locale.CHINESE).format(date) - "ja" -> java.text.SimpleDateFormat("yyyy年M月d日", java.util.Locale.JAPANESE).format(date) - "ko" -> java.text.SimpleDateFormat("yyyy년 M월 d일", java.util.Locale.KOREAN).format(date) - else -> java.text.SimpleDateFormat("MMMM d, yyyy", java.util.Locale.getDefault()).format(date) - } - - private fun formatTime(date: java.util.Date, lang: String): String = when (lang) { - "zh" -> java.text.SimpleDateFormat("H时m分s秒", java.util.Locale.CHINESE).format(date) - "ja" -> java.text.SimpleDateFormat("H時m分s秒", java.util.Locale.JAPANESE).format(date) - "ko" -> java.text.SimpleDateFormat("H시 m분 s초", java.util.Locale.KOREAN).format(date) - else -> formatTimeWestern(date, withSeconds = true) - } - - private fun formatFull(date: java.util.Date, lang: String): String = when (lang) { - "zh" -> java.text.SimpleDateFormat("yyyy年M月d日 EEEE H时m分s秒", java.util.Locale.CHINESE).format(date) - "ja" -> java.text.SimpleDateFormat("yyyy年M月d日 EEEE H時m分s秒", java.util.Locale.JAPANESE).format(date) - "ko" -> java.text.SimpleDateFormat("yyyy년 M월 d일 EEEE H시 m분 s초", java.util.Locale.KOREAN).format(date) - else -> "${java.text.SimpleDateFormat("MMMM d, yyyy, EEEE, ", java.util.Locale.getDefault()).format(date)}${formatTimeWestern(date, withSeconds = true)}" - } - - private fun formatDefaultTime(date: java.util.Date, lang: String): String = when (lang) { - "zh" -> java.text.SimpleDateFormat("yyyy年M月d日 H时m分", java.util.Locale.CHINESE).format(date) - "ja" -> java.text.SimpleDateFormat("yyyy年M月d日 H時m分", java.util.Locale.JAPANESE).format(date) - "ko" -> java.text.SimpleDateFormat("yyyy년 M월 d일 H시 m분", java.util.Locale.KOREAN).format(date) - else -> "${java.text.SimpleDateFormat("MMMM d, yyyy, ", java.util.Locale.getDefault()).format(date)}${formatTimeWestern(date, withSeconds = false)}" - } - - /** - * 非 CJK 语言的时间格式:避免冒号,使用英文单词,对 TTS 更稳定 - * 例:2 hours 30 minutes 15 seconds PM - */ - private fun formatTimeWestern(date: java.util.Date, withSeconds: Boolean): String { - val cal = java.util.Calendar.getInstance().apply { time = date } - val h12 = cal.get(java.util.Calendar.HOUR).let { if (it == 0) 12 else it } - val minute = cal.get(java.util.Calendar.MINUTE) - val second = cal.get(java.util.Calendar.SECOND) - val ampm = if (cal.get(java.util.Calendar.AM_PM) == java.util.Calendar.AM) "AM" else "PM" - - val sb = StringBuilder() - sb.append(h12).append(if (h12 == 1) " hour" else " hours") - if (minute > 0 || withSeconds) { - sb.append(' ').append(minute).append(if (minute == 1) " minute" else " minutes") - } - if (withSeconds) { - sb.append(' ').append(second).append(if (second == 1) " second" else " seconds") - } - sb.append(' ').append(ampm) - return sb.toString() - } - - /** - * 获取当前位置(简化版,只获取最后已知位置) - * @return 操作结果 - */ - suspend fun getCurrentLocation(): Result = withContext(Dispatchers.Main) { - Log.d(TAG, "开始获取当前位置") - - try { - val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - - // 检查位置权限 - if (!checkPermissionAndGuide(Manifest.permission.ACCESS_FINE_LOCATION, "位置信息", "获取位置")) { - return@withContext Result( - success = false, - message = "需要位置权限才能获取当前位置,请在权限管理中开启位置权限" - ) - } - - // 检查位置服务是否开启 - if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) && - !locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { - - Log.d(TAG, "位置服务未启用") - // 引导用户打开位置服务 - try { - val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - context.startActivity(intent) - } catch (e: Exception) { - Log.e(TAG, "打开位置设置失败: ${e.message}", e) - } - - return@withContext Result( - success = false, - message = "位置服务未启用,请打开GPS或网络定位" - ) - } - - // 获取最后已知位置 - var bestLocation: Location? = null - - // 尝试获取网络位置 - if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { - val networkLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) - if (networkLocation != null) { - bestLocation = networkLocation - Log.d(TAG, "获取到网络位置: ${networkLocation.latitude}, ${networkLocation.longitude}") - } - } - - // 尝试获取GPS位置(如果更精确) - if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { - val gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) - if (gpsLocation != null && (bestLocation == null || gpsLocation.accuracy < bestLocation.accuracy)) { - bestLocation = gpsLocation - Log.d(TAG, "获取到GPS位置: ${gpsLocation.latitude}, ${gpsLocation.longitude}") - } - } - - // 返回结果 - return@withContext if (bestLocation != null) { - val message = "当前位置: 经度 ${bestLocation.longitude}, 纬度 ${bestLocation.latitude}" - Log.d(TAG, message) - Result(success = true, message = message) - } else { - Log.d(TAG, "无法获取位置信息") - Result(success = false, message = "无法获取位置信息,请稍后重试") - } - - } catch (e: Exception) { - Log.e(TAG, "获取位置过程中发生异常: ${e.message}") - return@withContext Result( - success = false, - message = "获取位置失败: ${e.message}" - ) - } - } - - /** - * 打开系统录音机并尝试自动开始录音 - * @return 操作结果 - */ - fun openRecorder(): Result { - Log.d(TAG, "打开系统录音机并尝试自动开始录音") - - try { - // 创建用于启动录音机应用的Intent - val recordIntent = Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION) - recordIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - - // 添加额外参数尝试自动开始录音(不是所有应用都支持) - recordIntent.putExtra("android.intent.extra.quickCapture", true) - - // 检查是否有应用能处理此Intent - if (recordIntent.resolveActivity(context.packageManager) != null) { - context.startActivity(recordIntent) - return Result( - success = true, - message = "已打开录音机,尝试自动开始录音" - ) - } else { - // 尝试使用备用方法,针对不同厂商应用添加特定参数 - val commonRecorderIntents = mapOf( - "com.android.soundrecorder" to Intent().setClassName("com.android.soundrecorder", "com.android.soundrecorder.SoundRecorder").setAction("android.intent.action.RECORD_SOUND"), - "com.sec.android.app.voicenote" to Intent().setClassName("com.sec.android.app.voicenote", "com.sec.android.app.voicenote.Main").putExtra("launch_mode", "record"), - "com.miui.soundrecorder" to Intent().setClassName("com.miui.soundrecorder", "com.miui.soundrecorder.SoundRecorderActivity").putExtra("startOnBoot", true), - "com.huawei.recorder" to Intent().setClassName("com.huawei.recorder", "com.huawei.recorder.activity.MainActivity").putExtra("autoRecord", true), - "com.google.android.apps.recorder" to Intent().setClassName("com.google.android.apps.recorder", "com.google.android.apps.recorder.ui.RecorderActivity").putExtra("start_recording", true) - ) - - for ((packageName, intent) in commonRecorderIntents) { - try { - if (isPackageInstalled(packageName, context.packageManager)) { - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - context.startActivity(intent) - return Result( - success = true, - message = "已打开录音应用并尝试自动开始录音" - ) - } - } catch (e: Exception) { - Log.e(TAG, "尝试打开录音应用 $packageName 失败: ${e.message}") - // 继续尝试下一个应用 - } - } - - // 如果特定Intent都失败,回退到原来的方法 - val commonRecorderPackages = listOf( - "com.android.soundrecorder", - "com.sec.android.app.voicenote", - "com.miui.soundrecorder", - "com.huawei.recorder", - "com.google.android.apps.recorder" - ) - - for (packageName in commonRecorderPackages) { - try { - val packageIntent = context.packageManager.getLaunchIntentForPackage(packageName) - if (packageIntent != null) { - packageIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - // 尝试添加通用的自动开始录音参数 - packageIntent.putExtra("android.intent.extra.quickCapture", true) - context.startActivity(packageIntent) - return Result( - success = true, - message = "已打开录音应用,请手动开始录音" - ) - } - } catch (e: Exception) { - Log.e(TAG, "尝试打开录音应用 $packageName 失败: ${e.message}") - } - } - - return Result( - success = false, - message = "未找到录音应用" - ) - } - } catch (e: Exception) { - Log.e(TAG, "打开录音机失败: ${e.message}", e) - return Result( - success = false, - message = "打开录音机失败: ${e.message}" - ) - } - } - - // 检查应用是否已安装 - private fun isPackageInstalled(packageName: String, packageManager: PackageManager): Boolean { - return try { - packageManager.getPackageInfo(packageName, 0) - true - } catch (e: Exception) { - false - } - } - - - /** - * 停止当前导航 - * @return 操作结果 - */ - fun stopNavigation():Result{ - Log.d(TAG, "结束当前导航服务") - try { -// val intent = Intent() -// intent.component = ComponentName("com.example.plugin", "com.example.plugin.MyBackgroundService") -// stopService(intent) - return Result( - success = true, - message = "当前导航服务已结束" - ) - } catch (e: Exception) { - Log.e(TAG, "结束当前导航服务失败: ${e.message}", e) - return Result( - success = false, - message = "结束当前导航服务失败: ${e.message}" - ) - } - } - - - //设置音量 - fun setVolume(context: Context, streamType: Int, volume: Int) { - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - val maxVolume = audioManager.getStreamMaxVolume(streamType) - val safeVolume = volume.coerceIn(0, maxVolume) - audioManager.setStreamVolume(streamType, safeVolume, AudioManager.FLAG_SHOW_UI) - } - fun getCurrentVolume(context: Context, streamType: Int): Int { - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - return audioManager.getStreamVolume(streamType) - } - fun getMaxVolume(context: Context, streamType: Int): Int { - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - return audioManager.getStreamMaxVolume(streamType) - } - //增加音量/减少音量 - fun adjustVolume(context: Context, direction: Int) { - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, direction, AudioManager.FLAG_SHOW_UI) - } - - /** - * 操作结果数据类 - */ - data class Result( - val success: Boolean, - val message: String - ) -} diff --git a/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt b/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt deleted file mode 100644 index b198298a..00000000 --- a/apps/client/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt +++ /dev/null @@ -1,367 +0,0 @@ -package com.yunqiinnovation.open_ai_service.mcp - -import android.os.Parcel -import android.os.Parcelable -import android.util.Log -import io.ktor.client.* -import io.ktor.client.plugins.sse.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage -import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest -import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport -import kotlinx.coroutines.* -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.decodeFromString -import kotlin.properties.Delegates -import kotlin.time.Duration -import java.util.concurrent.atomic.AtomicBoolean -import org.json.JSONObject - - -/** - * 自定义SSE客户端传输层,修复原始SseClientTransport中的URL拼接问题 - * 解决URL查询参数与路径拼接错误的问题,确保消息端点URL格式正确 - */ -class CustomSseClientTransport( - private val client: HttpClient, - private val urlString: String?, - private val reconnectionTime: Duration? = null, - private val requestBuilder: HttpRequestBuilder.() -> Unit = {}, -) : AbstractTransport() { - private val TAG = "CustomSseClientTransport" - - private val scope by lazy { - CoroutineScope(session.coroutineContext + SupervisorJob()) - } - - // 使用Java标准库的AtomicBoolean替代kotlinx.atomicfu - private val initialized = AtomicBoolean(false) - private var session: ClientSSESession by Delegates.notNull() - private val endpoint = CompletableDeferred() - - private var job: Job? = null - - // 创建JSON解析器,增强灵活性设置 - private val json = Json { - ignoreUnknownKeys = true // 忽略未知字段 - isLenient = true // 宽松解析模式 - coerceInputValues = true // 尝试强制转换类型 - encodeDefaults = true // 编码默认值 - explicitNulls = false // 不要求显式null值 - } - - // 保存基础URL(不包含查询参数)和查询参数 - private var baseUrlWithoutParams: String? = null - private var queryParams: Map = emptyMap() - private var hostPart: String = "" // 添加类级别变量 - private var pathPart: String = "" // 添加类级别变量 - - /** - * 解析URL,分离基础URL、路径和查询参数 - * 返回三元组: (主机部分URL, 路径部分, 查询参数Map) - */ - private fun parseUrl(url: String): Triple> { - return try { - val params = mutableMapOf() - - // 确保URL有协议部分 - var processedUrl = url.trim() - if (!processedUrl.startsWith("http://") && !processedUrl.startsWith("https://")) { - processedUrl = "https://$processedUrl" - Log.d(TAG, "添加默认协议: $processedUrl") - } - - val urlObj = java.net.URL(processedUrl) - - // 解析查询参数 - if (urlObj.query != null) { - urlObj.query.split("&").forEach { param -> - val parts = param.split("=", limit = 2) - if (parts.size == 2) { - params[parts[0]] = parts[1] - } - } - } - - // 构建主机部分URL(协议+主机+端口) - val port = if (urlObj.port == -1) "" else ":${urlObj.port}" - val hostUrl = "${urlObj.protocol}://${urlObj.host}$port" - - // 路径部分 - val path = urlObj.path - - Triple(hostUrl, path, params) - } catch (e: Exception) { - Log.e(TAG, "解析URL失败: $url, ${e.message}") - Triple(url, "", emptyMap()) - } - } - - /** - * 收集SSE事件 - */ - private suspend fun collectEvents() { - job = scope.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) { - session.incoming.collect { event -> - when (event.event) { - "error" -> { - val e = IllegalStateException("SSE error: ${event.data}") - Log.e(TAG, "SSE错误: ${event.data}") - _onError(e) - throw e - } - - "open" -> { - Log.d(TAG, "SSE连接已打开") - // 连接已打开,等待endpoint事件 - } - - "endpoint" -> { - try { - val eventData = event.data ?: "" - Log.d(TAG, "收到endpoint事件: $eventData") - - // 检查eventData是否已包含hostPart - val fullEndpoint = if (eventData.contains(hostPart)) { - // 如果已包含hostPart,则直接使用eventData - Log.d(TAG, "eventData已包含hostPart,直接使用") - eventData - } else if (eventData.startsWith("/")) { - // 如果以/开头但不包含hostPart,则拼接 - "$hostPart$eventData" - } else { - "$eventData" - } - - Log.d(TAG, "构建的endpoint路径(不含参数): $fullEndpoint") - - // 添加查询参数到endpoint - val endpointWithParams = if (queryParams.isNotEmpty()) { - // 检查endpoint是否已有查询参数 - if (fullEndpoint.contains("?")) { - // 已有查询参数,添加&并附加其他参数 - val queryString = queryParams.entries.joinToString("&") { "${it.key}=${it.value}" } - "$fullEndpoint&$queryString" - } else { - // 没有查询参数,添加?并附加参数 - val queryString = queryParams.entries.joinToString("&") { "${it.key}=${it.value}" } - "$fullEndpoint?$queryString" - } - } else { - fullEndpoint - } - - Log.d(TAG, "最终消息端点: $endpointWithParams") - endpoint.complete(endpointWithParams) - } catch (e: Exception) { - Log.e(TAG, "处理endpoint事件失败: ${e.message}", e) - _onError(e) - close() - error(e) - } - } - - else -> { - try { - // 解析JSON-RPC消息 - val data = event.data - if (data != null) { - // Log.d(TAG, "收到事件数据: $data") - try { - // 尝试安全地解析JSON消息 - safeParseMessage(data) - } catch (e: Exception) { - Log.e(TAG, "解析JSON-RPC消息失败: ${e.message}", e) - // 错误已记录,但不中断连接,只发送错误通知 - _onError(e) - } - } - } catch (e: Exception) { - Log.e(TAG, "处理事件失败: ${e.message}", e) - _onError(e) - } - } - } - } - } - } - - /** - * 安全解析JSON-RPC消息 - */ - private suspend fun safeParseMessage(data: String) { - try { - // 先尝试使用标准解析 - val message = json.decodeFromString(data) - _onMessage(message) - } catch (e: Exception) { - // 如果标准解析失败,记录错误并尝试使用备用解析方式 - Log.w(TAG, "标准解析失败,尝试备用解析: ${e.message}") - - try { - // 尝试修复nextCursor缺失问题 - if (e.message?.contains("nextCursor") == true) { - // 尝试手动添加缺失的nextCursor字段 - val jsonObj = JSONObject(data) - - // 只有在解析ListToolsResult时处理 - if (data.contains("\"tools\"")) { - Log.d(TAG, "尝试修复ListToolsResult缺少nextCursor字段的问题") - - // 手动解析result部分并添加nextCursor - val resultJson = try { - if (jsonObj.has("result")) { - val resultObj = jsonObj.getJSONObject("result") - if (!resultObj.has("nextCursor")) { - resultObj.put("nextCursor", "") - jsonObj.put("result", resultObj) - } - jsonObj.toString() - } else { - // 如果没有result字段,可能是其他类型的消息 - data - } - } catch (ex: Exception) { - Log.e(TAG, "手动修复JSON失败: ${ex.message}") - data - } - - // 重新尝试解析修复后的JSON - val fixedMessage = json.decodeFromString(resultJson) - _onMessage(fixedMessage) - return - } - } - - // 通用错误处理 - Log.e(TAG, "无法解析消息,跳过: $data") - } catch (ex: Exception) { - Log.e(TAG, "备用解析也失败: ${ex.message}", ex) - // 不抛出异常,只记录错误 - _onError(e) - } - } - } - - /** - * 启动传输层 - */ - override suspend fun start() { - if (!initialized.compareAndSet(false, true)) { - Log.e(TAG, "传输层已经启动,不能重复启动") - error("CustomSseClientTransport already started!") - } - - // 解析URL和参数 - if (urlString != null) { - // 解析URL,提取主机部分、路径部分和查询参数 - val urlInfo = parseUrl(urlString) - hostPart = urlInfo.first - pathPart = urlInfo.second - queryParams = urlInfo.third - - // 存储不带查询参数的基础URL(主机+路径) - baseUrlWithoutParams = hostPart + pathPart - - Log.d(TAG, "原始URL: $urlString") - Log.d(TAG, "主机部分: $hostPart") - Log.d(TAG, "路径部分: $pathPart") - Log.d(TAG, "查询参数: $queryParams") - Log.d(TAG, "完整基础URL: $baseUrlWithoutParams") - } - - // 创建SSE会话 - 直接使用原始URL,不添加/sse后缀 - session = urlString?.let { - // 完整的SSE连接URL(主机部分+原始路径+查询参数) - val sseConnectUrl = if (queryParams.isNotEmpty()) { - // 如果路径已包含查询参数,就不再添加 - if (pathPart.contains("?")) { - "$hostPart$pathPart" - } else { - val queryString = queryParams.entries.joinToString("&") { "${it.key}=${it.value}" } - "$hostPart$pathPart?$queryString" - } - } else { - "$hostPart$pathPart" - } - - Log.d(TAG, "SSE连接URL: $sseConnectUrl") - - client.sseSession( - urlString = sseConnectUrl, - reconnectionTime = reconnectionTime, - block = requestBuilder, - ) - } ?: client.sseSession( - reconnectionTime = reconnectionTime, - block = requestBuilder, - ) - - // 收集SSE事件 - collectEvents() - - // 等待endpoint就绪 - endpoint.await() - Log.d(TAG, "传输层启动完成,消息端点已就绪") - } - - /** - * 发送消息 - */ - @OptIn(ExperimentalCoroutinesApi::class) - override suspend fun send(message: JSONRPCMessage) { - if (!endpoint.isCompleted) { - Log.e(TAG, "发送失败: 未连接") - error("Not connected") - } - - try { - val messageEndpoint = endpoint.getCompleted() - Log.d(TAG, "发送消息到: $messageEndpoint") - - // 序列化消息 - val jsonString = json.encodeToString(message) - - val response = client.post(messageEndpoint) { - headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString()) - setBody(jsonString) - } - - if (!response.status.isSuccess()) { - val text = response.bodyAsText() - Log.e(TAG, "发送消息失败: HTTP ${response.status}, $text") - error("Error POSTing to endpoint (HTTP ${response.status}): $text") - } - } catch (e: Exception) { - Log.e(TAG, "发送消息异常: ${e.message}", e) - _onError(e) - throw e - } - } - - /** - * 关闭传输层 - */ - override suspend fun close() { - if (!initialized.get()) { - Log.e(TAG, "关闭失败: 传输层未初始化") - error("CustomSseClientTransport is not initialized!") - } - session.cancel() - _onClose() - job?.cancelAndJoin() - Log.d(TAG, "传输层已关闭") - } - - /** - * 检查传输层是否已初始化 - */ - fun isInitialized(): Boolean { - return initialized.get() - } -} - - \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/ios/Classes/MCPClient.swift b/apps/client/local_plugins/open_ai_service/ios/Classes/MCPClient.swift deleted file mode 100644 index 121653f0..00000000 --- a/apps/client/local_plugins/open_ai_service/ios/Classes/MCPClient.swift +++ /dev/null @@ -1,366 +0,0 @@ -import Foundation - -/// 工具类型枚举 -public enum ToolType { - case localFunction // 本地函数 - case mcpTool // MCP工具 -} - -/// 函数处理器协议 -public protocol FunctionHandler { - /// 处理函数调用 - /// - Parameter arguments: 函数参数,字典格式 - /// - Returns: 函数执行结果,字符串 - func handle(arguments: [String: Any]) async throws -> String -} - -/// 单个MCP子客户端 -/// 负责连接到一个具体的MCP服务器并管理其工具 -public class MCPSubClient { - private let tag: String - private let serverId: String - private let serverUrl: String - - // 服务器提供的工具列表 - private var tools: [[String: Any]] = [] - - // 工具Map列表,用于OpenAI请求 - private var toolMaps: [[String: Any]] = [] - - // 是否已连接 - private var isConnectedFlag = false - - // 工具名称到工具的映射,用于快速查找 - private var toolMapping: [String: [String: Any]] = [:] - - // URL会话 - private lazy var urlSession: URLSession = { - let config = URLSessionConfiguration.default - config.timeoutIntervalForRequest = 30 - config.timeoutIntervalForResource = 30 - return URLSession(configuration: config) - }() - - public init(serverId: String, serverUrl: String) { - self.serverId = serverId - self.serverUrl = serverUrl - self.tag = "MCPSubClient[\(serverId)]" - print("\(tag) 初始化,URL: \(serverUrl)") - } - - /// 连接到MCP服务器(通过SSE) - /// - Returns: 是否连接成功 - public func connect() async -> Bool { - do { - print("\(tag) 开始连接服务器: \(serverUrl)") - - // 这里应该实现实际的SSE连接逻辑 - // 由于iOS没有直接的SSE库,我们可以使用URLSession来模拟 - // 或者使用第三方库如EventSource - - // 模拟连接成功并获取工具列表 - await processToolsList() - - isConnectedFlag = true - print("\(tag) 连接成功,可用工具: \(tools.count)个") - return true - } catch { - print("\(tag) 连接失败: \(error.localizedDescription)") - return false - } - } - - /// 处理工具列表 - private func processToolsList() async { - // 这里应该从MCP服务器获取实际的工具列表 - // 目前先模拟一些基本工具 - tools = [] - - // 更新工具映射 - toolMapping.removeAll() - for tool in tools { - if let name = tool["name"] as? String { - toolMapping[name] = tool - } - } - - // 更新工具Map列表 - updateToolMaps() - - print("\(tag) 成功处理 \(tools.count) 个工具") - } - - /// 更新工具Map列表,用于OpenAI请求 - /// 将MCP工具转换为OpenAI函数调用格式 - private func updateToolMaps() { - toolMaps.removeAll() - - for tool in tools { - guard let name = tool["name"] as? String, - let description = tool["description"] as? String else { - continue - } - - let functionMap: [String: Any] = [ - "name": name, - "description": description, - "parameters": [ - "type": "object", - "properties": [:], - "required": [] - ] - ] - - toolMaps.append([ - "type": "function", - "function": functionMap - ]) - } - - print("\(tag) 更新工具映射完成,共 \(toolMaps.count) 个工具") - } - - /// 获取工具Map列表,用于OpenAI请求 - public func getToolMaps() -> [[String: Any]] { - return toolMaps - } - - /// 获取可用的工具列表 - public func getTools() -> [[String: Any]] { - return tools - } - - /// 检查是否包含指定工具 - public func containsTool(name: String) -> Bool { - return toolMapping.keys.contains(name) - } - - /// 调用工具 - public func callTool(name: String, arguments: [String: Any]) async throws -> [String: Any] { - guard isConnectedFlag else { - print("\(tag) 未连接到服务器") - return ["context": "未连接到MCP服务器"] - } - - guard containsTool(name: name) else { - print("\(tag) 此客户端不包含工具: \(name)") - throw OpenAIException("工具不存在: \(name)") - } - - // 这里应该实现实际的工具调用逻辑 - // 目前先返回模拟结果 - return [ - "name": name, - "context": "工具调用结果: \(name)" - ] - } - - /// 检查是否已连接 - public func isConnected() -> Bool { - return isConnectedFlag - } - - /// 关闭连接 - public func close() { - isConnectedFlag = false - print("\(tag) 已关闭MCP服务器连接") - } -} - -/// MCP客户端,用于连接Model Context Protocol服务器和管理本地函数 -public class MCPClient { - private let tag = "MCPClient" - - // 本地函数Map,函数名 -> 处理器 - private var localFunctions: [String: FunctionHandler] = [:] - - // 本地函数定义Map,函数名 -> 定义 - private var localFunctionDefs: [String: String] = [:] - - // 子客户端列表,每个连接一个MCP服务器 - private var subClients: [String: MCPSubClient] = [:] - - // 是否已连接 - private var isConnectedFlag = false - - // 系统功能处理器 - private var systemFunctionHandler: SystemFunctionHandler? - - public init() { - // 初始化系统功能处理器 - systemFunctionHandler = SystemFunctionHandler() - initializeVoiceFunctions() - } - - /// 初始化语音功能处理器 - private func initializeVoiceFunctions() { - do { - // 使用SystemFunctionHandler注册所有功能 - systemFunctionHandler?.registerAllFunctions(client: self) - print("\(tag) 语音功能初始化完成") - } catch { - print("\(tag) 语音功能初始化失败: \(error.localizedDescription)") - } - } - - /// 连接到MCP服务器(通过SSE) - /// 直接接收完整的JSON配置字符串 - /// - Parameter mcpConfigJson: 包含mcpServers字段的JSON配置字符串 - /// - Returns: 是否连接成功 - public func connectToSSE(mcpConfigJson: String) async throws -> Bool { - // 清除现有连接 - closeAllConnections() - - var serverMap: [String: String] = [:] - var configLoaded = false - - // 尝试解析传入的JSON配置 - if !mcpConfigJson.isEmpty { - do { - guard let data = mcpConfigJson.data(using: .utf8), - let config = try JSONSerialization.jsonObject(with: data) as? [String: Any], - let mcpServers = config["mcpServers"] as? [String: [String: String]] else { - throw OpenAIException("无效的MCP配置格式") - } - - // 从服务器配置中提取URL - for (serverId, serverConfig) in mcpServers { - if let url = serverConfig["url"], !url.isEmpty { - serverMap[serverId] = url - print("\(tag) 从配置中添加服务器 [\(serverId)]: \(url)") - } - } - - configLoaded = !serverMap.isEmpty - } catch { - print("\(tag) 解析MCP配置失败: \(error.localizedDescription)") - } - } - - // 如果没有从配置中加载到服务器,使用默认配置 - if !configLoaded { - print("\(tag) 使用默认MCP服务器配置") - // 可以在这里添加默认的服务器配置 - } - - // 连接到所有服务器 - var successCount = 0 - for (serverId, serverUrl) in serverMap { - let subClient = MCPSubClient(serverId: serverId, serverUrl: serverUrl) - let connected = await subClient.connect() - - if connected { - subClients[serverId] = subClient - successCount += 1 - print("\(tag) 成功连接到服务器 [\(serverId)]") - } else { - print("\(tag) 连接服务器失败 [\(serverId)]") - } - } - - isConnectedFlag = successCount > 0 - print("\(tag) MCP客户端连接完成,成功连接 \(successCount)/\(serverMap.count) 个服务器") - - return isConnectedFlag - } - - /// 关闭所有连接 - private func closeAllConnections() { - for (_, subClient) in subClients { - subClient.close() - } - subClients.removeAll() - isConnectedFlag = false - } - - /// 获取工具Map列表,用于OpenAI请求 - public func getToolMaps() -> [[String: Any]] { - var allToolMaps: [[String: Any]] = [] - - // 添加所有子客户端的工具 - for (_, subClient) in subClients { - allToolMaps.append(contentsOf: subClient.getToolMaps()) - } - - // 添加本地函数 - for (name, _) in localFunctions { - if let def = localFunctionDefs[name], - let data = def.data(using: .utf8), - let functionDef = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { - allToolMaps.append([ - "type": "function", - "function": functionDef - ]) - } - } - - return allToolMaps - } - - /// 注册本地函数 - public func registerLocalFunction(name: String, description: String, parameters: [String: Any], handler: FunctionHandler) -> Bool { - do { - localFunctions[name] = handler - - let functionDef: [String: Any] = [ - "name": name, - "description": description, - "parameters": parameters - ] - - let jsonData = try JSONSerialization.data(withJSONObject: functionDef) - let jsonString = String(data: jsonData, encoding: .utf8) ?? "" - localFunctionDefs[name] = jsonString - - // print("\(tag) 成功注册本地函数: \(name)") - return true - } catch { - print("\(tag) 注册本地函数失败: \(error.localizedDescription)") - return false - } - } - - /// 调用工具 - public func callTool(name: String, arguments: [String: Any]) async throws -> [String: Any] { - // 首先检查是否是本地函数 - if let handler = localFunctions[name] { - do { - let result = try await handler.handle(arguments: arguments) - return ["context": result] - } catch { - throw OpenAIException("本地函数调用失败: \(error.localizedDescription)") - } - } - - // 然后检查MCP工具 - for (_, subClient) in subClients { - if subClient.containsTool(name: name) { - return try await subClient.callTool(name: name, arguments: arguments) - } - } - - throw OpenAIException("未找到工具: \(name)") - } - - /// 解析JSON参数 - public func parseJsonArguments(_ argumentsJson: String) throws -> [String: Any] { - guard let data = argumentsJson.data(using: .utf8), - let arguments = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - return [:] - } - return arguments - } - - /// 检查是否已连接 - public func isConnected() -> Bool { - return isConnectedFlag || !localFunctions.isEmpty - } - - /// 关闭连接 - public func close() { - closeAllConnections() - localFunctions.removeAll() - localFunctionDefs.removeAll() - print("\(tag) 已关闭MCP客户端") - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/ios/Classes/OpenAIService.swift b/apps/client/local_plugins/open_ai_service/ios/Classes/OpenAIService.swift deleted file mode 100644 index 35e40dc3..00000000 --- a/apps/client/local_plugins/open_ai_service/ios/Classes/OpenAIService.swift +++ /dev/null @@ -1,869 +0,0 @@ -import Foundation -import UIKit -import os -import os.log - -/// OpenAI服务异常 -public struct OpenAIException: Error { - let message: String - - public init(_ message: String) { - self.message = message - } - - public var localizedDescription: String { - return "OpenAIException: \(message)" - } -} - -/// 流式回调协议 -public protocol StreamCallback { - func onToken(_ token: String) - func onComplete() - func onError(_ error: Error) - func onFunctionCall(_ functionCall: [String: Any]) - func onFunctionCallResult(_ functionCall: [String: Any], _ functionCallResult: [String: Any]) -} - -/// 工具调用信息 -private class ToolCallInfo { - var id: String = "" - var name: String = "" - var arguments: String = "" - - func isValid() -> Bool { - return !id.isEmpty && !name.isEmpty - } -} - -/// OpenAI服务的原生实现 -public class OpenAIService: NSObject { - private let tag = "OpenAIService" - - // 日志对象 - private let logger = OSLog(subsystem: "com.yunqiinnovation.open_ai_service", category: "OpenAIService") - - // MARK: - 属性 - private var baseUrl = "" - private var apiKey = "" - private var model = "" - private var visionModel = "doubao-1-5-vision-pro-32k-250115" - private var isInitialized = false - - // MCP客户端 - private var mcpClient: MCPClient? - private var isMcpInitializedFlag = false - - // 流式请求相关 - private var currentStreamTask: URLSessionDataTask? - private var currentCallback: StreamCallback? - private var currentMessages: [[String: Any]] = [] - private var responseBuffer = Data() - private var toolCalls: [Int: ToolCallInfo] = [:] - private var isCanceled = false - - // 优化:添加行缓冲区,避免频繁字符串转换 - private var lineBuffer = Data() - private let newlineData = "\n".data(using: .utf8)! - - // 优化:添加重连机制 - private var retryCount = 0 - private let maxRetryCount = 3 - private var retryDelay: TimeInterval = 1.0 - - // 优化:添加性能监控 - private var streamStartTime: Date? - private var tokenCount = 0 - private var bytesReceived = 0 - - // URL会话 - private lazy var urlSession: URLSession = { - let config = URLSessionConfiguration.default - config.timeoutIntervalForRequest = 30 - config.timeoutIntervalForResource = 30 - return URLSession(configuration: config) - }() - - // 流式会话(使用 delegate) - private lazy var streamSession: URLSession = { - let config = URLSessionConfiguration.default - config.timeoutIntervalForRequest = 30 - config.timeoutIntervalForResource = 30 - - // 优化:减少网络缓存,避免内存压力 - config.urlCache = nil - config.requestCachePolicy = .reloadIgnoringLocalCacheData - - // 优化:限制并发连接数 - config.httpMaximumConnectionsPerHost = 2 - - // 优化:针对后台数据处理的线程配置 - let delegateQueue = OperationQueue() - // 使用utility QoS,平衡性能和功耗,不与UI线程竞争 - delegateQueue.qualityOfService = .utility - delegateQueue.maxConcurrentOperationCount = 1 - delegateQueue.name = "OpenAIService.StreamDelegate" - - return URLSession(configuration: config, delegate: self, delegateQueue: delegateQueue) - }() - - // MARK: - 初始化 - public override init() { - super.init() - } - - // MARK: - 公共方法 - - /// 初始化OpenAI服务 - public func initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String) -> Bool { - self.apiKey = apiKey - if !baseUrl.isEmpty { - self.baseUrl = baseUrl - } - if !model.isEmpty { - self.model = model - } - - // 初始化MCP客户端 - if mcpClient == nil { - mcpClient = MCPClient() - } - _ = initializeMcpClient(serverUrl: mcpServer) - - isInitialized = !apiKey.isEmpty - return isInitialized - } - - /// 创建用户消息 - public func createUserMessage(content: String) -> [String: Any] { - return [ - "role": "user", - "content": content - ] - } - - /// 创建助手消息 - public func createAssistantMessage(content: String) -> [String: Any] { - return [ - "role": "assistant", - "content": content - ] - } - - /// 创建系统消息 - public func createSystemMessage(content: String) -> [String: Any] { - return [ - "role": "system", - "content": content - ] - } - - /// 创建带图片的用户消息 - public func createUserMessageWithImage(text: String, imageBase64: String) -> [String: Any] { - var contentArray: [[String: Any]] = [] - - // 添加文本部分 - if !text.isEmpty { - contentArray.append([ - "type": "text", - "text": text - ]) - } - - // 添加图片部分 - contentArray.append([ - "type": "image_url", - "image_url": [ - "url": "data:image/jpeg;base64,\(imageBase64)" - ] - ]) - - return [ - "role": "user", - "content": contentArray - ] - } - - /// 发送消息(非流式输出) - public func sendMessage(messages: [[String: Any]]) throws -> String { - guard isInitialized && !apiKey.isEmpty else { - throw OpenAIException("OpenAI服务未初始化") - } - - // 检查是否包含图片,决定使用哪个模型 - var currentModel = model - if let lastMessage = messages.last, - let content = lastMessage["content"] as? String, - content.contains("image_url") { - currentModel = visionModel - } - - // 构建请求体 - var requestBody: [String: Any] = [ - "model": currentModel, - "messages": messages, - "temperature": 0.7, - "max_tokens": 2000, - "stream": false - ] - - // 添加工具列表 - if let tools = mcpClient?.getToolMaps(), !tools.isEmpty { - requestBody["tools"] = tools - } - - // 创建请求 - guard let url = URL(string: baseUrl), - let jsonData = try? JSONSerialization.data(withJSONObject: requestBody) else { - throw OpenAIException("创建请求失败") - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") - request.httpBody = jsonData - - // 同步请求 - let semaphore = DispatchSemaphore(value: 0) - var result: String = "" - var error: Error? - - let task = urlSession.dataTask(with: request) { data, response, taskError in - defer { semaphore.signal() } - - if let taskError = taskError { - error = OpenAIException("请求失败: \(taskError.localizedDescription)") - return - } - - guard let httpResponse = response as? HTTPURLResponse else { - error = OpenAIException("无效响应") - return - } - - guard httpResponse.statusCode == 200 else { - error = OpenAIException("API调用失败: \(httpResponse.statusCode)") - return - } - - guard let data = data, - let jsonResponse = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - error = OpenAIException("响应解析失败") - return - } - - // 检查是否有函数调用 - if let choices = jsonResponse["choices"] as? [[String: Any]], - let choice = choices.first, - let message = choice["message"] as? [String: Any] { - - // 检查是否有工具调用 - if let toolCalls = message["tool_calls"] as? [[String: Any]], - let toolCall = toolCalls.first, - let function = toolCall["function"] as? [String: Any], - let name = function["name"] as? String, - let arguments = function["arguments"] as? String, - let id = toolCall["id"] as? String { - - let functionCallJson: [String: Any] = [ - "name": name, - "arguments": arguments, - "id": id - ] - - if let jsonData = try? JSONSerialization.data(withJSONObject: functionCallJson), - let jsonString = String(data: jsonData, encoding: .utf8) { - result = jsonString - } else { - error = OpenAIException("函数调用序列化失败") - } - return - } - - // 如果没有工具调用,返回消息内容 - if let content = message["content"] as? String { - result = content - return - } - } - - error = OpenAIException("无效的响应格式") - } - - task.resume() - semaphore.wait() - - if let error = error { - throw error - } - - return result - } - - /// 发送消息(流式输出) - public func sendMessageStream(messages: [[String: Any]], callback: StreamCallback) { - guard isInitialized && !apiKey.isEmpty else { - callback.onError(OpenAIException("OpenAI服务未初始化")) - return - } - - // 重置状态 - isCanceled = false - currentCallback = callback - currentMessages = messages - responseBuffer = Data() - toolCalls = [:] - - // 优化:初始化性能监控 - streamStartTime = Date() - tokenCount = 0 - bytesReceived = 0 - - // 检查是否包含图片,决定使用哪个模型 - var currentModel = model - if let lastMessage = messages.last, - let content = lastMessage["content"] as? String, - content.contains("image_url") { - currentModel = visionModel - } - - // 构建请求体 - var requestBody: [String: Any] = [ - "model": currentModel, - "messages": messages, - "temperature": 0.7, - "max_tokens": 2000, - "stream": true - ] - - // 添加工具列表 - if let tools = mcpClient?.getToolMaps(), !tools.isEmpty { - requestBody["tools"] = tools - } - - // 创建请求 - guard let url = URL(string: baseUrl), - let jsonData = try? JSONSerialization.data(withJSONObject: requestBody) else { - callback.onError(OpenAIException("创建请求失败")) - return - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") - request.setValue("text/event-stream", forHTTPHeaderField: "Accept") - request.setValue("cache-control", forHTTPHeaderField: "no-cache") - request.httpBody = jsonData - - os_log("开始流式请求: %{public}@", log: logger, type: .info, url.absoluteString) - - // 使用 delegate 模式的 URLSession 创建任务 - let task = streamSession.dataTask(with: request) - currentStreamTask = task - task.resume() - } - - /// 处理缓冲区数据 - private func processBufferedData(callback: StreamCallback, isComplete: Bool = false) { - // 优化:使用流式解析,避免频繁的字符串转换 - processStreamData(callback: callback, isComplete: isComplete) - } - - /// 优化的流式数据处理 - private func processStreamData(callback: StreamCallback, isComplete: Bool = false) { - lineBuffer.append(responseBuffer) - responseBuffer = Data() - - var searchRange = lineBuffer.startIndex.. Bool { - guard let firstToolCall = toolCalls.values.first, firstToolCall.isValid() else { - return false - } - - // 创建函数调用字典 - let functionCall: [String: Any] = [ - "name": firstToolCall.name, - "arguments": firstToolCall.arguments, - "id": firstToolCall.id - ] - - os_log("工具调用: id=%{public}@, name=%{public}@, arguments=%{public}@", log: logger, type: .info, firstToolCall.id, firstToolCall.name, firstToolCall.arguments) - - // 通知上层工具调用事件 - callback.onFunctionCall(functionCall) - - // 在后台队列处理工具调用 - Task { - do { - if !self.isCanceled { - // 调用工具 - let result: [String: Any] - do { - let args = try self.parseJsonArguments(firstToolCall.arguments) - result = try await self.mcpClient?.callTool(name: firstToolCall.name, arguments: args) ?? ["context": "工具调用失败"] - } catch { - os_log("工具调用错误: %{public}@", log: logger, type: .error, error.localizedDescription) - result = ["context": "工具调用失败: \(error.localizedDescription)"] - } - - if !self.isCanceled { - // 处理结果 - callback.onFunctionCallResult(functionCall, result) - - let context = result["context"] as? String ?? "工具调用失败" - os_log("MCP工具调用完成: name=%{public}@, context=%{public}@", log: logger, type: .info, firstToolCall.name, context) - - // 将结果发送回OpenAI继续对话 - self.sendFunctionCallResult( - messages: self.currentMessages, - functionCall: functionCall, - functionResult: context, - callback: callback - ) - } - } - } catch { - if !self.isCanceled { - os_log("处理工具调用异常: %{public}@", log: logger, type: .error, error.localizedDescription) - let errorMessage = "工具调用处理失败: \(error.localizedDescription)" - self.sendFunctionCallResult( - messages: self.currentMessages, - functionCall: functionCall, - functionResult: errorMessage, - callback: callback - ) - } - } - } - - return true - } - - /// 发送函数调用结果 - public func sendFunctionCallResult(messages: [[String: Any]], functionCall: [String: Any], functionResult: String, callback: StreamCallback) { - if isCanceled { - os_log("请求已取消,不发送函数调用结果", log: logger, type: .info) - return - } - - var fullMessages = messages - - // 添加函数调用消息 - let callId = functionCall["id"] as? String ?? "call_\(Int(Date().timeIntervalSince1970))" - fullMessages.append([ - "role": "assistant", - "content": "", - "tool_calls": [[ - "id": callId, - "type": "function", - "function": [ - "name": functionCall["name"] as? String ?? "", - "arguments": functionCall["arguments"] as? String ?? "" - ] - ]] - ]) - - // 添加函数调用结果 - fullMessages.append([ - "role": "tool", - "content": functionResult, - "tool_call_id": callId - ]) - - // 发送完整对话 - sendMessageStream(messages: fullMessages, callback: callback) - } - - /// 取消当前流式请求 - public func cancelCurrentStream() -> Bool { - isCanceled = true - currentStreamTask?.cancel() - - // 优化:更彻底的状态清理 - cleanupStreamState() - - os_log("已取消当前流式请求", log: logger, type: .info) - return true - } - - /// 优化:统一的状态清理方法 - private func cleanupStreamState() { - currentStreamTask = nil - currentCallback = nil - currentMessages.removeAll() - responseBuffer = Data() - lineBuffer = Data() - toolCalls.removeAll() - retryCount = 0 - - // 优化:重置性能监控变量 - streamStartTime = nil - tokenCount = 0 - bytesReceived = 0 - } - - /// 取消所有操作并释放资源 - public func cancelAll() { - _ = cancelCurrentStream() - } - - // MARK: - MCP相关方法 - - /// 初始化MCP客户端 - public func initializeMcpClient(serverUrl: String) -> Bool { - if mcpClient != nil { - mcpClient?.close() - } - - mcpClient = MCPClient() - - Task { - do { - let result = try await mcpClient?.connectToSSE(mcpConfigJson: serverUrl) ?? false - isMcpInitializedFlag = result - os_log("MCP客户端初始化%{public}@", log: logger, type: .info, result ? "成功" : "失败") - } catch { - os_log("MCP客户端初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription) - isMcpInitializedFlag = false - } - } - - return true // 立即返回,实际连接在后台进行 - } - - /// MCP客户端是否已初始化 - public func isMcpInitialized() -> Bool { - return isMcpInitializedFlag && mcpClient?.isConnected() == true - } - - /// 关闭MCP客户端 - public func closeMcpClient() { - mcpClient?.close() - mcpClient = nil - isMcpInitializedFlag = false - } - - /// 注册函数 - public func registerFunction(name: String, description: String, parameters: [String: Any]) -> Bool { - guard let mcpClient = mcpClient else { - os_log("MCP客户端未初始化", log: logger, type: .error) - return false - } - - // 创建函数处理器 - let handler = LocalFunctionHandler(name: name) - - // 注册本地函数 - return mcpClient.registerLocalFunction( - name: name, - description: description, - parameters: parameters, - handler: handler - ) - } - - /// 处理MCP工具调用 - public func handleMcpToolCall(functionCallJson: String) async throws -> String { - guard let mcpClient = mcpClient, isMcpInitializedFlag else { - return "MCP客户端未初始化" - } - - guard let jsonData = functionCallJson.data(using: .utf8), - let functionCall = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any], - let name = functionCall["name"] as? String, - let argumentsJson = functionCall["arguments"] as? String else { - throw OpenAIException("解析函数调用失败") - } - - let arguments = try parseJsonArguments(argumentsJson) - let result = try await mcpClient.callTool(name: name, arguments: arguments) - - return result["context"] as? String ?? "无法处理MCP工具调用" - } - - // MARK: - 工具方法 - - /// 解析JSON参数 - private func parseJsonArguments(_ argumentsJson: String) throws -> [String: Any] { - guard let data = argumentsJson.data(using: .utf8), - let arguments = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - return [:] - } - return arguments - } - - /// 检查字符串是否为有效的JSON - private func isValidJson(jsonString: String) -> Bool { - guard !jsonString.isEmpty, - jsonString.hasPrefix("{"), - jsonString.hasSuffix("}") else { - return false - } - - var braceCount = 0 - var insideQuotes = false - var escapeNext = false - - for char in jsonString { - if escapeNext { - escapeNext = false - continue - } - - switch char { - case "\\": - escapeNext = true - case "\"": - if !escapeNext { - insideQuotes.toggle() - } - case "{": - if !insideQuotes { - braceCount += 1 - } - case "}": - if !insideQuotes { - braceCount -= 1 - } - default: - break - } - - // 如果括号不匹配(闭合太多),提前返回 - if braceCount < 0 { - return false - } - } - - // 所有引号都应该闭合,所有括号也应该匹配 - return braceCount == 0 && !insideQuotes - } - - /// 将文件转换为Base64字符串 - public func fileToBase64(filePath: String, maxSizeKB: Int = 20480) -> String? { - guard let image = UIImage(contentsOfFile: filePath) else { - os_log("文件不存在或无法读取: %{public}@", log: logger, type: .error, filePath) - return nil - } - - var processedImage = image - - // 检查图片尺寸,限制最大为1024*1024 - let maxDimension: CGFloat = 1024 - if image.size.width > maxDimension || image.size.height > maxDimension { - let ratio = min(maxDimension / image.size.width, maxDimension / image.size.height) - let newSize = CGSize(width: image.size.width * ratio, height: image.size.height * ratio) - - UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0) - image.draw(in: CGRect(origin: .zero, size: newSize)) - processedImage = UIGraphicsGetImageFromCurrentImageContext() ?? image - UIGraphicsEndImageContext() - } - - // 压缩图片 - guard let imageData = processedImage.jpegData(compressionQuality: 0.8) else { - return nil - } - - return imageData.base64EncodedString() - } -} - -// MARK: - URLSessionDataDelegate Extension -extension OpenAIService: URLSessionDataDelegate { - - /// 接收到响应头 - public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { - guard let httpResponse = response as? HTTPURLResponse else { - if !isCanceled { - // 直接回调,由调用方处理线程切换 - currentCallback?.onError(OpenAIException("无效响应")) - } - completionHandler(.cancel) - return - } - - guard httpResponse.statusCode == 200 else { - if !isCanceled { - // 直接回调,由调用方处理线程切换 - currentCallback?.onError(OpenAIException("API调用失败: \(httpResponse.statusCode)")) - } - completionHandler(.cancel) - return - } - - os_log("收到响应头,状态码: %d", log: logger, type: .info, httpResponse.statusCode) - completionHandler(.allow) - } - - /// 实时接收数据块 - public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - guard !isCanceled, let callback = currentCallback else { return } - - // 优化:更新性能指标 - bytesReceived += data.count - - // 将新数据添加到缓冲区 - responseBuffer.append(data) - - // 处理缓冲区中的完整行 - processBufferedData(callback: callback) - } - - /// 请求完成 - public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didCompleteWithError error: Error?) { - defer { - // 优化:使用统一的清理方法 - cleanupStreamState() - } - - guard !isCanceled else { - os_log("流式请求已取消", log: logger, type: .info) - return - } - - if let error = error { - os_log("流式请求错误: %{public}@", log: logger, type: .error, error.localizedDescription) - // 直接回调,由调用方处理线程切换 - currentCallback?.onError(OpenAIException("请求失败: \(error.localizedDescription)")) - return - } - - // 处理剩余的缓冲区数据 - if let callback = currentCallback { - processBufferedData(callback: callback, isComplete: true) - } - - // 优化:输出性能统计 - if let startTime = streamStartTime { - let duration = Date().timeIntervalSince(startTime) - let tokensPerSecond = duration > 0 ? Double(tokenCount) / duration : 0 - os_log("流式请求性能统计 - 耗时: %.2fs, tokens: %d, 字节: %d, tokens/s: %.2f", - log: logger, type: .info, duration, tokenCount, bytesReceived, tokensPerSecond) - } - - os_log("流式请求完成", log: logger, type: .info) - } -} - -/// 本地函数处理器 -private class LocalFunctionHandler: FunctionHandler { - private let functionName: String - - init(name: String) { - self.functionName = name - } - - func handle(arguments: [String: Any]) async throws -> String { - // 由于本地函数的实际处理是在Flutter端完成的 - // 这里只需返回一个标记,表示该函数是本地函数 - return "LOCAL_FUNCTION:\(functionName)" - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift b/apps/client/local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift deleted file mode 100644 index bf99d940..00000000 --- a/apps/client/local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift +++ /dev/null @@ -1,340 +0,0 @@ -import Flutter -import UIKit -import Foundation - -/// OpenAI服务插件主类 -public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler { - - // MARK: - 常量 - private static let methodChannelName = "com.yunqiinnovation.open_ai_service/methods" - private static let eventChannelName = "com.yunqiinnovation.open_ai_service/events" - - // MARK: - 属性 - private var methodChannel: FlutterMethodChannel? - private var eventChannel: FlutterEventChannel? - private var eventSink: FlutterEventSink? - private var openAIService: OpenAIService? - - // MARK: - 插件注册 - public static func register(with registrar: FlutterPluginRegistrar) { - let instance = OpenAIServicePlugin() - - // 初始化方法通道 - let methodChannel = FlutterMethodChannel( - name: methodChannelName, - binaryMessenger: registrar.messenger() - ) - instance.methodChannel = methodChannel - registrar.addMethodCallDelegate(instance, channel: methodChannel) - - // 初始化事件通道 - let eventChannel = FlutterEventChannel( - name: eventChannelName, - binaryMessenger: registrar.messenger() - ) - instance.eventChannel = eventChannel - eventChannel.setStreamHandler(instance) - - // 初始化OpenAI服务 - instance.openAIService = OpenAIService() - } - - // MARK: - FlutterPlugin - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - guard let service = openAIService else { - result(FlutterError(code: "SERVICE_NOT_INITIALIZED", - message: "OpenAI服务未初始化", - details: nil)) - return - } - - switch call.method { - case "initialize": - handleInitialize(call: call, result: result, service: service) - - case "registerFunction": - handleRegisterFunction(call: call, result: result, service: service) - - case "sendMessage": - handleSendMessage(call: call, result: result, service: service) - - case "sendMessageStream": - handleSendMessageStream(call: call, result: result, service: service) - - case "sendFunctionCallResult": - handleSendFunctionCallResult(call: call, result: result, service: service) - - case "createUserMessage": - handleCreateUserMessage(call: call, result: result, service: service) - - case "createAssistantMessage": - handleCreateAssistantMessage(call: call, result: result, service: service) - - case "initializeMcpClient": - handleInitializeMcpClient(call: call, result: result, service: service) - - case "isMcpInitialized": - handleIsMcpInitialized(call: call, result: result, service: service) - - case "closeMcpClient": - handleCloseMcpClient(call: call, result: result, service: service) - - case "handleMcpToolCall": - handleMcpToolCall(call: call, result: result, service: service) - - default: - result(FlutterMethodNotImplemented) - } - } - - // MARK: - FlutterStreamHandler - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - self.eventSink = events - return nil - } - - public func onCancel(withArguments arguments: Any?) -> FlutterError? { - self.eventSink = nil - return nil - } - - // MARK: - 方法处理 - private func handleInitialize(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let apiKey = args["apiKey"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少必要参数", - details: nil)) - return - } - - let baseUrl = args["baseUrl"] as? String ?? "" - let model = args["model"] as? String ?? "" - let mcpServer = args["mcpServer"] as? String ?? "" - - let success = service.initialize( - apiKey: apiKey, - baseUrl: baseUrl, - model: model, - mcpServer: mcpServer - ) - - result(success) - } - - private func handleRegisterFunction(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let name = args["name"] as? String, - let description = args["description"] as? String, - let parameters = args["parameters"] as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少必要参数", - details: nil)) - return - } - - let success = service.registerFunction(name: name, description: description, parameters: parameters) - result(success) - } - - private func handleSendMessage(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let messagesArray = args["messages"] as? [[String: Any]] else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少消息参数", - details: nil)) - return - } - - do { - let response = try service.sendMessage(messages: messagesArray) - result(response) - } catch { - result(FlutterError(code: "OPENAI_ERROR", - message: error.localizedDescription, - details: nil)) - } - } - - private func handleSendMessageStream(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let messagesArray = args["messages"] as? [[String: Any]] else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少消息参数", - details: nil)) - return - } - - guard let eventSink = self.eventSink else { - result(FlutterError(code: "NO_EVENT_SINK", - message: "没有可用的事件流接收器", - details: nil)) - return - } - - // 创建回调处理器 - let callback = StreamCallbackImpl(eventSink: eventSink) - - // 发送流式消息 - service.sendMessageStream(messages: messagesArray, callback: callback) - - result(true) - } - - private func handleSendFunctionCallResult(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let messagesArray = args["messages"] as? [[String: Any]], - let functionCall = args["functionCall"] as? [String: Any], - let functionResult = args["functionResult"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少必要参数", - details: nil)) - return - } - - guard let eventSink = self.eventSink else { - result(FlutterError(code: "NO_EVENT_SINK", - message: "没有可用的事件流接收器", - details: nil)) - return - } - - // 创建回调处理器 - let callback = StreamCallbackImpl(eventSink: eventSink) - - // 发送函数调用结果 - service.sendFunctionCallResult( - messages: messagesArray, - functionCall: functionCall, - functionResult: functionResult, - callback: callback - ) - - result(true) - } - - private func handleCreateUserMessage(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let content = args["content"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少内容参数", - details: nil)) - return - } - - let message = service.createUserMessage(content: content) - result(message) - } - - private func handleCreateAssistantMessage(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let content = args["content"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少内容参数", - details: nil)) - return - } - - let message = service.createAssistantMessage(content: content) - result(message) - } - - private func handleInitializeMcpClient(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let serverUrl = args["serverUrl"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少服务器URL参数", - details: nil)) - return - } - - let success = service.initializeMcpClient(serverUrl: serverUrl) - result(success) - } - - private func handleIsMcpInitialized(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - let isInitialized = service.isMcpInitialized() - result(isInitialized) - } - - private func handleCloseMcpClient(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - service.closeMcpClient() - result(true) - } - - private func handleMcpToolCall(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let functionCallJson = args["functionCall"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少函数调用参数", - details: nil)) - return - } - - Task { - do { - let toolResult = try await service.handleMcpToolCall(functionCallJson: functionCallJson) - DispatchQueue.main.async { - result(toolResult) - } - } catch { - DispatchQueue.main.async { - result(FlutterError(code: "MCP_TOOL_ERROR", - message: "处理MCP工具调用失败: \(error.localizedDescription)", - details: nil)) - } - } - } - } -} - -// MARK: - 流式回调实现 -private class StreamCallbackImpl: StreamCallback { - private let eventSink: FlutterEventSink - - init(eventSink: @escaping FlutterEventSink) { - self.eventSink = eventSink - } - - func onToken(_ token: String) { - DispatchQueue.main.async { - self.eventSink(["type": "token", "content": token]) - } - } - - func onComplete() { - DispatchQueue.main.async { - self.eventSink(["type": "complete"]) - } - } - - func onError(_ error: Error) { - DispatchQueue.main.async { - self.eventSink(["type": "error", "content": error.localizedDescription]) - } - } - - func onFunctionCall(_ functionCall: [String: Any]) { - DispatchQueue.main.async { - if let jsonData = try? JSONSerialization.data(withJSONObject: functionCall), - let jsonString = String(data: jsonData, encoding: .utf8) { - self.eventSink(["type": "function_call", "content": jsonString]) - } - } - } - - func onFunctionCallResult(_ functionCall: [String: Any], _ functionCallResult: [String: Any]) { - DispatchQueue.main.async { - if let callData = try? JSONSerialization.data(withJSONObject: functionCall), - let callString = String(data: callData, encoding: .utf8), - let resultData = try? JSONSerialization.data(withJSONObject: functionCallResult), - let resultString = String(data: resultData, encoding: .utf8) { - self.eventSink([ - "type": "function_call_result", - "function_call": callString, - "result": resultString - ]) - } - } - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift b/apps/client/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift deleted file mode 100644 index 7c577e70..00000000 --- a/apps/client/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift +++ /dev/null @@ -1,680 +0,0 @@ -import Foundation -import UIKit -import EventKit -import CoreLocation -import MediaPlayer -import AVFoundation - -/// 操作结果 -public struct Result { - let success: Bool - let message: String - - public init(success: Bool, message: String) { - self.success = success - self.message = message - } -} - -/// 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能 -public class SystemFunctionHandler: NSObject { - private let tag = "SystemFunctionHandler" - - // 位置管理器 - private var locationManager: CLLocationManager? - private var locationCompletion: ((Result) -> Void)? - - // 事件存储 - private var eventStore: EKEventStore? - - public override init() { - super.init() - setupLocationManager() - setupEventStore() - } - - /// 设置位置管理器 - private func setupLocationManager() { - locationManager = CLLocationManager() - locationManager?.delegate = self - locationManager?.desiredAccuracy = kCLLocationAccuracyBest - } - - /// 设置事件存储 - private func setupEventStore() { - eventStore = EKEventStore() - } - - /// 在MCPClient中注册所有系统功能 - /// - Parameter client: MCPClient实例 - public func registerAllFunctions(client: MCPClient) { - do { - // 注册退出交互函数 - registerExitInteractionFunction(client: client) - - // 注册翻译模式函数 - registerTranslationModeFunction(client: client) - - // 注册设置提醒函数 - registerSetReminderFunction(client: client) - - // 注册获取当前时间函数 - registerGetCurrentTimeFunction(client: client) - - // 注册获取当前位置函数 - registerGetCurrentLocationFunction(client: client) - - // 注册媒体控制函数 - registerMediaPlayFunction(client: client) - registerMediaPauseFunction(client: client) - registerMediaPreviousFunction(client: client) - registerMediaNextFunction(client: client) - - // 注册打开录音机功能 - registerOpenRecorderFunction(client: client) - - print("\(tag) 所有系统功能注册完成") - } catch { - print("\(tag) 注册系统功能失败: \(error.localizedDescription)") - } - } - - // MARK: - 函数注册 - - /// 注册退出交互函数 - private func registerExitInteractionFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = ExitInteractionHandler() - _ = client.registerLocalFunction( - name: "exit_interaction", - description: "结束当前交互", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册退出交互功能") - } - - /// 注册翻译模式函数 - private func registerTranslationModeFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = TranslationModeHandler() - _ = client.registerLocalFunction( - name: "enter_translation_mode", - description: "用户请求进入实时翻译模式时,启动实时翻译功能", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册翻译模式功能") - } - - /// 注册设置提醒函数 - private func registerSetReminderFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [ - "title": [ - "type": "string", - "description": "提醒标题" - ], - "content": [ - "type": "string", - "description": "提醒内容" - ], - "time": [ - "type": "string", - "description": "提醒时间,格式为'yyyy-MM-dd HH:mm',如'2023-12-31 14:30'" - ] - ], - "required": ["title", "time"] - ] - - let handler = SetReminderHandler(eventStore: eventStore) - _ = client.registerLocalFunction( - name: "set_reminder", - description: "设置提醒事项", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册设置提醒功能") - } - - /// 注册获取当前时间函数 - private func registerGetCurrentTimeFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [ - "format": [ - "type": "string", - "enum": ["short", "date", "time", "full", "iso"], - "description": "时间输出粒度,仅可使用枚举值:short(仅时分)、date(仅日期)、time(时分秒)、full(日期+星期+时间)、iso(机器可读)。禁止传入 SimpleDateFormat/strftime 等格式串。" - ] - ], - "required": [] - ] - - let handler = GetCurrentTimeHandler() - _ = client.registerLocalFunction( - name: "get_current_time", - description: "获取当前日期和时间,返回值已按当前语言本地化并对TTS友好(不含冒号等易被误读的符号)。播报或回复给用户时必须原样使用 result 字段的文本,不要自行改写为 HH:mm:ss 等格式。", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册获取当前时间功能") - } - - /// 注册获取当前位置函数 - private func registerGetCurrentLocationFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = GetCurrentLocationHandler(systemHandler: self) - _ = client.registerLocalFunction( - name: "get_current_location", - description: "获取当前地理位置", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册获取当前位置功能") - } - - /// 注册媒体播放功能 - private func registerMediaPlayFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = MediaPlayHandler() - _ = client.registerLocalFunction( - name: "media_play", - description: "播放媒体", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册媒体播放功能") - } - - /// 注册媒体暂停功能 - private func registerMediaPauseFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = MediaPauseHandler() - _ = client.registerLocalFunction( - name: "media_pause", - description: "暂停媒体播放", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册媒体暂停功能") - } - - /// 注册媒体上一首功能 - private func registerMediaPreviousFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = MediaPreviousHandler() - _ = client.registerLocalFunction( - name: "media_previous", - description: "播放上一首", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册媒体上一首功能") - } - - /// 注册媒体下一首功能 - private func registerMediaNextFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = MediaNextHandler() - _ = client.registerLocalFunction( - name: "media_next", - description: "播放下一首", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册媒体下一首功能") - } - - /// 注册打开录音机功能 - private func registerOpenRecorderFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = OpenRecorderHandler() - _ = client.registerLocalFunction( - name: "open_recorder", - description: "打开系统录音机并开始录音", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册打开录音机功能") - } - - // MARK: - 内部方法 - - /// 获取当前位置 - internal func getCurrentLocation() async -> Result { - return await withCheckedContinuation { continuation in - locationCompletion = { result in - continuation.resume(returning: result) - } - - guard let locationManager = locationManager else { - continuation.resume(returning: Result(success: false, message: "位置管理器未初始化")) - return - } - - // 检查位置权限 - let authStatus = locationManager.authorizationStatus - switch authStatus { - case .notDetermined: - locationManager.requestWhenInUseAuthorization() - return - case .denied, .restricted: - continuation.resume(returning: Result(success: false, message: "位置权限被拒绝")) - return - case .authorizedWhenInUse, .authorizedAlways: - break - @unknown default: - continuation.resume(returning: Result(success: false, message: "未知的位置权限状态")) - return - } - - // 检查位置服务是否可用 - guard CLLocationManager.locationServicesEnabled() else { - continuation.resume(returning: Result(success: false, message: "位置服务未启用")) - return - } - - // 请求位置 - locationManager.requestLocation() - } - } -} - -// MARK: - CLLocationManagerDelegate -extension SystemFunctionHandler: CLLocationManagerDelegate { - public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - guard let location = locations.last else { - locationCompletion?(Result(success: false, message: "无法获取位置信息")) - return - } - - let latitude = location.coordinate.latitude - let longitude = location.coordinate.longitude - let message = "当前位置:纬度 \(latitude),经度 \(longitude)" - - locationCompletion?(Result(success: true, message: message)) - locationCompletion = nil - } - - public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - locationCompletion?(Result(success: false, message: "获取位置失败:\(error.localizedDescription)")) - locationCompletion = nil - } - - public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { - if status == .authorizedWhenInUse || status == .authorizedAlways { - manager.requestLocation() - } else if status == .denied || status == .restricted { - locationCompletion?(Result(success: false, message: "位置权限被拒绝")) - locationCompletion = nil - } - } -} - -// MARK: - 功能处理器实现 - -/// 退出交互处理器 -private class ExitInteractionHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - // 发送退出交互通知 - NotificationCenter.default.post(name: NSNotification.Name("ExitInteraction"), object: nil) - return "{\"result\": \"已结束当前交互\"}" - } -} - -/// 翻译模式处理器 -private class TranslationModeHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - // 发送进入翻译模式通知 - NotificationCenter.default.post(name: NSNotification.Name("EnterTranslationMode"), object: nil) - return "{\"result\": \"已进入翻译模式\"}" - } -} - -/// 设置提醒处理器 -private class SetReminderHandler: FunctionHandler { - private let eventStore: EKEventStore? - - init(eventStore: EKEventStore?) { - self.eventStore = eventStore - } - - func handle(arguments: [String: Any]) async throws -> String { - guard let title = arguments["title"] as? String, - let timeString = arguments["time"] as? String else { - throw OpenAIException("缺少必要参数") - } - - let content = arguments["content"] as? String ?? "" - let result = await setReminder(title: title, content: content, time: timeString) - return "{\"result\": \"\(result.message)\"}" - } - - private func setReminder(title: String, content: String, time: String) async -> Result { - guard let eventStore = eventStore else { - return Result(success: false, message: "事件存储未初始化") - } - - // 请求日历权限 - let status = await requestCalendarAccess() - guard status == .authorized else { - return Result(success: false, message: "日历权限被拒绝") - } - - // 解析时间 - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd HH:mm" - guard let date = formatter.date(from: time) else { - return Result(success: false, message: "时间格式错误") - } - - // 创建提醒 - let reminder = EKReminder(eventStore: eventStore) - reminder.title = title - reminder.notes = content - reminder.calendar = eventStore.defaultCalendarForNewReminders() - - let alarm = EKAlarm(absoluteDate: date) - reminder.addAlarm(alarm) - - do { - try eventStore.save(reminder, commit: true) - return Result(success: true, message: "提醒设置成功") - } catch { - return Result(success: false, message: "设置提醒失败:\(error.localizedDescription)") - } - } - - private func requestCalendarAccess() async -> EKAuthorizationStatus { - return await withCheckedContinuation { continuation in - eventStore?.requestAccess(to: .reminder) { granted, error in - let status = EKEventStore.authorizationStatus(for: .reminder) - continuation.resume(returning: status) - } - } - } -} - -/// 获取当前时间处理器 -private class GetCurrentTimeHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let format = arguments["format"] as? String - let result = getCurrentTime(format: format) - return "{\"result\": \"\(result.message)\", \"time\": \"\(result.message)\"}" - } - - private func getCurrentTime(format: String?) -> Result { - let now = Date() - let lang = Locale.current.languageCode ?? "en" - - let timeStr: String - switch format?.lowercased() { - case "short": - timeStr = formatShortTime(now, lang: lang) - case "date": - timeStr = formatDate(now, lang: lang) - case "time": - timeStr = formatTime(now, lang: lang) - case "full": - timeStr = formatFull(now, lang: lang) - case "iso": - let f = DateFormatter() - f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - timeStr = f.string(from: now) - default: - timeStr = formatDefault(now, lang: lang) - } - - let resultTime: String - switch format?.lowercased() { - case "short", "time", "iso": - resultTime = timeStr - case "full": - resultTime = timeStr - default: - let weekFormatter = DateFormatter() - weekFormatter.dateFormat = "EEEE" - weekFormatter.locale = Locale.current - resultTime = "\(timeStr) \(weekFormatter.string(from: now))" - } - - return Result(success: true, message: resultTime) - } - - // TTS友好的时间格式,避免AI将":"分隔的时间误读为数字 - // CJK 使用单位数 m/s(去掉前导零),其他语言使用英文单词避免冒号 - private func formatShortTime(_ date: Date, lang: String) -> String { - let f = DateFormatter() - switch lang { - case "zh": f.dateFormat = "H时m分"; f.locale = Locale(identifier: "zh_CN"); return f.string(from: date) - case "ja": f.dateFormat = "H時m分"; f.locale = Locale(identifier: "ja_JP"); return f.string(from: date) - case "ko": f.dateFormat = "H시 m분"; f.locale = Locale(identifier: "ko_KR"); return f.string(from: date) - default: return formatTimeWestern(date, withSeconds: false) - } - } - - private func formatDate(_ date: Date, lang: String) -> String { - let f = DateFormatter() - switch lang { - case "zh": f.dateFormat = "yyyy年M月d日"; f.locale = Locale(identifier: "zh_CN") - case "ja": f.dateFormat = "yyyy年M月d日"; f.locale = Locale(identifier: "ja_JP") - case "ko": f.dateFormat = "yyyy년 M월 d일"; f.locale = Locale(identifier: "ko_KR") - default: f.dateFormat = "MMMM d, yyyy"; f.locale = Locale.current - } - return f.string(from: date) - } - - private func formatTime(_ date: Date, lang: String) -> String { - let f = DateFormatter() - switch lang { - case "zh": f.dateFormat = "H时m分s秒"; f.locale = Locale(identifier: "zh_CN"); return f.string(from: date) - case "ja": f.dateFormat = "H時m分s秒"; f.locale = Locale(identifier: "ja_JP"); return f.string(from: date) - case "ko": f.dateFormat = "H시 m분 s초"; f.locale = Locale(identifier: "ko_KR"); return f.string(from: date) - default: return formatTimeWestern(date, withSeconds: true) - } - } - - private func formatFull(_ date: Date, lang: String) -> String { - let f = DateFormatter() - switch lang { - case "zh": f.dateFormat = "yyyy年M月d日 EEEE H时m分s秒"; f.locale = Locale(identifier: "zh_CN"); return f.string(from: date) - case "ja": f.dateFormat = "yyyy年M月d日 EEEE H時m分s秒"; f.locale = Locale(identifier: "ja_JP"); return f.string(from: date) - case "ko": f.dateFormat = "yyyy년 M월 d일 EEEE H시 m분 s초"; f.locale = Locale(identifier: "ko_KR"); return f.string(from: date) - default: - f.dateFormat = "MMMM d, yyyy, EEEE, "; f.locale = Locale.current - return f.string(from: date) + formatTimeWestern(date, withSeconds: true) - } - } - - private func formatDefault(_ date: Date, lang: String) -> String { - let f = DateFormatter() - switch lang { - case "zh": f.dateFormat = "yyyy年M月d日 H时m分"; f.locale = Locale(identifier: "zh_CN"); return f.string(from: date) - case "ja": f.dateFormat = "yyyy年M月d日 H時m分"; f.locale = Locale(identifier: "ja_JP"); return f.string(from: date) - case "ko": f.dateFormat = "yyyy년 M월 d일 H시 m분"; f.locale = Locale(identifier: "ko_KR"); return f.string(from: date) - default: - f.dateFormat = "MMMM d, yyyy, "; f.locale = Locale.current - return f.string(from: date) + formatTimeWestern(date, withSeconds: false) - } - } - - /// 非 CJK 语言的时间格式:避免冒号,使用英文单词,对 TTS 更稳定 - private func formatTimeWestern(_ date: Date, withSeconds: Bool) -> String { - let cal = Calendar.current - let hour24 = cal.component(.hour, from: date) - let minute = cal.component(.minute, from: date) - let second = cal.component(.second, from: date) - let h12Raw = hour24 % 12 - let h12 = h12Raw == 0 ? 12 : h12Raw - let ampm = hour24 < 12 ? "AM" : "PM" - - var parts: [String] = [] - parts.append("\(h12) \(h12 == 1 ? "hour" : "hours")") - if minute > 0 || withSeconds { - parts.append("\(minute) \(minute == 1 ? "minute" : "minutes")") - } - if withSeconds { - parts.append("\(second) \(second == 1 ? "second" : "seconds")") - } - parts.append(ampm) - return parts.joined(separator: " ") - } -} - -/// 获取当前位置处理器 -private class GetCurrentLocationHandler: FunctionHandler { - private weak var systemHandler: SystemFunctionHandler? - - init(systemHandler: SystemFunctionHandler) { - self.systemHandler = systemHandler - } - - func handle(arguments: [String: Any]) async throws -> String { - guard let systemHandler = systemHandler else { - throw OpenAIException("系统处理器未初始化") - } - - let result = await systemHandler.getCurrentLocation() - return "{\"result\": \"\(result.message)\", \"success\": \(result.success)}" - } -} - -/// 媒体播放处理器 -private class MediaPlayHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let result = controlMedia(action: "play") - return "{\"result\": \"\(result.message)\"}" - } - - private func controlMedia(action: String) -> Result { - let commandCenter = MPRemoteCommandCenter.shared() - - switch action { - case "play": - // 发送播放命令通知 - NotificationCenter.default.post(name: NSNotification.Name("MediaPlay"), object: nil) - return Result(success: true, message: "已开始播放媒体") - default: - return Result(success: false, message: "未知的媒体操作") - } - } -} - -/// 媒体暂停处理器 -private class MediaPauseHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let result = controlMedia(action: "pause") - return "{\"result\": \"\(result.message)\"}" - } - - private func controlMedia(action: String) -> Result { - switch action { - case "pause": - // 发送暂停命令通知 - NotificationCenter.default.post(name: NSNotification.Name("MediaPause"), object: nil) - return Result(success: true, message: "已暂停媒体播放") - default: - return Result(success: false, message: "未知的媒体操作") - } - } -} - -/// 媒体上一首处理器 -private class MediaPreviousHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let result = controlMedia(action: "previous") - return "{\"result\": \"\(result.message)\"}" - } - - private func controlMedia(action: String) -> Result { - switch action { - case "previous": - // 发送上一首命令通知 - NotificationCenter.default.post(name: NSNotification.Name("MediaPrevious"), object: nil) - return Result(success: true, message: "已切换到上一首") - default: - return Result(success: false, message: "未知的媒体操作") - } - } -} - -/// 媒体下一首处理器 -private class MediaNextHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let result = controlMedia(action: "next") - return "{\"result\": \"\(result.message)\"}" - } - - private func controlMedia(action: String) -> Result { - switch action { - case "next": - // 发送下一首命令通知 - NotificationCenter.default.post(name: NSNotification.Name("MediaNext"), object: nil) - return Result(success: true, message: "已切换到下一首") - default: - return Result(success: false, message: "未知的媒体操作") - } - } -} - -/// 打开录音机处理器 -private class OpenRecorderHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let result = openRecorder() - return "{\"result\": \"\(result.message)\"}" - } - - private func openRecorder() -> Result { - // 尝试打开语音备忘录应用 - if let url = URL(string: "voicememos://") { - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) - return Result(success: true, message: "已打开语音备忘录") - } - } - - return Result(success: false, message: "无法打开录音机应用") - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Package.swift b/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Package.swift deleted file mode 100644 index 64aa6b14..00000000 --- a/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Package.swift +++ /dev/null @@ -1,44 +0,0 @@ -// swift-tools-version: 5.9 -import PackageDescription - -let package = Package( - name: "open_ai_service", - platforms: [.iOS("13.0")], - products: [ - .library(name: "open-ai-service", targets: ["open_ai_service"]) - ], - dependencies: [ - // OpenAI Swift SDK - .package(url: "https://github.com/MacPaw/OpenAI.git", from: "0.4.3") - ], - targets: [ - .target( - name: "open_ai_service", - dependencies: [ - .product(name: "OpenAI", package: "OpenAI") - ], - path: "Sources/open_ai_service", - linkerSettings: [ - .linkedFramework("Foundation"), - .linkedFramework("UIKit"), - .linkedFramework("EventKit"), - .linkedFramework("CoreLocation"), - .linkedFramework("MediaPlayer"), - .linkedFramework("AVFoundation"), - .linkedFramework("AudioToolbox"), - .linkedFramework("Accelerate"), - .linkedFramework("QuartzCore"), - .linkedFramework("Security"), - .linkedFramework("SystemConfiguration"), - .linkedFramework("CoreTelephony"), - .linkedFramework("CFNetwork"), - .linkedFramework("CoreGraphics"), - .linkedFramework("MobileCoreServices"), - .linkedFramework("CoreData"), - .linkedFramework("ImageIO"), - .linkedFramework("CoreText"), - .linkedFramework("CoreImage") - ] - ) - ] -) \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/MCPClient.swift b/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/MCPClient.swift deleted file mode 100644 index 121653f0..00000000 --- a/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/MCPClient.swift +++ /dev/null @@ -1,366 +0,0 @@ -import Foundation - -/// 工具类型枚举 -public enum ToolType { - case localFunction // 本地函数 - case mcpTool // MCP工具 -} - -/// 函数处理器协议 -public protocol FunctionHandler { - /// 处理函数调用 - /// - Parameter arguments: 函数参数,字典格式 - /// - Returns: 函数执行结果,字符串 - func handle(arguments: [String: Any]) async throws -> String -} - -/// 单个MCP子客户端 -/// 负责连接到一个具体的MCP服务器并管理其工具 -public class MCPSubClient { - private let tag: String - private let serverId: String - private let serverUrl: String - - // 服务器提供的工具列表 - private var tools: [[String: Any]] = [] - - // 工具Map列表,用于OpenAI请求 - private var toolMaps: [[String: Any]] = [] - - // 是否已连接 - private var isConnectedFlag = false - - // 工具名称到工具的映射,用于快速查找 - private var toolMapping: [String: [String: Any]] = [:] - - // URL会话 - private lazy var urlSession: URLSession = { - let config = URLSessionConfiguration.default - config.timeoutIntervalForRequest = 30 - config.timeoutIntervalForResource = 30 - return URLSession(configuration: config) - }() - - public init(serverId: String, serverUrl: String) { - self.serverId = serverId - self.serverUrl = serverUrl - self.tag = "MCPSubClient[\(serverId)]" - print("\(tag) 初始化,URL: \(serverUrl)") - } - - /// 连接到MCP服务器(通过SSE) - /// - Returns: 是否连接成功 - public func connect() async -> Bool { - do { - print("\(tag) 开始连接服务器: \(serverUrl)") - - // 这里应该实现实际的SSE连接逻辑 - // 由于iOS没有直接的SSE库,我们可以使用URLSession来模拟 - // 或者使用第三方库如EventSource - - // 模拟连接成功并获取工具列表 - await processToolsList() - - isConnectedFlag = true - print("\(tag) 连接成功,可用工具: \(tools.count)个") - return true - } catch { - print("\(tag) 连接失败: \(error.localizedDescription)") - return false - } - } - - /// 处理工具列表 - private func processToolsList() async { - // 这里应该从MCP服务器获取实际的工具列表 - // 目前先模拟一些基本工具 - tools = [] - - // 更新工具映射 - toolMapping.removeAll() - for tool in tools { - if let name = tool["name"] as? String { - toolMapping[name] = tool - } - } - - // 更新工具Map列表 - updateToolMaps() - - print("\(tag) 成功处理 \(tools.count) 个工具") - } - - /// 更新工具Map列表,用于OpenAI请求 - /// 将MCP工具转换为OpenAI函数调用格式 - private func updateToolMaps() { - toolMaps.removeAll() - - for tool in tools { - guard let name = tool["name"] as? String, - let description = tool["description"] as? String else { - continue - } - - let functionMap: [String: Any] = [ - "name": name, - "description": description, - "parameters": [ - "type": "object", - "properties": [:], - "required": [] - ] - ] - - toolMaps.append([ - "type": "function", - "function": functionMap - ]) - } - - print("\(tag) 更新工具映射完成,共 \(toolMaps.count) 个工具") - } - - /// 获取工具Map列表,用于OpenAI请求 - public func getToolMaps() -> [[String: Any]] { - return toolMaps - } - - /// 获取可用的工具列表 - public func getTools() -> [[String: Any]] { - return tools - } - - /// 检查是否包含指定工具 - public func containsTool(name: String) -> Bool { - return toolMapping.keys.contains(name) - } - - /// 调用工具 - public func callTool(name: String, arguments: [String: Any]) async throws -> [String: Any] { - guard isConnectedFlag else { - print("\(tag) 未连接到服务器") - return ["context": "未连接到MCP服务器"] - } - - guard containsTool(name: name) else { - print("\(tag) 此客户端不包含工具: \(name)") - throw OpenAIException("工具不存在: \(name)") - } - - // 这里应该实现实际的工具调用逻辑 - // 目前先返回模拟结果 - return [ - "name": name, - "context": "工具调用结果: \(name)" - ] - } - - /// 检查是否已连接 - public func isConnected() -> Bool { - return isConnectedFlag - } - - /// 关闭连接 - public func close() { - isConnectedFlag = false - print("\(tag) 已关闭MCP服务器连接") - } -} - -/// MCP客户端,用于连接Model Context Protocol服务器和管理本地函数 -public class MCPClient { - private let tag = "MCPClient" - - // 本地函数Map,函数名 -> 处理器 - private var localFunctions: [String: FunctionHandler] = [:] - - // 本地函数定义Map,函数名 -> 定义 - private var localFunctionDefs: [String: String] = [:] - - // 子客户端列表,每个连接一个MCP服务器 - private var subClients: [String: MCPSubClient] = [:] - - // 是否已连接 - private var isConnectedFlag = false - - // 系统功能处理器 - private var systemFunctionHandler: SystemFunctionHandler? - - public init() { - // 初始化系统功能处理器 - systemFunctionHandler = SystemFunctionHandler() - initializeVoiceFunctions() - } - - /// 初始化语音功能处理器 - private func initializeVoiceFunctions() { - do { - // 使用SystemFunctionHandler注册所有功能 - systemFunctionHandler?.registerAllFunctions(client: self) - print("\(tag) 语音功能初始化完成") - } catch { - print("\(tag) 语音功能初始化失败: \(error.localizedDescription)") - } - } - - /// 连接到MCP服务器(通过SSE) - /// 直接接收完整的JSON配置字符串 - /// - Parameter mcpConfigJson: 包含mcpServers字段的JSON配置字符串 - /// - Returns: 是否连接成功 - public func connectToSSE(mcpConfigJson: String) async throws -> Bool { - // 清除现有连接 - closeAllConnections() - - var serverMap: [String: String] = [:] - var configLoaded = false - - // 尝试解析传入的JSON配置 - if !mcpConfigJson.isEmpty { - do { - guard let data = mcpConfigJson.data(using: .utf8), - let config = try JSONSerialization.jsonObject(with: data) as? [String: Any], - let mcpServers = config["mcpServers"] as? [String: [String: String]] else { - throw OpenAIException("无效的MCP配置格式") - } - - // 从服务器配置中提取URL - for (serverId, serverConfig) in mcpServers { - if let url = serverConfig["url"], !url.isEmpty { - serverMap[serverId] = url - print("\(tag) 从配置中添加服务器 [\(serverId)]: \(url)") - } - } - - configLoaded = !serverMap.isEmpty - } catch { - print("\(tag) 解析MCP配置失败: \(error.localizedDescription)") - } - } - - // 如果没有从配置中加载到服务器,使用默认配置 - if !configLoaded { - print("\(tag) 使用默认MCP服务器配置") - // 可以在这里添加默认的服务器配置 - } - - // 连接到所有服务器 - var successCount = 0 - for (serverId, serverUrl) in serverMap { - let subClient = MCPSubClient(serverId: serverId, serverUrl: serverUrl) - let connected = await subClient.connect() - - if connected { - subClients[serverId] = subClient - successCount += 1 - print("\(tag) 成功连接到服务器 [\(serverId)]") - } else { - print("\(tag) 连接服务器失败 [\(serverId)]") - } - } - - isConnectedFlag = successCount > 0 - print("\(tag) MCP客户端连接完成,成功连接 \(successCount)/\(serverMap.count) 个服务器") - - return isConnectedFlag - } - - /// 关闭所有连接 - private func closeAllConnections() { - for (_, subClient) in subClients { - subClient.close() - } - subClients.removeAll() - isConnectedFlag = false - } - - /// 获取工具Map列表,用于OpenAI请求 - public func getToolMaps() -> [[String: Any]] { - var allToolMaps: [[String: Any]] = [] - - // 添加所有子客户端的工具 - for (_, subClient) in subClients { - allToolMaps.append(contentsOf: subClient.getToolMaps()) - } - - // 添加本地函数 - for (name, _) in localFunctions { - if let def = localFunctionDefs[name], - let data = def.data(using: .utf8), - let functionDef = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { - allToolMaps.append([ - "type": "function", - "function": functionDef - ]) - } - } - - return allToolMaps - } - - /// 注册本地函数 - public func registerLocalFunction(name: String, description: String, parameters: [String: Any], handler: FunctionHandler) -> Bool { - do { - localFunctions[name] = handler - - let functionDef: [String: Any] = [ - "name": name, - "description": description, - "parameters": parameters - ] - - let jsonData = try JSONSerialization.data(withJSONObject: functionDef) - let jsonString = String(data: jsonData, encoding: .utf8) ?? "" - localFunctionDefs[name] = jsonString - - // print("\(tag) 成功注册本地函数: \(name)") - return true - } catch { - print("\(tag) 注册本地函数失败: \(error.localizedDescription)") - return false - } - } - - /// 调用工具 - public func callTool(name: String, arguments: [String: Any]) async throws -> [String: Any] { - // 首先检查是否是本地函数 - if let handler = localFunctions[name] { - do { - let result = try await handler.handle(arguments: arguments) - return ["context": result] - } catch { - throw OpenAIException("本地函数调用失败: \(error.localizedDescription)") - } - } - - // 然后检查MCP工具 - for (_, subClient) in subClients { - if subClient.containsTool(name: name) { - return try await subClient.callTool(name: name, arguments: arguments) - } - } - - throw OpenAIException("未找到工具: \(name)") - } - - /// 解析JSON参数 - public func parseJsonArguments(_ argumentsJson: String) throws -> [String: Any] { - guard let data = argumentsJson.data(using: .utf8), - let arguments = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - return [:] - } - return arguments - } - - /// 检查是否已连接 - public func isConnected() -> Bool { - return isConnectedFlag || !localFunctions.isEmpty - } - - /// 关闭连接 - public func close() { - closeAllConnections() - localFunctions.removeAll() - localFunctionDefs.removeAll() - print("\(tag) 已关闭MCP客户端") - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/OpenAIService.swift b/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/OpenAIService.swift deleted file mode 100644 index 35e40dc3..00000000 --- a/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/OpenAIService.swift +++ /dev/null @@ -1,869 +0,0 @@ -import Foundation -import UIKit -import os -import os.log - -/// OpenAI服务异常 -public struct OpenAIException: Error { - let message: String - - public init(_ message: String) { - self.message = message - } - - public var localizedDescription: String { - return "OpenAIException: \(message)" - } -} - -/// 流式回调协议 -public protocol StreamCallback { - func onToken(_ token: String) - func onComplete() - func onError(_ error: Error) - func onFunctionCall(_ functionCall: [String: Any]) - func onFunctionCallResult(_ functionCall: [String: Any], _ functionCallResult: [String: Any]) -} - -/// 工具调用信息 -private class ToolCallInfo { - var id: String = "" - var name: String = "" - var arguments: String = "" - - func isValid() -> Bool { - return !id.isEmpty && !name.isEmpty - } -} - -/// OpenAI服务的原生实现 -public class OpenAIService: NSObject { - private let tag = "OpenAIService" - - // 日志对象 - private let logger = OSLog(subsystem: "com.yunqiinnovation.open_ai_service", category: "OpenAIService") - - // MARK: - 属性 - private var baseUrl = "" - private var apiKey = "" - private var model = "" - private var visionModel = "doubao-1-5-vision-pro-32k-250115" - private var isInitialized = false - - // MCP客户端 - private var mcpClient: MCPClient? - private var isMcpInitializedFlag = false - - // 流式请求相关 - private var currentStreamTask: URLSessionDataTask? - private var currentCallback: StreamCallback? - private var currentMessages: [[String: Any]] = [] - private var responseBuffer = Data() - private var toolCalls: [Int: ToolCallInfo] = [:] - private var isCanceled = false - - // 优化:添加行缓冲区,避免频繁字符串转换 - private var lineBuffer = Data() - private let newlineData = "\n".data(using: .utf8)! - - // 优化:添加重连机制 - private var retryCount = 0 - private let maxRetryCount = 3 - private var retryDelay: TimeInterval = 1.0 - - // 优化:添加性能监控 - private var streamStartTime: Date? - private var tokenCount = 0 - private var bytesReceived = 0 - - // URL会话 - private lazy var urlSession: URLSession = { - let config = URLSessionConfiguration.default - config.timeoutIntervalForRequest = 30 - config.timeoutIntervalForResource = 30 - return URLSession(configuration: config) - }() - - // 流式会话(使用 delegate) - private lazy var streamSession: URLSession = { - let config = URLSessionConfiguration.default - config.timeoutIntervalForRequest = 30 - config.timeoutIntervalForResource = 30 - - // 优化:减少网络缓存,避免内存压力 - config.urlCache = nil - config.requestCachePolicy = .reloadIgnoringLocalCacheData - - // 优化:限制并发连接数 - config.httpMaximumConnectionsPerHost = 2 - - // 优化:针对后台数据处理的线程配置 - let delegateQueue = OperationQueue() - // 使用utility QoS,平衡性能和功耗,不与UI线程竞争 - delegateQueue.qualityOfService = .utility - delegateQueue.maxConcurrentOperationCount = 1 - delegateQueue.name = "OpenAIService.StreamDelegate" - - return URLSession(configuration: config, delegate: self, delegateQueue: delegateQueue) - }() - - // MARK: - 初始化 - public override init() { - super.init() - } - - // MARK: - 公共方法 - - /// 初始化OpenAI服务 - public func initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String) -> Bool { - self.apiKey = apiKey - if !baseUrl.isEmpty { - self.baseUrl = baseUrl - } - if !model.isEmpty { - self.model = model - } - - // 初始化MCP客户端 - if mcpClient == nil { - mcpClient = MCPClient() - } - _ = initializeMcpClient(serverUrl: mcpServer) - - isInitialized = !apiKey.isEmpty - return isInitialized - } - - /// 创建用户消息 - public func createUserMessage(content: String) -> [String: Any] { - return [ - "role": "user", - "content": content - ] - } - - /// 创建助手消息 - public func createAssistantMessage(content: String) -> [String: Any] { - return [ - "role": "assistant", - "content": content - ] - } - - /// 创建系统消息 - public func createSystemMessage(content: String) -> [String: Any] { - return [ - "role": "system", - "content": content - ] - } - - /// 创建带图片的用户消息 - public func createUserMessageWithImage(text: String, imageBase64: String) -> [String: Any] { - var contentArray: [[String: Any]] = [] - - // 添加文本部分 - if !text.isEmpty { - contentArray.append([ - "type": "text", - "text": text - ]) - } - - // 添加图片部分 - contentArray.append([ - "type": "image_url", - "image_url": [ - "url": "data:image/jpeg;base64,\(imageBase64)" - ] - ]) - - return [ - "role": "user", - "content": contentArray - ] - } - - /// 发送消息(非流式输出) - public func sendMessage(messages: [[String: Any]]) throws -> String { - guard isInitialized && !apiKey.isEmpty else { - throw OpenAIException("OpenAI服务未初始化") - } - - // 检查是否包含图片,决定使用哪个模型 - var currentModel = model - if let lastMessage = messages.last, - let content = lastMessage["content"] as? String, - content.contains("image_url") { - currentModel = visionModel - } - - // 构建请求体 - var requestBody: [String: Any] = [ - "model": currentModel, - "messages": messages, - "temperature": 0.7, - "max_tokens": 2000, - "stream": false - ] - - // 添加工具列表 - if let tools = mcpClient?.getToolMaps(), !tools.isEmpty { - requestBody["tools"] = tools - } - - // 创建请求 - guard let url = URL(string: baseUrl), - let jsonData = try? JSONSerialization.data(withJSONObject: requestBody) else { - throw OpenAIException("创建请求失败") - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") - request.httpBody = jsonData - - // 同步请求 - let semaphore = DispatchSemaphore(value: 0) - var result: String = "" - var error: Error? - - let task = urlSession.dataTask(with: request) { data, response, taskError in - defer { semaphore.signal() } - - if let taskError = taskError { - error = OpenAIException("请求失败: \(taskError.localizedDescription)") - return - } - - guard let httpResponse = response as? HTTPURLResponse else { - error = OpenAIException("无效响应") - return - } - - guard httpResponse.statusCode == 200 else { - error = OpenAIException("API调用失败: \(httpResponse.statusCode)") - return - } - - guard let data = data, - let jsonResponse = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - error = OpenAIException("响应解析失败") - return - } - - // 检查是否有函数调用 - if let choices = jsonResponse["choices"] as? [[String: Any]], - let choice = choices.first, - let message = choice["message"] as? [String: Any] { - - // 检查是否有工具调用 - if let toolCalls = message["tool_calls"] as? [[String: Any]], - let toolCall = toolCalls.first, - let function = toolCall["function"] as? [String: Any], - let name = function["name"] as? String, - let arguments = function["arguments"] as? String, - let id = toolCall["id"] as? String { - - let functionCallJson: [String: Any] = [ - "name": name, - "arguments": arguments, - "id": id - ] - - if let jsonData = try? JSONSerialization.data(withJSONObject: functionCallJson), - let jsonString = String(data: jsonData, encoding: .utf8) { - result = jsonString - } else { - error = OpenAIException("函数调用序列化失败") - } - return - } - - // 如果没有工具调用,返回消息内容 - if let content = message["content"] as? String { - result = content - return - } - } - - error = OpenAIException("无效的响应格式") - } - - task.resume() - semaphore.wait() - - if let error = error { - throw error - } - - return result - } - - /// 发送消息(流式输出) - public func sendMessageStream(messages: [[String: Any]], callback: StreamCallback) { - guard isInitialized && !apiKey.isEmpty else { - callback.onError(OpenAIException("OpenAI服务未初始化")) - return - } - - // 重置状态 - isCanceled = false - currentCallback = callback - currentMessages = messages - responseBuffer = Data() - toolCalls = [:] - - // 优化:初始化性能监控 - streamStartTime = Date() - tokenCount = 0 - bytesReceived = 0 - - // 检查是否包含图片,决定使用哪个模型 - var currentModel = model - if let lastMessage = messages.last, - let content = lastMessage["content"] as? String, - content.contains("image_url") { - currentModel = visionModel - } - - // 构建请求体 - var requestBody: [String: Any] = [ - "model": currentModel, - "messages": messages, - "temperature": 0.7, - "max_tokens": 2000, - "stream": true - ] - - // 添加工具列表 - if let tools = mcpClient?.getToolMaps(), !tools.isEmpty { - requestBody["tools"] = tools - } - - // 创建请求 - guard let url = URL(string: baseUrl), - let jsonData = try? JSONSerialization.data(withJSONObject: requestBody) else { - callback.onError(OpenAIException("创建请求失败")) - return - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") - request.setValue("text/event-stream", forHTTPHeaderField: "Accept") - request.setValue("cache-control", forHTTPHeaderField: "no-cache") - request.httpBody = jsonData - - os_log("开始流式请求: %{public}@", log: logger, type: .info, url.absoluteString) - - // 使用 delegate 模式的 URLSession 创建任务 - let task = streamSession.dataTask(with: request) - currentStreamTask = task - task.resume() - } - - /// 处理缓冲区数据 - private func processBufferedData(callback: StreamCallback, isComplete: Bool = false) { - // 优化:使用流式解析,避免频繁的字符串转换 - processStreamData(callback: callback, isComplete: isComplete) - } - - /// 优化的流式数据处理 - private func processStreamData(callback: StreamCallback, isComplete: Bool = false) { - lineBuffer.append(responseBuffer) - responseBuffer = Data() - - var searchRange = lineBuffer.startIndex.. Bool { - guard let firstToolCall = toolCalls.values.first, firstToolCall.isValid() else { - return false - } - - // 创建函数调用字典 - let functionCall: [String: Any] = [ - "name": firstToolCall.name, - "arguments": firstToolCall.arguments, - "id": firstToolCall.id - ] - - os_log("工具调用: id=%{public}@, name=%{public}@, arguments=%{public}@", log: logger, type: .info, firstToolCall.id, firstToolCall.name, firstToolCall.arguments) - - // 通知上层工具调用事件 - callback.onFunctionCall(functionCall) - - // 在后台队列处理工具调用 - Task { - do { - if !self.isCanceled { - // 调用工具 - let result: [String: Any] - do { - let args = try self.parseJsonArguments(firstToolCall.arguments) - result = try await self.mcpClient?.callTool(name: firstToolCall.name, arguments: args) ?? ["context": "工具调用失败"] - } catch { - os_log("工具调用错误: %{public}@", log: logger, type: .error, error.localizedDescription) - result = ["context": "工具调用失败: \(error.localizedDescription)"] - } - - if !self.isCanceled { - // 处理结果 - callback.onFunctionCallResult(functionCall, result) - - let context = result["context"] as? String ?? "工具调用失败" - os_log("MCP工具调用完成: name=%{public}@, context=%{public}@", log: logger, type: .info, firstToolCall.name, context) - - // 将结果发送回OpenAI继续对话 - self.sendFunctionCallResult( - messages: self.currentMessages, - functionCall: functionCall, - functionResult: context, - callback: callback - ) - } - } - } catch { - if !self.isCanceled { - os_log("处理工具调用异常: %{public}@", log: logger, type: .error, error.localizedDescription) - let errorMessage = "工具调用处理失败: \(error.localizedDescription)" - self.sendFunctionCallResult( - messages: self.currentMessages, - functionCall: functionCall, - functionResult: errorMessage, - callback: callback - ) - } - } - } - - return true - } - - /// 发送函数调用结果 - public func sendFunctionCallResult(messages: [[String: Any]], functionCall: [String: Any], functionResult: String, callback: StreamCallback) { - if isCanceled { - os_log("请求已取消,不发送函数调用结果", log: logger, type: .info) - return - } - - var fullMessages = messages - - // 添加函数调用消息 - let callId = functionCall["id"] as? String ?? "call_\(Int(Date().timeIntervalSince1970))" - fullMessages.append([ - "role": "assistant", - "content": "", - "tool_calls": [[ - "id": callId, - "type": "function", - "function": [ - "name": functionCall["name"] as? String ?? "", - "arguments": functionCall["arguments"] as? String ?? "" - ] - ]] - ]) - - // 添加函数调用结果 - fullMessages.append([ - "role": "tool", - "content": functionResult, - "tool_call_id": callId - ]) - - // 发送完整对话 - sendMessageStream(messages: fullMessages, callback: callback) - } - - /// 取消当前流式请求 - public func cancelCurrentStream() -> Bool { - isCanceled = true - currentStreamTask?.cancel() - - // 优化:更彻底的状态清理 - cleanupStreamState() - - os_log("已取消当前流式请求", log: logger, type: .info) - return true - } - - /// 优化:统一的状态清理方法 - private func cleanupStreamState() { - currentStreamTask = nil - currentCallback = nil - currentMessages.removeAll() - responseBuffer = Data() - lineBuffer = Data() - toolCalls.removeAll() - retryCount = 0 - - // 优化:重置性能监控变量 - streamStartTime = nil - tokenCount = 0 - bytesReceived = 0 - } - - /// 取消所有操作并释放资源 - public func cancelAll() { - _ = cancelCurrentStream() - } - - // MARK: - MCP相关方法 - - /// 初始化MCP客户端 - public func initializeMcpClient(serverUrl: String) -> Bool { - if mcpClient != nil { - mcpClient?.close() - } - - mcpClient = MCPClient() - - Task { - do { - let result = try await mcpClient?.connectToSSE(mcpConfigJson: serverUrl) ?? false - isMcpInitializedFlag = result - os_log("MCP客户端初始化%{public}@", log: logger, type: .info, result ? "成功" : "失败") - } catch { - os_log("MCP客户端初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription) - isMcpInitializedFlag = false - } - } - - return true // 立即返回,实际连接在后台进行 - } - - /// MCP客户端是否已初始化 - public func isMcpInitialized() -> Bool { - return isMcpInitializedFlag && mcpClient?.isConnected() == true - } - - /// 关闭MCP客户端 - public func closeMcpClient() { - mcpClient?.close() - mcpClient = nil - isMcpInitializedFlag = false - } - - /// 注册函数 - public func registerFunction(name: String, description: String, parameters: [String: Any]) -> Bool { - guard let mcpClient = mcpClient else { - os_log("MCP客户端未初始化", log: logger, type: .error) - return false - } - - // 创建函数处理器 - let handler = LocalFunctionHandler(name: name) - - // 注册本地函数 - return mcpClient.registerLocalFunction( - name: name, - description: description, - parameters: parameters, - handler: handler - ) - } - - /// 处理MCP工具调用 - public func handleMcpToolCall(functionCallJson: String) async throws -> String { - guard let mcpClient = mcpClient, isMcpInitializedFlag else { - return "MCP客户端未初始化" - } - - guard let jsonData = functionCallJson.data(using: .utf8), - let functionCall = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any], - let name = functionCall["name"] as? String, - let argumentsJson = functionCall["arguments"] as? String else { - throw OpenAIException("解析函数调用失败") - } - - let arguments = try parseJsonArguments(argumentsJson) - let result = try await mcpClient.callTool(name: name, arguments: arguments) - - return result["context"] as? String ?? "无法处理MCP工具调用" - } - - // MARK: - 工具方法 - - /// 解析JSON参数 - private func parseJsonArguments(_ argumentsJson: String) throws -> [String: Any] { - guard let data = argumentsJson.data(using: .utf8), - let arguments = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - return [:] - } - return arguments - } - - /// 检查字符串是否为有效的JSON - private func isValidJson(jsonString: String) -> Bool { - guard !jsonString.isEmpty, - jsonString.hasPrefix("{"), - jsonString.hasSuffix("}") else { - return false - } - - var braceCount = 0 - var insideQuotes = false - var escapeNext = false - - for char in jsonString { - if escapeNext { - escapeNext = false - continue - } - - switch char { - case "\\": - escapeNext = true - case "\"": - if !escapeNext { - insideQuotes.toggle() - } - case "{": - if !insideQuotes { - braceCount += 1 - } - case "}": - if !insideQuotes { - braceCount -= 1 - } - default: - break - } - - // 如果括号不匹配(闭合太多),提前返回 - if braceCount < 0 { - return false - } - } - - // 所有引号都应该闭合,所有括号也应该匹配 - return braceCount == 0 && !insideQuotes - } - - /// 将文件转换为Base64字符串 - public func fileToBase64(filePath: String, maxSizeKB: Int = 20480) -> String? { - guard let image = UIImage(contentsOfFile: filePath) else { - os_log("文件不存在或无法读取: %{public}@", log: logger, type: .error, filePath) - return nil - } - - var processedImage = image - - // 检查图片尺寸,限制最大为1024*1024 - let maxDimension: CGFloat = 1024 - if image.size.width > maxDimension || image.size.height > maxDimension { - let ratio = min(maxDimension / image.size.width, maxDimension / image.size.height) - let newSize = CGSize(width: image.size.width * ratio, height: image.size.height * ratio) - - UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0) - image.draw(in: CGRect(origin: .zero, size: newSize)) - processedImage = UIGraphicsGetImageFromCurrentImageContext() ?? image - UIGraphicsEndImageContext() - } - - // 压缩图片 - guard let imageData = processedImage.jpegData(compressionQuality: 0.8) else { - return nil - } - - return imageData.base64EncodedString() - } -} - -// MARK: - URLSessionDataDelegate Extension -extension OpenAIService: URLSessionDataDelegate { - - /// 接收到响应头 - public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { - guard let httpResponse = response as? HTTPURLResponse else { - if !isCanceled { - // 直接回调,由调用方处理线程切换 - currentCallback?.onError(OpenAIException("无效响应")) - } - completionHandler(.cancel) - return - } - - guard httpResponse.statusCode == 200 else { - if !isCanceled { - // 直接回调,由调用方处理线程切换 - currentCallback?.onError(OpenAIException("API调用失败: \(httpResponse.statusCode)")) - } - completionHandler(.cancel) - return - } - - os_log("收到响应头,状态码: %d", log: logger, type: .info, httpResponse.statusCode) - completionHandler(.allow) - } - - /// 实时接收数据块 - public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - guard !isCanceled, let callback = currentCallback else { return } - - // 优化:更新性能指标 - bytesReceived += data.count - - // 将新数据添加到缓冲区 - responseBuffer.append(data) - - // 处理缓冲区中的完整行 - processBufferedData(callback: callback) - } - - /// 请求完成 - public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didCompleteWithError error: Error?) { - defer { - // 优化:使用统一的清理方法 - cleanupStreamState() - } - - guard !isCanceled else { - os_log("流式请求已取消", log: logger, type: .info) - return - } - - if let error = error { - os_log("流式请求错误: %{public}@", log: logger, type: .error, error.localizedDescription) - // 直接回调,由调用方处理线程切换 - currentCallback?.onError(OpenAIException("请求失败: \(error.localizedDescription)")) - return - } - - // 处理剩余的缓冲区数据 - if let callback = currentCallback { - processBufferedData(callback: callback, isComplete: true) - } - - // 优化:输出性能统计 - if let startTime = streamStartTime { - let duration = Date().timeIntervalSince(startTime) - let tokensPerSecond = duration > 0 ? Double(tokenCount) / duration : 0 - os_log("流式请求性能统计 - 耗时: %.2fs, tokens: %d, 字节: %d, tokens/s: %.2f", - log: logger, type: .info, duration, tokenCount, bytesReceived, tokensPerSecond) - } - - os_log("流式请求完成", log: logger, type: .info) - } -} - -/// 本地函数处理器 -private class LocalFunctionHandler: FunctionHandler { - private let functionName: String - - init(name: String) { - self.functionName = name - } - - func handle(arguments: [String: Any]) async throws -> String { - // 由于本地函数的实际处理是在Flutter端完成的 - // 这里只需返回一个标记,表示该函数是本地函数 - return "LOCAL_FUNCTION:\(functionName)" - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/OpenAIServicePlugin.swift b/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/OpenAIServicePlugin.swift deleted file mode 100644 index bf99d940..00000000 --- a/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/OpenAIServicePlugin.swift +++ /dev/null @@ -1,340 +0,0 @@ -import Flutter -import UIKit -import Foundation - -/// OpenAI服务插件主类 -public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler { - - // MARK: - 常量 - private static let methodChannelName = "com.yunqiinnovation.open_ai_service/methods" - private static let eventChannelName = "com.yunqiinnovation.open_ai_service/events" - - // MARK: - 属性 - private var methodChannel: FlutterMethodChannel? - private var eventChannel: FlutterEventChannel? - private var eventSink: FlutterEventSink? - private var openAIService: OpenAIService? - - // MARK: - 插件注册 - public static func register(with registrar: FlutterPluginRegistrar) { - let instance = OpenAIServicePlugin() - - // 初始化方法通道 - let methodChannel = FlutterMethodChannel( - name: methodChannelName, - binaryMessenger: registrar.messenger() - ) - instance.methodChannel = methodChannel - registrar.addMethodCallDelegate(instance, channel: methodChannel) - - // 初始化事件通道 - let eventChannel = FlutterEventChannel( - name: eventChannelName, - binaryMessenger: registrar.messenger() - ) - instance.eventChannel = eventChannel - eventChannel.setStreamHandler(instance) - - // 初始化OpenAI服务 - instance.openAIService = OpenAIService() - } - - // MARK: - FlutterPlugin - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - guard let service = openAIService else { - result(FlutterError(code: "SERVICE_NOT_INITIALIZED", - message: "OpenAI服务未初始化", - details: nil)) - return - } - - switch call.method { - case "initialize": - handleInitialize(call: call, result: result, service: service) - - case "registerFunction": - handleRegisterFunction(call: call, result: result, service: service) - - case "sendMessage": - handleSendMessage(call: call, result: result, service: service) - - case "sendMessageStream": - handleSendMessageStream(call: call, result: result, service: service) - - case "sendFunctionCallResult": - handleSendFunctionCallResult(call: call, result: result, service: service) - - case "createUserMessage": - handleCreateUserMessage(call: call, result: result, service: service) - - case "createAssistantMessage": - handleCreateAssistantMessage(call: call, result: result, service: service) - - case "initializeMcpClient": - handleInitializeMcpClient(call: call, result: result, service: service) - - case "isMcpInitialized": - handleIsMcpInitialized(call: call, result: result, service: service) - - case "closeMcpClient": - handleCloseMcpClient(call: call, result: result, service: service) - - case "handleMcpToolCall": - handleMcpToolCall(call: call, result: result, service: service) - - default: - result(FlutterMethodNotImplemented) - } - } - - // MARK: - FlutterStreamHandler - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - self.eventSink = events - return nil - } - - public func onCancel(withArguments arguments: Any?) -> FlutterError? { - self.eventSink = nil - return nil - } - - // MARK: - 方法处理 - private func handleInitialize(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let apiKey = args["apiKey"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少必要参数", - details: nil)) - return - } - - let baseUrl = args["baseUrl"] as? String ?? "" - let model = args["model"] as? String ?? "" - let mcpServer = args["mcpServer"] as? String ?? "" - - let success = service.initialize( - apiKey: apiKey, - baseUrl: baseUrl, - model: model, - mcpServer: mcpServer - ) - - result(success) - } - - private func handleRegisterFunction(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let name = args["name"] as? String, - let description = args["description"] as? String, - let parameters = args["parameters"] as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少必要参数", - details: nil)) - return - } - - let success = service.registerFunction(name: name, description: description, parameters: parameters) - result(success) - } - - private func handleSendMessage(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let messagesArray = args["messages"] as? [[String: Any]] else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少消息参数", - details: nil)) - return - } - - do { - let response = try service.sendMessage(messages: messagesArray) - result(response) - } catch { - result(FlutterError(code: "OPENAI_ERROR", - message: error.localizedDescription, - details: nil)) - } - } - - private func handleSendMessageStream(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let messagesArray = args["messages"] as? [[String: Any]] else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少消息参数", - details: nil)) - return - } - - guard let eventSink = self.eventSink else { - result(FlutterError(code: "NO_EVENT_SINK", - message: "没有可用的事件流接收器", - details: nil)) - return - } - - // 创建回调处理器 - let callback = StreamCallbackImpl(eventSink: eventSink) - - // 发送流式消息 - service.sendMessageStream(messages: messagesArray, callback: callback) - - result(true) - } - - private func handleSendFunctionCallResult(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let messagesArray = args["messages"] as? [[String: Any]], - let functionCall = args["functionCall"] as? [String: Any], - let functionResult = args["functionResult"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少必要参数", - details: nil)) - return - } - - guard let eventSink = self.eventSink else { - result(FlutterError(code: "NO_EVENT_SINK", - message: "没有可用的事件流接收器", - details: nil)) - return - } - - // 创建回调处理器 - let callback = StreamCallbackImpl(eventSink: eventSink) - - // 发送函数调用结果 - service.sendFunctionCallResult( - messages: messagesArray, - functionCall: functionCall, - functionResult: functionResult, - callback: callback - ) - - result(true) - } - - private func handleCreateUserMessage(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let content = args["content"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少内容参数", - details: nil)) - return - } - - let message = service.createUserMessage(content: content) - result(message) - } - - private func handleCreateAssistantMessage(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let content = args["content"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少内容参数", - details: nil)) - return - } - - let message = service.createAssistantMessage(content: content) - result(message) - } - - private func handleInitializeMcpClient(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let serverUrl = args["serverUrl"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少服务器URL参数", - details: nil)) - return - } - - let success = service.initializeMcpClient(serverUrl: serverUrl) - result(success) - } - - private func handleIsMcpInitialized(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - let isInitialized = service.isMcpInitialized() - result(isInitialized) - } - - private func handleCloseMcpClient(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - service.closeMcpClient() - result(true) - } - - private func handleMcpToolCall(call: FlutterMethodCall, result: @escaping FlutterResult, service: OpenAIService) { - guard let args = call.arguments as? [String: Any], - let functionCallJson = args["functionCall"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "缺少函数调用参数", - details: nil)) - return - } - - Task { - do { - let toolResult = try await service.handleMcpToolCall(functionCallJson: functionCallJson) - DispatchQueue.main.async { - result(toolResult) - } - } catch { - DispatchQueue.main.async { - result(FlutterError(code: "MCP_TOOL_ERROR", - message: "处理MCP工具调用失败: \(error.localizedDescription)", - details: nil)) - } - } - } - } -} - -// MARK: - 流式回调实现 -private class StreamCallbackImpl: StreamCallback { - private let eventSink: FlutterEventSink - - init(eventSink: @escaping FlutterEventSink) { - self.eventSink = eventSink - } - - func onToken(_ token: String) { - DispatchQueue.main.async { - self.eventSink(["type": "token", "content": token]) - } - } - - func onComplete() { - DispatchQueue.main.async { - self.eventSink(["type": "complete"]) - } - } - - func onError(_ error: Error) { - DispatchQueue.main.async { - self.eventSink(["type": "error", "content": error.localizedDescription]) - } - } - - func onFunctionCall(_ functionCall: [String: Any]) { - DispatchQueue.main.async { - if let jsonData = try? JSONSerialization.data(withJSONObject: functionCall), - let jsonString = String(data: jsonData, encoding: .utf8) { - self.eventSink(["type": "function_call", "content": jsonString]) - } - } - } - - func onFunctionCallResult(_ functionCall: [String: Any], _ functionCallResult: [String: Any]) { - DispatchQueue.main.async { - if let callData = try? JSONSerialization.data(withJSONObject: functionCall), - let callString = String(data: callData, encoding: .utf8), - let resultData = try? JSONSerialization.data(withJSONObject: functionCallResult), - let resultString = String(data: resultData, encoding: .utf8) { - self.eventSink([ - "type": "function_call_result", - "function_call": callString, - "result": resultString - ]) - } - } - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/SystemFunctionHandler.swift b/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/SystemFunctionHandler.swift deleted file mode 100644 index 996a9385..00000000 --- a/apps/client/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/SystemFunctionHandler.swift +++ /dev/null @@ -1,701 +0,0 @@ -import Foundation -import UIKit -import EventKit -import CoreLocation -import MediaPlayer -import AVFoundation - -/// 操作结果 -public struct Result { - let success: Bool - let message: String - - public init(success: Bool, message: String) { - self.success = success - self.message = message - } -} - -/// 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能 -public class SystemFunctionHandler: NSObject { - private let tag = "SystemFunctionHandler" - - // 位置管理器 - private var locationManager: CLLocationManager? - private var locationCompletion: ((Result) -> Void)? - - // 事件存储 - private var eventStore: EKEventStore? - - public override init() { - super.init() - setupLocationManager() - setupEventStore() - } - - /// 设置位置管理器 - private func setupLocationManager() { - locationManager = CLLocationManager() - locationManager?.delegate = self - locationManager?.desiredAccuracy = kCLLocationAccuracyBest - } - - /// 设置事件存储 - private func setupEventStore() { - eventStore = EKEventStore() - } - - /// 在MCPClient中注册所有系统功能 - /// - Parameter client: MCPClient实例 - public func registerAllFunctions(client: MCPClient) { - do { - // 注册退出交互函数 - registerExitInteractionFunction(client: client) - - // 注册翻译模式函数 - registerTranslationModeFunction(client: client) - - // 注册设置提醒函数 - registerSetReminderFunction(client: client) - - // 注册获取当前时间函数 - registerGetCurrentTimeFunction(client: client) - - // 注册获取当前位置函数 - registerGetCurrentLocationFunction(client: client) - - // 注册媒体控制函数 - registerMediaPlayFunction(client: client) - registerMediaPauseFunction(client: client) - registerMediaPreviousFunction(client: client) - registerMediaNextFunction(client: client) - - // 注册打开录音机功能 - registerOpenRecorderFunction(client: client) - - print("\(tag) 所有系统功能注册完成") - } catch { - print("\(tag) 注册系统功能失败: \(error.localizedDescription)") - } - } - - // MARK: - 函数注册 - - /// 注册退出交互函数 - private func registerExitInteractionFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = ExitInteractionHandler() - _ = client.registerLocalFunction( - name: "exit_interaction", - description: "结束当前交互", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册退出交互功能") - } - - /// 注册翻译模式函数 - private func registerTranslationModeFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = TranslationModeHandler() - _ = client.registerLocalFunction( - name: "enter_translation_mode", - description: "用户请求进入实时翻译模式时,启动实时翻译功能", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册翻译模式功能") - } - - /// 注册设置提醒函数 - private func registerSetReminderFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [ - "title": [ - "type": "string", - "description": "提醒标题" - ], - "content": [ - "type": "string", - "description": "提醒内容" - ], - "time": [ - "type": "string", - "description": "提醒时间,格式为'yyyy-MM-dd HH:mm',如'2023-12-31 14:30'" - ] - ], - "required": ["title", "time"] - ] - - let handler = SetReminderHandler(eventStore: eventStore) - _ = client.registerLocalFunction( - name: "set_reminder", - description: "设置提醒事项", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册设置提醒功能") - } - - /// 注册获取当前时间函数 - private func registerGetCurrentTimeFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [ - "format": [ - "type": "string", - "enum": ["short", "date", "time", "full", "iso"], - "description": "时间输出粒度,仅可使用枚举值:short(仅时分)、date(仅日期)、time(时分秒)、full(日期+星期+时间)、iso(机器可读)。禁止传入 SimpleDateFormat/strftime 等格式串。" - ] - ], - "required": [] - ] - - let handler = GetCurrentTimeHandler() - _ = client.registerLocalFunction( - name: "get_current_time", - description: "获取当前日期和时间,返回值已按当前语言本地化并对TTS友好(不含冒号等易被误读的符号)。播报或回复给用户时必须原样使用 result 字段的文本,不要自行改写为 HH:mm:ss 等格式。", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册获取当前时间功能") - } - - /// 注册获取当前位置函数 - private func registerGetCurrentLocationFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = GetCurrentLocationHandler(systemHandler: self) - _ = client.registerLocalFunction( - name: "get_current_location", - description: "获取当前地理位置", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册获取当前位置功能") - } - - /// 注册媒体播放功能 - private func registerMediaPlayFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = MediaPlayHandler() - _ = client.registerLocalFunction( - name: "media_play", - description: "播放媒体", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册媒体播放功能") - } - - /// 注册媒体暂停功能 - private func registerMediaPauseFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = MediaPauseHandler() - _ = client.registerLocalFunction( - name: "media_pause", - description: "暂停媒体播放", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册媒体暂停功能") - } - - /// 注册媒体上一首功能 - private func registerMediaPreviousFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = MediaPreviousHandler() - _ = client.registerLocalFunction( - name: "media_previous", - description: "播放上一首", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册媒体上一首功能") - } - - /// 注册媒体下一首功能 - private func registerMediaNextFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = MediaNextHandler() - _ = client.registerLocalFunction( - name: "media_next", - description: "播放下一首", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册媒体下一首功能") - } - - /// 注册打开录音机功能 - private func registerOpenRecorderFunction(client: MCPClient) { - let parameters: [String: Any] = [ - "type": "object", - "properties": [:], - "required": [] - ] - - let handler = OpenRecorderHandler() - _ = client.registerLocalFunction( - name: "open_recorder", - description: "打开系统录音机并开始录音", - parameters: parameters, - handler: handler - ) - print("\(tag) 已注册打开录音机功能") - } - - // MARK: - 内部方法 - - /// 获取当前位置 - internal func getCurrentLocation() async -> Result { - return await withCheckedContinuation { continuation in - locationCompletion = { result in - continuation.resume(returning: result) - } - - // 设置15秒超时 - DispatchQueue.global().asyncAfter(deadline: .now() + 15.0) { [weak self] in - if self?.locationCompletion != nil { - self?.locationCompletion?(Result(success: false, message: "位置请求超时")) - self?.locationCompletion = nil - } - } - - // 检查位置服务是否可用 - guard CLLocationManager.locationServicesEnabled() else { - continuation.resume(returning: Result(success: false, message: "位置服务未启用")) - return - } - - // 确保locationManager已初始化 - guard let locationManager = locationManager else { - continuation.resume(returning: Result(success: false, message: "位置管理器未初始化")) - return - } - - // 检查位置权限 - 兼容iOS 13.0+ - let authStatus: CLAuthorizationStatus - if #available(iOS 14.0, *) { - authStatus = locationManager.authorizationStatus - } else { - authStatus = CLLocationManager.authorizationStatus() - } - - switch authStatus { - case .notDetermined: - // 请求权限,权限结果会在 didChangeAuthorization 回调中处理 - locationManager.requestWhenInUseAuthorization() - // 不要在这里 return,让权限回调来处理后续逻辑 - case .denied, .restricted: - continuation.resume(returning: Result(success: false, message: "位置权限被拒绝")) - return - case .authorizedWhenInUse, .authorizedAlways: - // 已有权限,直接请求位置 - locationManager.requestLocation() - @unknown default: - continuation.resume(returning: Result(success: false, message: "未知的位置权限状态")) - return - } - } - } -} - -// MARK: - CLLocationManagerDelegate -extension SystemFunctionHandler: CLLocationManagerDelegate { - public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - guard let location = locations.last else { - locationCompletion?(Result(success: false, message: "无法获取位置信息")) - return - } - - let latitude = location.coordinate.latitude - let longitude = location.coordinate.longitude - let message = "当前位置:纬度 \(latitude),经度 \(longitude)" - - locationCompletion?(Result(success: true, message: message)) - locationCompletion = nil - } - - public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - locationCompletion?(Result(success: false, message: "获取位置失败:\(error.localizedDescription)")) - locationCompletion = nil - } - - public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { - // 只有在有位置请求等待时才处理权限变化 - guard locationCompletion != nil else { - return - } - - if status == .authorizedWhenInUse || status == .authorizedAlways { - manager.requestLocation() - } else if status == .denied || status == .restricted { - locationCompletion?(Result(success: false, message: "位置权限被拒绝")) - locationCompletion = nil - } - } -} - -// MARK: - 功能处理器实现 - -/// 退出交互处理器 -private class ExitInteractionHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - // 发送退出交互通知 - NotificationCenter.default.post(name: NSNotification.Name("ExitInteraction"), object: nil) - return "{\"result\": \"已结束当前交互\"}" - } -} - -/// 翻译模式处理器 -private class TranslationModeHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - // 发送进入翻译模式通知 - NotificationCenter.default.post(name: NSNotification.Name("EnterTranslationMode"), object: nil) - return "{\"result\": \"已进入翻译模式\"}" - } -} - -/// 设置提醒处理器 -private class SetReminderHandler: FunctionHandler { - private let eventStore: EKEventStore? - - init(eventStore: EKEventStore?) { - self.eventStore = eventStore - } - - func handle(arguments: [String: Any]) async throws -> String { - guard let title = arguments["title"] as? String, - let timeString = arguments["time"] as? String else { - throw OpenAIException("缺少必要参数") - } - - let content = arguments["content"] as? String ?? "" - let result = await setReminder(title: title, content: content, time: timeString) - return "{\"result\": \"\(result.message)\"}" - } - - private func setReminder(title: String, content: String, time: String) async -> Result { - guard let eventStore = eventStore else { - return Result(success: false, message: "事件存储未初始化") - } - - // 请求日历权限 - let status = await requestCalendarAccess() - guard status == .authorized else { - return Result(success: false, message: "日历权限被拒绝") - } - - // 解析时间 - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd HH:mm" - guard let date = formatter.date(from: time) else { - return Result(success: false, message: "时间格式错误") - } - - // 创建提醒 - let reminder = EKReminder(eventStore: eventStore) - reminder.title = title - reminder.notes = content - reminder.calendar = eventStore.defaultCalendarForNewReminders() - - let alarm = EKAlarm(absoluteDate: date) - reminder.addAlarm(alarm) - - do { - try eventStore.save(reminder, commit: true) - return Result(success: true, message: "提醒设置成功") - } catch { - return Result(success: false, message: "设置提醒失败:\(error.localizedDescription)") - } - } - - private func requestCalendarAccess() async -> EKAuthorizationStatus { - return await withCheckedContinuation { continuation in - eventStore?.requestAccess(to: .reminder) { granted, error in - let status = EKEventStore.authorizationStatus(for: .reminder) - continuation.resume(returning: status) - } - } - } -} - -/// 获取当前时间处理器 -private class GetCurrentTimeHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let format = arguments["format"] as? String - let result = getCurrentTime(format: format) - return "{\"result\": \"\(result.message)\", \"time\": \"\(result.message)\"}" - } - - private func getCurrentTime(format: String?) -> Result { - let now = Date() - let lang = Locale.current.languageCode ?? "en" - - let timeStr: String - switch format?.lowercased() { - case "short": - timeStr = formatShortTime(now, lang: lang) - case "date": - timeStr = formatDate(now, lang: lang) - case "time": - timeStr = formatTime(now, lang: lang) - case "full": - timeStr = formatFull(now, lang: lang) - case "iso": - let f = DateFormatter() - f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - timeStr = f.string(from: now) - default: - timeStr = formatDefault(now, lang: lang) - } - - let resultTime: String - switch format?.lowercased() { - case "short", "time", "iso": - resultTime = timeStr - case "full": - resultTime = timeStr - default: - let weekFormatter = DateFormatter() - weekFormatter.dateFormat = "EEEE" - weekFormatter.locale = Locale.current - resultTime = "\(timeStr) \(weekFormatter.string(from: now))" - } - - return Result(success: true, message: resultTime) - } - - // TTS友好的时间格式,避免AI将":"分隔的时间误读为数字 - // CJK 使用单位数 m/s(去掉前导零),其他语言使用英文单词避免冒号 - private func formatShortTime(_ date: Date, lang: String) -> String { - let f = DateFormatter() - switch lang { - case "zh": f.dateFormat = "H时m分"; f.locale = Locale(identifier: "zh_CN"); return f.string(from: date) - case "ja": f.dateFormat = "H時m分"; f.locale = Locale(identifier: "ja_JP"); return f.string(from: date) - case "ko": f.dateFormat = "H시 m분"; f.locale = Locale(identifier: "ko_KR"); return f.string(from: date) - default: return formatTimeWestern(date, withSeconds: false) - } - } - - private func formatDate(_ date: Date, lang: String) -> String { - let f = DateFormatter() - switch lang { - case "zh": f.dateFormat = "yyyy年M月d日"; f.locale = Locale(identifier: "zh_CN") - case "ja": f.dateFormat = "yyyy年M月d日"; f.locale = Locale(identifier: "ja_JP") - case "ko": f.dateFormat = "yyyy년 M월 d일"; f.locale = Locale(identifier: "ko_KR") - default: f.dateFormat = "MMMM d, yyyy"; f.locale = Locale.current - } - return f.string(from: date) - } - - private func formatTime(_ date: Date, lang: String) -> String { - let f = DateFormatter() - switch lang { - case "zh": f.dateFormat = "H时m分s秒"; f.locale = Locale(identifier: "zh_CN"); return f.string(from: date) - case "ja": f.dateFormat = "H時m分s秒"; f.locale = Locale(identifier: "ja_JP"); return f.string(from: date) - case "ko": f.dateFormat = "H시 m분 s초"; f.locale = Locale(identifier: "ko_KR"); return f.string(from: date) - default: return formatTimeWestern(date, withSeconds: true) - } - } - - private func formatFull(_ date: Date, lang: String) -> String { - let f = DateFormatter() - switch lang { - case "zh": f.dateFormat = "yyyy年M月d日 EEEE H时m分s秒"; f.locale = Locale(identifier: "zh_CN"); return f.string(from: date) - case "ja": f.dateFormat = "yyyy年M月d日 EEEE H時m分s秒"; f.locale = Locale(identifier: "ja_JP"); return f.string(from: date) - case "ko": f.dateFormat = "yyyy년 M월 d일 EEEE H시 m분 s초"; f.locale = Locale(identifier: "ko_KR"); return f.string(from: date) - default: - f.dateFormat = "MMMM d, yyyy, EEEE, "; f.locale = Locale.current - return f.string(from: date) + formatTimeWestern(date, withSeconds: true) - } - } - - private func formatDefault(_ date: Date, lang: String) -> String { - let f = DateFormatter() - switch lang { - case "zh": f.dateFormat = "yyyy年M月d日 H时m分"; f.locale = Locale(identifier: "zh_CN"); return f.string(from: date) - case "ja": f.dateFormat = "yyyy年M月d日 H時m分"; f.locale = Locale(identifier: "ja_JP"); return f.string(from: date) - case "ko": f.dateFormat = "yyyy년 M월 d일 H시 m분"; f.locale = Locale(identifier: "ko_KR"); return f.string(from: date) - default: - f.dateFormat = "MMMM d, yyyy, "; f.locale = Locale.current - return f.string(from: date) + formatTimeWestern(date, withSeconds: false) - } - } - - /// 非 CJK 语言的时间格式:避免冒号,使用英文单词,对 TTS 更稳定 - private func formatTimeWestern(_ date: Date, withSeconds: Bool) -> String { - let cal = Calendar.current - let hour24 = cal.component(.hour, from: date) - let minute = cal.component(.minute, from: date) - let second = cal.component(.second, from: date) - let h12Raw = hour24 % 12 - let h12 = h12Raw == 0 ? 12 : h12Raw - let ampm = hour24 < 12 ? "AM" : "PM" - - var parts: [String] = [] - parts.append("\(h12) \(h12 == 1 ? "hour" : "hours")") - if minute > 0 || withSeconds { - parts.append("\(minute) \(minute == 1 ? "minute" : "minutes")") - } - if withSeconds { - parts.append("\(second) \(second == 1 ? "second" : "seconds")") - } - parts.append(ampm) - return parts.joined(separator: " ") - } -} - -/// 获取当前位置处理器 -private class GetCurrentLocationHandler: FunctionHandler { - private weak var systemHandler: SystemFunctionHandler? - - init(systemHandler: SystemFunctionHandler) { - self.systemHandler = systemHandler - } - - func handle(arguments: [String: Any]) async throws -> String { - guard let systemHandler = systemHandler else { - throw OpenAIException("系统处理器未初始化") - } - - let result = await systemHandler.getCurrentLocation() - return "{\"result\": \"\(result.message)\", \"success\": \(result.success)}" - } -} - -/// 媒体播放处理器 -private class MediaPlayHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let result = controlMedia(action: "play") - return "{\"result\": \"\(result.message)\"}" - } - - private func controlMedia(action: String) -> Result { - let commandCenter = MPRemoteCommandCenter.shared() - - switch action { - case "play": - // 发送播放命令通知 - NotificationCenter.default.post(name: NSNotification.Name("MediaPlay"), object: nil) - return Result(success: true, message: "已开始播放媒体") - default: - return Result(success: false, message: "未知的媒体操作") - } - } -} - -/// 媒体暂停处理器 -private class MediaPauseHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let result = controlMedia(action: "pause") - return "{\"result\": \"\(result.message)\"}" - } - - private func controlMedia(action: String) -> Result { - switch action { - case "pause": - // 发送暂停命令通知 - NotificationCenter.default.post(name: NSNotification.Name("MediaPause"), object: nil) - return Result(success: true, message: "已暂停媒体播放") - default: - return Result(success: false, message: "未知的媒体操作") - } - } -} - -/// 媒体上一首处理器 -private class MediaPreviousHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let result = controlMedia(action: "previous") - return "{\"result\": \"\(result.message)\"}" - } - - private func controlMedia(action: String) -> Result { - switch action { - case "previous": - // 发送上一首命令通知 - NotificationCenter.default.post(name: NSNotification.Name("MediaPrevious"), object: nil) - return Result(success: true, message: "已切换到上一首") - default: - return Result(success: false, message: "未知的媒体操作") - } - } -} - -/// 媒体下一首处理器 -private class MediaNextHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let result = controlMedia(action: "next") - return "{\"result\": \"\(result.message)\"}" - } - - private func controlMedia(action: String) -> Result { - switch action { - case "next": - // 发送下一首命令通知 - NotificationCenter.default.post(name: NSNotification.Name("MediaNext"), object: nil) - return Result(success: true, message: "已切换到下一首") - default: - return Result(success: false, message: "未知的媒体操作") - } - } -} - -/// 打开录音机处理器 -private class OpenRecorderHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let result = openRecorder() - return "{\"result\": \"\(result.message)\"}" - } - - private func openRecorder() -> Result { - // 尝试打开语音备忘录应用 - if let url = URL(string: "voicememos://") { - if UIApplication.shared.canOpenURL(url) { - Task { - await UIApplication.shared.open(url) - } - return Result(success: true, message: "已打开语音备忘录") - } - } - - return Result(success: false, message: "无法打开录音机应用") - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/open_ai_service/lib/open_ai_service.dart b/apps/client/local_plugins/open_ai_service/lib/open_ai_service.dart deleted file mode 100644 index 89ada557..00000000 --- a/apps/client/local_plugins/open_ai_service/lib/open_ai_service.dart +++ /dev/null @@ -1,420 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/services.dart'; - -/// OpenAI服务异常 -class OpenAIException implements Exception { - final String message; - - OpenAIException(this.message); - - @override - String toString() => 'OpenAIException: $message'; -} - -/// OpenAI服务事件类型 -enum OpenAIEventType { - token, - complete, - error, - functionCall, -} - -/// OpenAI服务事件 -class OpenAIEvent { - final OpenAIEventType type; - final dynamic content; - final Map? meta; - - OpenAIEvent({required this.type, this.content, this.meta}); - - factory OpenAIEvent.fromMap(Map map) { - final typeStr = map['type'] as String; - final content = map['content']; - final rawMeta = map['meta'] as String?; - final meta = rawMeta != null ? jsonDecode(rawMeta) : null; - return OpenAIEvent( - type: _typeFromString(typeStr), - content: content, - meta: meta, - ); - } - - static OpenAIEventType _typeFromString(String typeStr) { - switch (typeStr) { - case 'token': - return OpenAIEventType.token; - case 'complete': - return OpenAIEventType.complete; - case 'error': - return OpenAIEventType.error; - case 'functionCall': - return OpenAIEventType.functionCall; - default: - throw ArgumentError('未知的事件类型: $typeStr'); - } - } -} - -/// OpenAI服务插件 -class OpenAIService { - static const MethodChannel _channel = - MethodChannel('com.yunqiinnovation.open_ai_service/methods'); - static const EventChannel _eventChannel = - EventChannel('com.yunqiinnovation.open_ai_service/events'); - - /// 事件流控制器 - StreamController? _eventStreamController; - - /// 事件流 - Stream? _eventStream; - - /// 获取事件流 - Stream get eventStream { - if (_eventStream == null) { - _eventStreamController = StreamController.broadcast(); - _eventStream = _eventStreamController!.stream; - - // 监听原生事件 - _eventChannel.receiveBroadcastStream().listen( - (dynamic event) { - // 收到事件 OpenAIEvent: $event - if (event is Map) { - final eventMap = Map.from(event); - final openAIEvent = OpenAIEvent.fromMap(eventMap); - _eventStreamController!.add(openAIEvent); - } - }, - onError: (error) { - _eventStreamController!.addError(OpenAIException('事件流错误: $error')); - }, - ); - } - - return _eventStream!; - } - - /// 初始化OpenAI服务 - /// - /// [apiKey] OpenAI API密钥 - /// [baseUrl] 可选,自定义API基础URL - /// [model] 可选,自定义使用的模型 - /// [mcpServer] 可选,MCP服务器地址配置 - Future initialize({ - required String apiKey, - String baseUrl = '', - String model = '', - String mcpServer = '', - }) async { - try { - final result = await _channel.invokeMethod( - 'initialize', - { - 'apiKey': apiKey, - 'baseUrl': baseUrl, - 'model': model, - 'mcpServer': mcpServer, - }, - ); - - return result ?? false; - } catch (e) { - throw OpenAIException('初始化失败: $e'); - } - } - - /// 注册函数 - /// - /// [name] 函数名称 - /// [description] 函数描述 - /// [parameters] 函数参数 - Future registerFunction({ - required String name, - required String description, - required Map parameters, - }) async { - try { - final result = await _channel.invokeMethod( - 'registerFunction', - { - 'name': name, - 'description': description, - 'parameters': parameters, - }, - ); - - return result ?? false; - } catch (e) { - throw OpenAIException('注册函数失败: $e'); - } - } - - /// 创建用户消息 - /// - /// [content] 消息内容 - Future> createUserMessage(String content) async { - try { - final result = await _channel.invokeMethod>( - 'createUserMessage', - {'content': content}, - ); - - if (result == null) { - throw OpenAIException('创建用户消息失败: 结果为空'); - } - - return Map.from(result); - } catch (e) { - throw OpenAIException('创建用户消息失败: $e'); - } - } - - /// 创建助手消息 - /// - /// [content] 消息内容 - Future> createAssistantMessage(String content) async { - try { - final result = await _channel.invokeMethod>( - 'createAssistantMessage', - {'content': content}, - ); - - if (result == null) { - throw OpenAIException('创建助手消息失败: 结果为空'); - } - - return Map.from(result); - } catch (e) { - throw OpenAIException('创建助手消息失败: $e'); - } - } - - /// 发送消息(非流式输出) - /// - /// [messages] 消息列表 - Future sendMessage({ - required List> messages, - String? systemPrompt, //提示词 - String? userProperties, //用户属性 - }) async { - try { - // 构建新的消息列表 - final List> updatedMessages = []; - // 如果有提示词,先添加 system role - if (systemPrompt != null && systemPrompt.isNotEmpty) { - updatedMessages.add({ - 'role': 'system', - 'content': systemPrompt, - }); - } - - // 如果有用户属性,可以作为 user 的一个“隐式指令”传进去 - if (userProperties != null && userProperties.isNotEmpty) { - updatedMessages.add({ - 'role': 'user', - 'content': '【用户属性】$userProperties', - }); - } - // 添加原始消息 - updatedMessages.addAll(messages); - final result = await _channel.invokeMethod( - 'sendMessage', - { - 'messages': updatedMessages, - }, - ); - - if (result == null) { - throw OpenAIException('发送消息失败: 结果为空'); - } - - return result; - } catch (e) { - throw OpenAIException('发送消息失败: $e'); - } - } - - /// 发送消息(流式输出) - /// - /// [messages] 消息列表 - /// - /// 返回一个布尔值,表示请求是否已开始 - Future sendMessageStream({ - required List> messages, - String? systemPrompt, //提示词 - String? userProperties, //用户属性 - }) async { - try { - // 构建新的消息列表 - final List> updatedMessages = []; - // 如果有提示词,先添加 system role - if (systemPrompt != null && systemPrompt.isNotEmpty) { - updatedMessages.add({ - 'role': 'system', - 'content': systemPrompt, - }); - } - - // 如果有用户属性,可以作为 user 的一个“隐式指令”传进去 - if (userProperties != null && userProperties.isNotEmpty) { - updatedMessages.add({ - 'role': 'user', - 'content': '【用户属性】$userProperties', - }); - } - // 添加原始消息 - updatedMessages.addAll(messages); - final result = await _channel.invokeMethod( - 'sendMessageStream', - { - 'messages': updatedMessages, - }, - ); - - return result ?? false; - } catch (e) { - throw OpenAIException('发送流式消息失败: $e'); - } - } - - /// 发送函数调用结果 - /// - /// [messages] 消息列表 - /// [functionCall] 函数调用信息 - /// [functionResult] 函数调用结果 - /// - /// 返回一个布尔值,表示请求是否已开始 - Future sendFunctionCallResult({ - required List> messages, - required Map functionCall, - required String functionResult, - }) async { - try { - final result = await _channel.invokeMethod( - 'sendFunctionCallResult', - { - 'messages': messages, - 'functionCall': functionCall, - 'functionResult': functionResult, - }, - ); - - return result ?? false; - } catch (e) { - throw OpenAIException('发送函数调用结果失败: $e'); - } - } - - /// 从JSON字符串解析函数调用 - Map parseFunctionCall(String functionCallJson) { - try { - return json.decode(functionCallJson) as Map; - } catch (e) { - throw OpenAIException('解析函数调用失败: $e'); - } - } - - /// 初始化MCP客户端 - /// - /// [serverUrl] MCP服务器WebSocket地址 - /// - /// 返回一个布尔值,表示请求是否已开始初始化(实际初始化过程是异步的) - Future initializeMcpClient(String serverUrl) async { - try { - final result = await _channel.invokeMethod( - 'initializeMcpClient', - {'serverUrl': serverUrl}, - ); - - return result ?? false; - } catch (e) { - throw OpenAIException('初始化MCP客户端失败: $e'); - } - } - - /// 检查MCP客户端是否已初始化 - Future isMcpInitialized() async { - try { - final result = await _channel.invokeMethod('isMcpInitialized'); - return result ?? false; - } catch (e) { - throw OpenAIException('检查MCP客户端状态失败: $e'); - } - } - - /// 关闭MCP客户端 - Future closeMcpClient() async { - try { - final result = await _channel.invokeMethod('closeMcpClient'); - return result ?? false; - } catch (e) { - throw OpenAIException('关闭MCP客户端失败: $e'); - } - } - - /// 处理MCP工具调用 - /// - /// [functionCall] 函数调用信息,必须包含name和arguments字段 - /// - /// 返回工具调用结果 - Future handleMcpToolCall(Map functionCall) async { - try { - final result = await _channel.invokeMethod( - 'handleMcpToolCall', - {'functionCall': jsonEncode(functionCall)}, - ); - - return result ?? "处理MCP工具调用失败: 结果为空"; - } catch (e) { - throw OpenAIException('处理MCP工具调用失败: $e'); - } - } - - /// 处理函数调用(自动处理本地函数和MCP工具) - /// - /// 这是一个辅助方法,用于简化函数调用处理流程: - /// 1. MCP工具会被自动处理,不会通过回调返回 - /// 2. 只有本地函数才需要手动处理 - /// 3. 提供统一的处理接口 - /// - /// 参数: - /// [functionCall] 函数调用信息 - /// [messages] 消息上下文 - /// [localFunctionHandler] 本地函数处理器,接收函数名和参数,返回处理结果 - /// - /// 返回值: - /// 布尔值表示是否成功处理(已将结果发送回OpenAI) - Future handleFunctionCall({ - required Map functionCall, - required List> messages, - required Future Function( - String name, Map arguments) - localFunctionHandler, - }) async { - final name = functionCall['name'] as String; - final argumentsJson = functionCall['arguments'] as String; - - // 解析参数 - Map arguments; - try { - arguments = json.decode(argumentsJson) as Map; - } catch (e) { - arguments = {}; - } - - try { - // 调用本地函数处理器 - final result = await localFunctionHandler(name, arguments); - - // 发送结果回OpenAI - return sendFunctionCallResult( - messages: messages, - functionCall: functionCall, - functionResult: result, - ); - } catch (e) { - throw OpenAIException('处理函数调用失败: $e'); - } - } -} diff --git a/apps/client/local_plugins/open_ai_service/pubspec.yaml b/apps/client/local_plugins/open_ai_service/pubspec.yaml deleted file mode 100644 index d616f5ee..00000000 --- a/apps/client/local_plugins/open_ai_service/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: open_ai_service -description: 原生OpenAI服务插件,提供与OpenAI API的交互功能,支持流式输出和函数调用。 -version: 0.0.1 -homepage: https://github.com/yunqiinnovation/deep_voice - -environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=2.5.0" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^2.0.0 - -flutter: - plugin: - platforms: - android: - package: com.yunqiinnovation.open_ai_service - pluginClass: OpenAIServicePlugin - ios: - pluginClass: OpenAIServicePlugin - swiftPackage: - path: ios/open_ai_service \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/.gitignore b/apps/client/local_plugins/realtime/.gitignore deleted file mode 100644 index f5af9847..00000000 --- a/apps/client/local_plugins/realtime/.gitignore +++ /dev/null @@ -1,51 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# Visual Studio Code related -.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -build/ - -# iOS related -**/ios/.generated/ -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Flutter.podspec -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* -.build/ - -# Coverage -coverage/ - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/IMPLEMENTATION.md b/apps/client/local_plugins/realtime/IMPLEMENTATION.md deleted file mode 100644 index a5d49a97..00000000 --- a/apps/client/local_plugins/realtime/IMPLEMENTATION.md +++ /dev/null @@ -1,190 +0,0 @@ -# Realtime Plugin 实现总结 - -## 项目概述 - -基于您提供的Android原生示例,我为DeepVoice项目创建了一个realtime插件,用于通过WebSocket接入Vocode服务器实现实时语音聊天功能。该插件已完成Android和iOS双平台实现。 - -## 架构设计 - -### 插件结构 -``` -local_plugins/realtime/ -├── android/ # Android实现 -│ ├── build.gradle # Android构建配置 -│ ├── src/main/ -│ │ ├── AndroidManifest.xml # Android权限配置 -│ │ └── kotlin/com/yunqiinnovation/realtime/ -│ │ ├── RealtimePlugin.kt # 主插件入口 -│ │ ├── RealtimeAudioManager.kt # 音频管理器(AudioRecord/AudioTrack) -│ │ └── RealtimeWebSocketManager.kt # WebSocket管理器(OkHttp) -├── ios/ # iOS实现 -│ ├── Classes/ # Objective-C桥接文件 -│ │ ├── RealtimePlugin.h -│ │ └── RealtimePlugin.m -│ ├── realtime/ # Swift Package -│ │ ├── Package.swift -│ │ └── Sources/realtime/ -│ │ ├── RealtimePlugin.swift # 主插件入口 -│ │ ├── RealtimeAudioManager.swift # 音频管理器(AVAudioEngine) -│ │ └── RealtimeWebSocketManager.swift # WebSocket管理器(URLSession) -│ └── realtime.podspec -├── lib/ -│ └── realtime.dart # Flutter接口 -├── pubspec.yaml -└── README.md -``` - -### 核心组件 - -1. **RealtimePlugin (SwiftRealtimePlugin)** - - 主插件入口,处理Flutter方法调用 - - 管理各组件间的协调 - - 处理事件分发 - -2. **RealtimeAudioManager** - - 使用AVAudioEngine进行音频录制 - - 使用AVAudioPlayerNode进行音频播放 - - 支持16kHz/16-bit/单声道格式 - - 实现20ms帧长的音频处理 - -3. **RealtimeWebSocketManager** - - 使用URLSessionWebSocketTask实现WebSocket通信 - - 支持音频数据和文本消息的收发 - - 自动连接状态管理 - -## 技术实现对照 - -| 功能 | Android示例 | iOS实现 | -|------|-------------|---------| -| 音频录制 | AudioRecord | AVAudioEngine + AVAudioInputNode | -| 音频播放 | AudioTrack | AVAudioPlayerNode | -| WebSocket | OkHttp WebSocket | URLSessionWebSocketTask | -| 线程管理 | Kotlin协程 | DispatchQueue | -| 音频格式 | 16kHz/16-bit/单声道 | 16kHz/16-bit/单声道 | -| 帧长 | 20ms (320字节) | 20ms (320字节) | - -## Flutter接口 - -### 主要类 -- `RealtimeService`: 主服务类 -- `RealtimeEvent`: 事件类 -- `ConnectionStatus`: 连接状态枚举 -- `VoiceStatus`: 语音状态枚举 -- `RealtimeException`: 异常类 - -### 主要方法 -- `initialize()`: 初始化服务 -- `connect()`: 连接服务器 -- `disconnect()`: 断开连接 -- `startRecording()`: 开始录音 -- `stopRecording()`: 停止录音 -- `eventStream`: 事件流 - -## 使用方式 - -### 1. 在RealtimeController中集成 -已更新`lib/modules/realtime/controllers/realtime_controller.dart`使用真实的realtime插件,取代了原来的模拟实现。 - -### 2. 事件监听 -```dart -_realtimeService.eventStream.listen((event) { - switch (event.type) { - case 'connectionStatusChanged': - // 处理连接状态变化 - case 'voiceStatusChanged': - // 处理语音状态变化 - case 'textReceived': - // 处理文本消息 - case 'error': - // 处理错误 - } -}); -``` - -## 配置要求 - -### 依赖配置 -已添加到主项目的`pubspec.yaml`: -```yaml -realtime: - path: local_plugins/realtime -``` - -### 权限配置 -iOS的`Info.plist`已包含必要的麦克风权限: -```xml -NSMicrophoneUsageDescription -需要麦克风权限用于语音识别和录音功能 -``` - -## 与Android示例的对应关系 - -1. **初始化对应** - - Android: 创建AudioRecord, AudioTrack, OkHttpClient - - iOS: 创建AVAudioEngine, AVAudioPlayerNode, URLSession - -2. **录音线程对应** - - Android: `loopRecordSend()` 协程 - - iOS: AVAudioInputNode的installTap回调 - -3. **播放线程对应** - - Android: `loopPlayback()` 协程 + LinkedBlockingQueue - - iOS: DispatchQueue + AVAudioPCMBuffer队列 - -4. **WebSocket对应** - - Android: OkHttp WebSocketListener - - iOS: URLSessionWebSocketDelegate - -## 特性支持 - -✅ **已实现** -- Android和iOS双平台支持 -- 实时音频录制和播放(16kHz/16-bit/单声道) -- WebSocket双向通信 -- 状态管理和事件通知 -- 错误处理 -- 资源管理和清理 -- 20ms帧长处理 -- Kotlin协程和DispatchQueue线程管理 - -❌ **未实现(可扩展)** -- 音频编码(Opus等) -- 噪声消除/回声抑制 -- 自动重连机制 -- 音频质量自适应 - -## 使用注意事项 - -1. **服务器地址配置** - ```dart - // 需要替换为实际的Vocode服务器地址 - serverUrl: 'wss://your-vocode-server/ws' - ``` - -2. **音频格式一致性** - - 确保服务器支持16kHz/16-bit/单声道格式 - - 帧长固定为20ms (320字节) - -3. **错误处理** - - 监听事件流中的错误 - - 处理网络断线和重连 - -4. **资源管理** - - 及时调用dispose()释放资源 - - 避免内存泄漏 - -## 测试建议 - -1. **本地测试** - - 先用echo服务器测试WebSocket连接 - - 验证音频录制和播放功能 - -2. **集成测试** - - 与真实Vocode服务器集成 - - 测试端到端语音交互 - -3. **性能测试** - - 测试长时间使用的稳定性 - - 监控内存和CPU使用 - -这个实现为DeepVoice项目提供了完整的实时语音交互能力,可以直接与Vocode服务器进行通信,实现类似Android示例的功能。 \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/README.md b/apps/client/local_plugins/realtime/README.md deleted file mode 100644 index a5bc746f..00000000 --- a/apps/client/local_plugins/realtime/README.md +++ /dev/null @@ -1,125 +0,0 @@ -# Realtime Plugin - -实时语音聊天插件,通过WebSocket连接Vocode服务器实现实时语音交互。 - -## 功能特性 - -- **实时音频录制**: 16kHz/16-bit/单声道格式录音 -- **WebSocket通信**: 与Vocode服务器进行实时数据传输 -- **音频播放**: 播放服务器返回的语音数据 -- **状态管理**: 连接状态和语音状态监控 -- **事件回调**: 支持各种事件的监听和处理 - -## 技术实现 - -### Android端实现 -- **音频录制**: 使用AudioRecord进行16kHz/16-bit/单声道录音 -- **音频播放**: 使用AudioTrack进行音频播放 -- **WebSocket**: 使用OkHttp WebSocket客户端 -- **线程管理**: 使用Kotlin协程处理录音和播放 - -### iOS端实现 -- **音频录制**: 使用AVAudioEngine和AVAudioInputNode -- **音频播放**: 使用AVAudioPlayerNode进行播放 -- **WebSocket**: 使用URLSessionWebSocketTask -- **线程管理**: 使用DispatchQueue管理录音和播放线程 - -### 核心组件 -- `RealtimePlugin`: 主插件入口,处理Flutter方法调用 -- `RealtimeAudioManager`: 音频管理器,负责录音和播放 -- `RealtimeWebSocketManager`: WebSocket管理器,负责网络通信 - -**Android端组件**: -- AudioRecord + AudioTrack + OkHttp + Kotlin协程 -- 对应您提供的Android原生示例功能 - -**iOS端组件**: -- AVAudioEngine + AVAudioPlayerNode + URLSessionWebSocketTask + DispatchQueue - -## 使用方法 - -```dart -import 'package:realtime/realtime.dart'; - -final realtimeService = RealtimeService(); - -// 初始化 -await realtimeService.initialize( - serverUrl: 'wss://your-server/ws', - sampleRate: 16000, - channels: 1, - bitsPerSample: 16, -); - -// 监听事件 -realtimeService.eventStream.listen((event) { - switch (event.type) { - case 'connectionStatusChanged': - // 处理连接状态变化 - break; - case 'voiceStatusChanged': - // 处理语音状态变化 - break; - case 'textReceived': - // 处理收到的文本消息 - break; - case 'error': - // 处理错误 - break; - } -}); - -// 连接服务器 -await realtimeService.connect(); - -// 开始录音 -await realtimeService.startRecording(); - -// 停止录音 -await realtimeService.stopRecording(); - -// 断开连接 -await realtimeService.disconnect(); - -// 释放资源 -await realtimeService.dispose(); -``` - -## 音频格式 - -- **采样率**: 16000 Hz -- **声道数**: 1(单声道) -- **位深**: 16-bit -- **帧长**: 20ms (320字节) - -## 权限要求 - -### Android -权限已自动包含在插件中: -- `RECORD_AUDIO`: 音频录制权限 -- `INTERNET`: 网络访问权限 -- `ACCESS_NETWORK_STATE`: 网络状态权限 - -### iOS -在Info.plist中添加麦克风权限: -```xml -NSMicrophoneUsageDescription -应用需要麦克风权限进行语音录制 -``` - -## 注意事项 - -1. 确保服务器地址正确且可访问 -2. 网络环境良好,避免频繁断线 -3. 音频格式与服务器保持一致 -4. 及时释放资源,避免内存泄漏 - -## 错误处理 - -插件会自动处理常见错误: -- 网络连接失败 -- 音频设备不可用 -- 权限被拒绝 -- 服务器断开连接 - -通过事件流可以监听这些错误并进行相应处理。 \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/android/build.gradle.kts b/apps/client/local_plugins/realtime/android/build.gradle.kts deleted file mode 100644 index 10c4dad2..00000000 --- a/apps/client/local_plugins/realtime/android/build.gradle.kts +++ /dev/null @@ -1,47 +0,0 @@ -plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") - kotlin("plugin.serialization") version "1.9.24" -} - -android { - namespace = "com.yunqiinnovation.realtime" - compileSdk = 35 - - defaultConfig { - minSdk = 21 - targetSdk = 35 - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - kotlinOptions { - jvmTarget = "11" - } - - packaging { - resources { - excludes.add("META-INF/DEPENDENCIES") - excludes.add("META-INF/LICENSE") - excludes.add("META-INF/LICENSE.txt") - excludes.add("META-INF/license.txt") - excludes.add("META-INF/NOTICE") - excludes.add("META-INF/NOTICE.txt") - excludes.add("META-INF/notice.txt") - excludes.add("META-INF/*.kotlin_module") - } - } -} - -dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") - - // OkHttp for WebSocket - implementation(platform("com.squareup.okhttp3:okhttp-bom:4.12.0")) - implementation("com.squareup.okhttp3:okhttp") -} \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/android/settings.gradle.kts b/apps/client/local_plugins/realtime/android/settings.gradle.kts deleted file mode 100644 index dd4a80ad..00000000 --- a/apps/client/local_plugins/realtime/android/settings.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = "realtime" \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/android/src/main/AndroidManifest.xml b/apps/client/local_plugins/realtime/android/src/main/AndroidManifest.xml deleted file mode 100644 index 64df6a0a..00000000 --- a/apps/client/local_plugins/realtime/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeAudioManager.kt b/apps/client/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeAudioManager.kt deleted file mode 100644 index d4c130e7..00000000 --- a/apps/client/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeAudioManager.kt +++ /dev/null @@ -1,590 +0,0 @@ -package com.yunqiinnovation.realtime - -import android.content.Context -import android.media.* -import android.media.audiofx.AcousticEchoCanceler -import android.media.audiofx.AutomaticGainControl -import android.media.audiofx.NoiseSuppressor -import android.os.Build -import android.util.Log -import kotlinx.coroutines.* -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -import java.nio.ByteOrder -import java.util.concurrent.LinkedBlockingQueue -import kotlin.math.sqrt - - -/** - * 实时音频管理器 - * - * 负责音频的录制和播放,对应Android示例中的AudioRecord和AudioTrack功能 - */ -class RealtimeAudioManager(private val context: Context) { - companion object { - private const val TAG = "RealtimeAudioManager" - } - - // 音频配置 - private var audioConfig: AudioConfig? = null - - // 录音相关 - private var audioRecord: AudioRecord? = null - private var isRecording = false - private var recordJob: Job? = null - - // 播放相关 - private var audioTrack: AudioTrack? = null - private var isPlaying = false - private var playJob: Job? = null - private val playbackQueue = LinkedBlockingQueue() - private val playbackInternalBuffer = ByteArrayOutputStream() - - // 回音消除相关 - private var acousticEchoCanceler: AcousticEchoCanceler? = null - private var noiseSuppressor: NoiseSuppressor? = null - private var automaticGainControl: AutomaticGainControl? = null - private var audioManager: AudioManager? = null - private var originalAudioMode = AudioManager.MODE_NORMAL - - // 音频焦点 - private var audioFocusRequest: AudioFocusRequest? = null - private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> - Log.d(TAG, "音频焦点变化: $focusChange") - } - - // 回调 - var onAudioData: ((ByteArray) -> Unit)? = null - var onStatusChanged: ((VoiceStatus) -> Unit)? = null - var onRmsChanged: ((Float) -> Unit)? = null // 新增RMS回调 - - // 协程作用域 - private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - /// 初始化音频管理器 - fun initialize(config: AudioConfig): Boolean { - this.audioConfig = config - - // 初始化系统音频管理器 - audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - - // 保存原始音频模式 - originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL - - return setupAudioRecord(config) && setupAudioTrack(config) - } - - /// 设置AudioRecord(录音) - private fun setupAudioRecord(config: AudioConfig): Boolean { - try { - val channelConfig = if (config.channels == 1) { - AudioFormat.CHANNEL_IN_MONO - } else { - AudioFormat.CHANNEL_IN_STEREO - } - - val audioFormat = when (config.bitsPerSample) { - 16 -> AudioFormat.ENCODING_PCM_16BIT - 8 -> AudioFormat.ENCODING_PCM_8BIT - else -> AudioFormat.ENCODING_PCM_16BIT - } - - val bufferSize = AudioRecord.getMinBufferSize( - config.sampleRate, - channelConfig, - audioFormat - ) - - if (bufferSize == AudioRecord.ERROR || bufferSize == AudioRecord.ERROR_BAD_VALUE) { - Log.e(TAG, "无法获取音频录制缓冲区大小") - return false - } - - audioRecord = AudioRecord( - MediaRecorder.AudioSource.MIC, - config.sampleRate, - channelConfig, - audioFormat, - bufferSize - ) - - if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { - Log.e(TAG, "AudioRecord初始化失败") - return false - } - - Log.i(TAG, "AudioRecord初始化成功,采样率: ${config.sampleRate}, 声道: ${config.channels}, 位深: ${config.bitsPerSample}") - return true - } catch (e: Exception) { - Log.e(TAG, "设置AudioRecord失败: ${e.message}", e) - return false - } - } - - /// 设置AudioTrack(播放) - private fun setupAudioTrack(config: AudioConfig): Boolean { - try { - val channelConfig = if (config.channels == 1) { - AudioFormat.CHANNEL_OUT_MONO - } else { - AudioFormat.CHANNEL_OUT_STEREO - } - - val audioFormat = when (config.bitsPerSample) { - 16 -> AudioFormat.ENCODING_PCM_16BIT - 8 -> AudioFormat.ENCODING_PCM_8BIT - else -> AudioFormat.ENCODING_PCM_16BIT - } - - val bufferSize = AudioTrack.getMinBufferSize( - config.sampleRate, - channelConfig, - audioFormat - ) - - if (bufferSize == AudioTrack.ERROR || bufferSize == AudioTrack.ERROR_BAD_VALUE) { - Log.e(TAG, "无法获取音频播放缓冲区大小") - return false - } - - // 总是使用VOICE_COMMUNICATION模式以获得最佳语音处理效果 - val audioAttributes = AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build() - - audioTrack = AudioTrack.Builder() - .setAudioAttributes(audioAttributes) - .setAudioFormat( - AudioFormat.Builder() - .setSampleRate(config.sampleRate) - .setChannelMask(channelConfig) - .setEncoding(audioFormat) - .build() - ) - .setBufferSizeInBytes(bufferSize) - .setTransferMode(AudioTrack.MODE_STREAM) - .build() - - if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) { - Log.e(TAG, "AudioTrack初始化失败") - return false - } - - Log.i(TAG, "AudioTrack初始化成功, 启用语音通信模式") - return true - } catch (e: Exception) { - Log.e(TAG, "设置AudioTrack失败: ${e.message}", e) - return false - } - } - - /// 开始录音 - fun startRecording(): Boolean { - if (isRecording || audioRecord == null) { - return false - } - - try { - // 抢占音频焦点(暂停外部和内部其他音乐) - requestAudioFocus() - - // 设置回音消除(总是启用) - setupEchoCancellation() - - audioRecord?.startRecording() - isRecording = true - onStatusChanged?.invoke(VoiceStatus.RECORDING) - - // 启动录音协程 - recordJob = scope.launch { - loopRecordSend() - } - - Log.i(TAG, "开始录音, 回音消除: ${acousticEchoCanceler?.enabled}") - return true - } catch (e: Exception) { - Log.e(TAG, "开始录音失败: ${e.message}", e) - return false - } - } - - /// 录音循环(对应Android示例中的loopRecordSend) - private suspend fun loopRecordSend() { - val config = audioConfig ?: return - val frameSize = config.frameSize - val buffer = ByteArray(frameSize) - - while (isRecording && audioRecord != null) { - try { - val bytesRead = audioRecord!!.read(buffer, 0, frameSize) - - if (bytesRead > 0) { - // 确保读取的数据长度正确 - val audioData = if (bytesRead == frameSize) { - buffer - } else { - buffer.copyOf(bytesRead) - } - - // 计算RMS值 - val rms = calculateRMS(audioData) - onRmsChanged?.invoke(rms) - - // 直接回调音频数据到Flutter层 - onAudioData?.invoke(audioData) - } else { - Log.w(TAG, "录音读取数据失败: $bytesRead") - } - } catch (e: Exception) { - Log.e(TAG, "录音循环异常: ${e.message}", e) - break - } - } - } - - /// 停止录音 - fun stopRecording(): Boolean { - if (!isRecording) { - return false - } - - try { - isRecording = false - recordJob?.cancel() - audioRecord?.stop() - - // 清理回音消除 - cleanupEchoCancellation() - - // 录音和播放都停止时归还焦点,让外部音乐恢复 - if (!isPlaying) { - abandonAudioFocus() - } - - onStatusChanged?.invoke(VoiceStatus.IDLE) - Log.i(TAG, "录音已停止") - return true - } catch (e: Exception) { - Log.e(TAG, "停止录音失败: ${e.message}", e) - return false - } - } - - /// 播放音频数据 - fun playAudioData(data: ByteArray) { - if (data.isNotEmpty()) { - // 计算播放数据的RMS值 - val rms = calculateRMS(data) - onRmsChanged?.invoke(rms) - - playbackQueue.offer(data) - if (!isPlaying) { - startPlayback() - } - } - } - - /// 开始播放 - private fun startPlayback() { - if (isPlaying || audioTrack == null) { - return - } - - try { - isPlaying = true - onStatusChanged?.invoke(VoiceStatus.PLAYING) - audioTrack?.play() - - playJob = scope.launch { - loopPlayback() - } - Log.i(TAG, "开始播放") - } catch (e: Exception) { - Log.e(TAG, "开始播放失败: ${e.message}", e) - } - } - - /// 播放循环 - private suspend fun loopPlayback() { - Log.d(TAG, "启动播放循环") - - while (isPlaying) { - try { - // take() 会一直等待直到队列中有数据 - val data = playbackQueue.take() - - - // 直接播放整个数据块,让AudioTrack自己处理时序 - audioTrack?.write(data, 0, data.size) - - } catch (e: InterruptedException) { - Log.d(TAG, "播放循环被中断") - break - } catch (e: Exception) { - Log.e(TAG, "播放循环异常: ${e.message}", e) - } - } - Log.d(TAG, "播放循环结束") - stopPlaybackInternal() - } - - /// 内部停止播放方法 - private fun stopPlaybackInternal() { - if (!isPlaying) return - isPlaying = false - - playbackQueue.clear() - - // 使用try-catch保护,因为AudioTrack状态可能不稳定 - try { - if (audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING) { - audioTrack?.pause() - audioTrack?.flush() - } - // 不要在这里调用stop,stop是用来释放资源的,pause/flush更适合流式播放的暂停 - } catch (e: Exception) { - Log.e(TAG, "暂停和清空AudioTrack失败: ${e.message}", e) - } - - onStatusChanged?.invoke(VoiceStatus.IDLE) - Log.i(TAG, "播放已通过内部调用停止") - } - - /// 停止播放(外部调用) - fun stopPlaying(): Boolean { - if (!isPlaying) { - return true - } - try { - isPlaying = false - playJob?.cancel() // 取消协程 - playJob = null - stopPlaybackInternal() - - // 恢复音频模式 - restoreAudioMode() - - // 录音和播放都停止时归还焦点,让外部音乐恢复 - if (!isRecording) { - abandonAudioFocus() - } - - return true - } catch (e: Exception) { - Log.e(TAG, "停止播放失败: ${e.message}", e) - return false - } - } - - /// 更新配置 - fun updateConfig(config: AudioConfig): Boolean { - // 如果正在录音或播放,先停止 - if (isRecording) { - stopRecording() - } - if (isPlaying) { - stopPlaying() - } - - // 释放旧的音频对象 - releaseAudioObjects() - - return initialize(config) - } - - /// 释放音频对象 - private fun releaseAudioObjects() { - try { - audioRecord?.release() - audioRecord = null - - audioTrack?.release() - audioTrack = null - } catch (e: Exception) { - Log.e(TAG, "释放音频对象失败: ${e.message}", e) - } - } - - /// 释放资源 - fun dispose() { - stopRecording() - stopPlaying() - - // 清理回音消除和恢复音频模式 - cleanupEchoCancellation() - restoreAudioMode() - - // 兜底归还音频焦点 - abandonAudioFocus() - - scope.cancel() - releaseAudioObjects() - playbackQueue.clear() - - Log.i(TAG, "音频管理器已释放") - } - - /// 请求音频焦点:使用 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,让外部和内部其他音频暂停 - private fun requestAudioFocus() { - val am = audioManager ?: return - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (audioFocusRequest != null) return - val attrs = AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build() - val req = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) - .setAudioAttributes(attrs) - .setOnAudioFocusChangeListener(audioFocusChangeListener) - .build() - audioFocusRequest = req - val result = am.requestAudioFocus(req) - Log.i(TAG, "请求音频焦点(GAIN) 结果=$result") - } else { - @Suppress("DEPRECATION") - val result = am.requestAudioFocus( - audioFocusChangeListener, - AudioManager.STREAM_VOICE_CALL, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE - ) - Log.i(TAG, "请求音频焦点(GAIN, legacy) 结果=$result") - } - } catch (e: Exception) { - Log.e(TAG, "请求音频焦点失败: ${e.message}", e) - } - } - - /// 归还音频焦点 - private fun abandonAudioFocus() { - val am = audioManager ?: return - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - audioFocusRequest?.let { am.abandonAudioFocusRequest(it) } - audioFocusRequest = null - } else { - @Suppress("DEPRECATION") - am.abandonAudioFocus(audioFocusChangeListener) - } - Log.i(TAG, "已归还音频焦点") - } catch (e: Exception) { - Log.e(TAG, "归还音频焦点失败: ${e.message}", e) - } - } - - - - - /// 设置回音消除(总是启用) - private fun setupEchoCancellation() { - val record = audioRecord ?: return - - try { - // 设置音频模式为通信模式 - audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION - - // 设置声学回音消除 - if (AcousticEchoCanceler.isAvailable()) { - acousticEchoCanceler = AcousticEchoCanceler.create(record.audioSessionId) - acousticEchoCanceler?.enabled = true - Log.i(TAG, "声学回音消除已启用") - } else { - Log.w(TAG, "设备不支持声学回音消除") - } - - // 设置噪声抑制 - if (NoiseSuppressor.isAvailable()) { - noiseSuppressor = NoiseSuppressor.create(record.audioSessionId) - noiseSuppressor?.enabled = true - Log.i(TAG, "噪声抑制已启用") - } - - // 设置自动增益控制 - if (AutomaticGainControl.isAvailable()) { - automaticGainControl = AutomaticGainControl.create(record.audioSessionId) - automaticGainControl?.enabled = true - Log.i(TAG, "自动增益控制已启用") - } - - Log.i(TAG, "回音消除和音频增强功能已全部启用") - } catch (e: Exception) { - Log.e(TAG, "设置回音消除失败: ${e.message}", e) - } - } - - /// 清理回音消除 - private fun cleanupEchoCancellation() { - try { - acousticEchoCanceler?.let { - it.enabled = false - it.release() - acousticEchoCanceler = null - Log.d(TAG, "声学回音消除已释放") - } - - noiseSuppressor?.let { - it.enabled = false - it.release() - noiseSuppressor = null - Log.d(TAG, "噪声抑制已释放") - } - - automaticGainControl?.let { - it.enabled = false - it.release() - automaticGainControl = null - Log.d(TAG, "自动增益控制已释放") - } - } catch (e: Exception) { - Log.e(TAG, "清理回音消除失败: ${e.message}", e) - } - } - - /// 恢复音频模式 - private fun restoreAudioMode() { - try { - audioManager?.mode = originalAudioMode - Log.d(TAG, "音频模式已恢复为: $originalAudioMode") - } catch (e: Exception) { - Log.e(TAG, "恢复音频模式失败: ${e.message}", e) - } - } - - fun destroy() { - Log.d(TAG, "销毁AudioManager") - stopRecording() - stopPlaying() - scope.cancel() // 确保所有协程都已取消 - - audioRecord?.release() - audioTrack?.release() - releaseAudioObjects() - playbackQueue.clear() - - Log.i(TAG, "音频管理器已释放") - } - - /// 计算音频数据的RMS值 - private fun calculateRMS(audioData: ByteArray): Float { - if (audioData.isEmpty()) return 0f - - var sum = 0.0 - // 处理16位PCM数据 - for (i in 0 until audioData.size step 2) { - if (i + 1 < audioData.size) { - // Little-endian: 低字节在前 - val sample = (audioData[i].toInt() and 0xFF) or - (audioData[i + 1].toInt() shl 8) - // 转换为有符号16位整数 - val signedSample = sample.toShort().toFloat() - // 归一化到[-1, 1] - val normalizedSample = signedSample / 32768f - sum += normalizedSample * normalizedSample - } - } - - val mean = sum / (audioData.size / 2) - return sqrt(mean).toFloat() - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimePlugin.kt b/apps/client/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimePlugin.kt deleted file mode 100644 index b8077c2c..00000000 --- a/apps/client/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimePlugin.kt +++ /dev/null @@ -1,455 +0,0 @@ -package com.yunqiinnovation.realtime - -import android.content.Context -import android.os.Handler -import android.os.Looper -import android.util.Log -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import kotlinx.coroutines.* -import org.json.JSONObject - -/** - * 实时语音聊天插件 - * - * 通过WebSocket连接Vocode服务器实现实时语音交互 - * 包含录音、播放、WebSocket通信等功能 - */ -class RealtimePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler { - companion object { - private const val TAG = "RealtimePlugin" - private const val METHOD_CHANNEL = "realtime/methods" - private const val EVENT_CHANNEL = "realtime/events" - } - - private lateinit var context: Context - private lateinit var methodChannel: MethodChannel - private lateinit var eventChannel: EventChannel - private var eventSink: EventChannel.EventSink? = null - - // 核心组件:设为可空,因为它们的生命周期与页面会话绑定 - private var audioManager: RealtimeAudioManager? = null - private var webSocketManager: RealtimeWebSocketManager? = null - - // 协程作用域:需要可变,因为在重用插件时需要重新创建 - private var scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - - // 主线程Handler,用于向Flutter发送事件与回调 - private val mainHandler = Handler(Looper.getMainLooper()) - - // 配置参数 - private var serverUrl: String = "" - private var sampleRate: Int = 16000 - private var channels: Int = 1 - private var bitsPerSample: Int = 16 - - // 状态 - @Volatile - private var connectionStatus: ConnectionStatus = ConnectionStatus.DISCONNECTED - @Volatile - private var voiceStatus: VoiceStatus = VoiceStatus.IDLE - - // 清理状态标志,确保清理逻辑只执行一次 - @Volatile - private var isCleanedUp = false - private val cleanupLock = Any() - - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - context = flutterPluginBinding.applicationContext - - methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, METHOD_CHANNEL) - methodChannel.setMethodCallHandler(this) - - eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL) - eventChannel.setStreamHandler(this) - - Log.i(TAG, "Realtime插件已附加到引擎") - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - Log.i(TAG, "插件正在从引擎分离,执行清理...") - performCleanup() - methodChannel.setMethodCallHandler(null) - eventChannel.setStreamHandler(null) - Log.i(TAG, "Realtime插件已从引擎分离") - } - - /// 设置各组件的回调 - private fun setupCallbacks() { - Log.d(TAG, "setupCallbacks: 正在为新的管理器实例设置回调...") - - // 音频管理器回调 - audioManager?.onAudioData = { data -> - // 发送到WebSocket - webSocketManager?.sendAudioData(data) - // 发送录音PCM数据到Flutter用于可视化 - val pcm16Data = convertByteArrayToPCM16(data) - sendEvent("recordingPcmData", pcm16Data) - } - - audioManager?.onStatusChanged = { status -> - voiceStatus = status - sendEvent("voiceStatusChanged", status.value) - } - - // 新增RMS回调 - audioManager?.onRmsChanged = { rms -> - sendEvent("audioRms", rms) - } - - // WebSocket管理器回调 - webSocketManager?.onConnectionStatusChanged = { status -> - connectionStatus = status - sendEvent("connectionStatusChanged", status.value) - } - - webSocketManager?.onAudioReceived = { data -> - // 添加音频数据前50字节的日志输出 - val dataPreview = if (data.size > 50) { - data.sliceArray(0..49).joinToString(" ") { "%02x".format(it) } + "..." - } else { - data.joinToString(" ") { "%02x".format(it) } - } - Log.d(TAG, "收到音频数据前50字节: $dataPreview") - Log.d(TAG, "音频数据总长度: ${data.size} 字节") - - audioManager?.playAudioData(data) - // 发送播放PCM数据到Flutter用于可视化 - val pcm16Data = convertByteArrayToPCM16(data) - sendEvent("playbackPcmData", pcm16Data) - } - - webSocketManager?.onTextReceived = { text -> - // 直接发送原始JSON文本到Flutter层处理 - sendEvent("textReceived", text) - } - - webSocketManager?.onError = { error -> - sendEvent("error", error) - } - } - - /// 发送事件到Flutter端 - private fun sendEvent(type: String, data: Any?) { - val sink = eventSink - if (sink == null) { - Log.d(TAG, "事件流已关闭,忽略事件: $type") - return - } - - val event = mapOf( - "type" to type, - "data" to data - ) - - try { - mainHandler.post { - try { - sink.success(event) - } catch (e: Exception) { - Log.w(TAG, "发送事件失败: \\${e.message}") - } - } - } catch (e: Exception) { - Log.w(TAG, "发送事件时出现异常: \\${e.message}") - } - } - - override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "initialize" -> handleInitialize(call, result) - "connect" -> handleConnect(result) - "disconnect" -> handleDisconnect(result) - "startRecording" -> handleStartRecording(result) - "stopRecording" -> handleStopRecording(result) - "stopPlaying" -> handleStopPlaying(result) - "getConnectionStatus" -> result.success(connectionStatus.value) - "getVoiceStatus" -> result.success(voiceStatus.value) - "sendTextMessage" -> handleSendTextMessage(call, result) - "setAudioConfig" -> handleSetAudioConfig(call, result) - "dispose" -> handleDispose(result) - else -> result.notImplemented() - } - } - - private fun handleInitialize(call: MethodCall, result: Result) { - try { - Log.d(TAG, "handleInitialize: 开始新一轮的初始化...") - - // 1. 如果之前的协程作用域已被取消,则创建一个全新的 - if (!scope.isActive) { - scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - Log.i(TAG, "handleInitialize: 检测到协程作用域已失效,已创建新实例。") - } - - // 2. 重置清理状态标志,允许下一次的清理操作 - synchronized(cleanupLock) { - if (isCleanedUp) { - isCleanedUp = false - Log.i(TAG, "handleInitialize: 清理状态已重置,插件可再次使用。") - } - } - - // 3. 为新会话创建全新的管理器实例 - Log.d(TAG, "handleInitialize: 正在创建新的AudioManager和WebSocketManager实例...") - audioManager = RealtimeAudioManager(context) - webSocketManager = RealtimeWebSocketManager() - - // 4. 为新实例设置回调 - setupCallbacks() - - val args = call.arguments as? Map - val serverUrl = args?.get("serverUrl") as? String - ?: return result.error("INVALID_ARGUMENTS", "服务器地址不能为空", null) - - this.serverUrl = serverUrl - - args["sampleRate"]?.let { this.sampleRate = (it as Number).toInt() } - args["channels"]?.let { this.channels = (it as Number).toInt() } - args["bitsPerSample"]?.let { this.bitsPerSample = (it as Number).toInt() } - - // 初始化音频管理器 - val audioConfig = AudioConfig(sampleRate, channels, bitsPerSample) - val success = audioManager?.initialize(audioConfig) ?: false - - if (success) { - webSocketManager?.initialize(serverUrl) - Log.i(TAG, "实时语音插件初始化成功") - } else { - Log.e(TAG, "实时语音插件初始化失败") - } - - result.success(success) - } catch (e: Exception) { - Log.e(TAG, "初始化失败: ${e.message}", e) - result.error("INITIALIZATION_ERROR", "初始化失败: ${e.message}", null) - } - } - - private fun handleConnect(result: Result) { - Log.d(TAG, "handleConnect: 收到连接请求。scope是否活跃? ${scope.isActive}") - scope.launch { - try { - val success = webSocketManager?.connect() ?: false - mainHandler.post { result.success(success) } - } catch (e: Exception) { - Log.e(TAG, "连接失败: ${e.message}", e) - mainHandler.post { result.error("CONNECTION_ERROR", "连接失败: ${e.message}", null) } - } - } - } - - private fun handleDisconnect(result: Result) { - scope.launch { - try { - webSocketManager?.disconnect() - mainHandler.post { result.success(true) } - } catch (e: Exception) { - Log.e(TAG, "断开连接失败: ${e.message}", e) - mainHandler.post { result.error("DISCONNECTION_ERROR", "断开连接失败: ${e.message}", null) } - } - } - } - - private fun handleStartRecording(result: Result) { - scope.launch { - try { - val success = audioManager?.startRecording() ?: false - mainHandler.post { result.success(success) } - } catch (e: Exception) { - Log.e(TAG, "开始录音失败: ${e.message}", e) - mainHandler.post { result.error("RECORDING_ERROR", "开始录音失败: ${e.message}", null) } - } - } - } - - private fun handleStopRecording(result: Result) { - scope.launch { - try { - val success = audioManager?.stopRecording() ?: false - mainHandler.post { result.success(success) } - } catch (e: Exception) { - Log.e(TAG, "停止录音失败: ${e.message}", e) - mainHandler.post { result.error("RECORDING_ERROR", "停止录音失败: ${e.message}", null) } - } - } - } - - private fun handleStopPlaying(result: Result) { - scope.launch { - try { - val success = audioManager?.stopPlaying() ?: false - mainHandler.post { result.success(success) } - } catch (e: Exception) { - Log.e(TAG, "停止播放失败: ${e.message}", e) - mainHandler.post { result.error("PLAYBACK_ERROR", "停止播放失败: ${e.message}", null) } - } - } - } - - private fun handleSendTextMessage(call: MethodCall, result: Result) { - try { - val args = call.arguments as? Map - val message = args?.get("message") as? String - ?: return result.error("INVALID_ARGUMENTS", "消息内容不能为空", null) - - val success = webSocketManager?.sendTextMessage(message) ?: false - result.success(success) - } catch (e: Exception) { - Log.e(TAG, "发送文本消息失败: ${e.message}", e) - result.error("MESSAGE_ERROR", "发送文本消息失败: ${e.message}", null) - } - } - - private fun handleSetAudioConfig(call: MethodCall, result: Result) { - try { - val args = call.arguments as? Map - ?: return result.error("INVALID_ARGUMENTS", "参数无效", null) - - Log.d(TAG, "收到的音频配置参数: $args") - - args["sampleRate"]?.let { - Log.d(TAG, "sampleRate: $it, type: ${it.javaClass.name}") - this.sampleRate = (it as Number).toInt() - } - args["channels"]?.let { - Log.d(TAG, "channels: $it, type: ${it.javaClass.name}") - this.channels = (it as Number).toInt() - } - args["bitsPerSample"]?.let { - Log.d(TAG, "bitsPerSample: $it, type: ${it.javaClass.name}") - this.bitsPerSample = (it as Number).toInt() - } - - val audioConfig = AudioConfig(sampleRate, channels, bitsPerSample) - val success = audioManager?.updateConfig(audioConfig) ?: false - result.success(success) - } catch (e: Exception) { - Log.e(TAG, "设置音频配置失败: ${e.message}", e) - result.error("CONFIG_ERROR", "设置音频配置失败: ${e.message}", null) - } - } - - private fun handleDispose(result: Result) { - Log.i(TAG, "Flutter端请求释放资源...") - performCleanup() - // 立即返回,不等待异步清理完成 - result.success(null) - } - - /** - * 将字节数组转换为PCM16整数数组 - * PCM16格式:16-bit signed integers, little-endian - */ - private fun convertByteArrayToPCM16(data: ByteArray): List { - val pcm16List = mutableListOf() - - // 确保数据长度是偶数(每2个字节一个采样) - val validSize = data.size and 0xFFFFFFFE.toInt() // 清除最低位,确保偶数 - - // 每2个字节组成一个16-bit采样 - for (i in 0 until validSize step 2) { - if (i + 1 < data.size) { // 安全检查 - // Little-endian: 低字节在前 - val low = data[i].toInt() and 0xFF - val high = data[i + 1].toInt() shl 8 - val sample = (high or low).toShort().toInt() // 转换为有符号16位整数 - pcm16List.add(sample) - } - } - - return pcm16List - } - - /** - * 执行核心清理操作。 - * 此方法是幂等的(可重复调用)和线程安全的。 - */ - private fun performCleanup() { - synchronized(cleanupLock) { - if (isCleanedUp) { - Log.d(TAG, "资源已清理,跳过。") - return - } - Log.i(TAG, "开始执行核心资源清理...") - - // 立即标记,防止重入 - isCleanedUp = true - - // 立即停止接收事件 - eventSink = null - - // 在后台IO线程中执行所有耗时操作,避免阻塞主线程 - CoroutineScope(Dispatchers.IO).launch { - runCatching { audioManager?.stopRecording() } - .onFailure { Log.w(TAG, "停止录音时异常: ${it.message}") } - - runCatching { audioManager?.stopPlaying() } - .onFailure { Log.w(TAG, "停止播放时异常: ${it.message}") } - - runCatching { webSocketManager?.disconnect() } - .onFailure { Log.w(TAG, "断开WebSocket时异常: ${it.message}") } - - // 短暂延迟,以确保挂起的操作有时间完成 - // delay(100L) - - runCatching { audioManager?.dispose() } - .onFailure { Log.w(TAG, "释放音频管理器时异常: ${it.message}") } - - runCatching { webSocketManager?.dispose() } - .onFailure { Log.w(TAG, "释放WebSocket管理器时异常: ${it.message}") } - - runCatching { - scope.cancel() - Log.i(TAG, "插件协程作用域已取消。") - }.onFailure { Log.w(TAG, "取消协程作用域时异常: ${it.message}") } - - // 清理完成,将实例置空 - audioManager = null - webSocketManager = null - - Log.i(TAG, "核心资源清理完成。") - } - } - } - - // EventChannel.StreamHandler 实现 - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - eventSink = events - Log.i(TAG, "事件流监听已开始") - } - - override fun onCancel(arguments: Any?) { - eventSink = null - Log.i(TAG, "事件流监听已取消") - } -} - -// 枚举定义 -enum class ConnectionStatus(val value: String) { - DISCONNECTED("disconnected"), - CONNECTING("connecting"), - CONNECTED("connected"), - ERROR("error") -} - -enum class VoiceStatus(val value: String) { - IDLE("idle"), - RECORDING("recording"), - PROCESSING("processing"), - PLAYING("playing") -} - -// 音频配置 -data class AudioConfig( - val sampleRate: Int, - val channels: Int, - val bitsPerSample: Int -) { - val frameSize: Int - get() = (sampleRate * channels * bitsPerSample / 8 * 20) / 1000 // 20ms帧 -} \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeWebSocketManager.kt b/apps/client/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeWebSocketManager.kt deleted file mode 100644 index 2726ceef..00000000 --- a/apps/client/local_plugins/realtime/android/src/main/kotlin/com/yunqiinnovation/realtime/RealtimeWebSocketManager.kt +++ /dev/null @@ -1,303 +0,0 @@ -package com.yunqiinnovation.realtime - -import android.util.Log -import kotlinx.coroutines.* -import okhttp3.* -import okio.ByteString -import java.util.concurrent.TimeUnit - -/** - * 实时WebSocket管理器 - * - * 负责与Vocode服务器的WebSocket通信,对应Android示例中的OkHttp WebSocket功能 - */ -class RealtimeWebSocketManager : WebSocketListener() { - companion object { - private const val TAG = "RealtimeWebSocketManager" - private const val CONNECT_TIMEOUT = 30L - private const val READ_TIMEOUT = 60L - private const val WRITE_TIMEOUT = 30L - } - - // OkHttp客户端和WebSocket - private var okHttpClient: OkHttpClient? = null - private var webSocket: WebSocket? = null - - // 服务器URL - private var serverUrl: String = "" - - // 连接状态 - @Volatile - private var isConnected = false - @Volatile - private var isConnecting = false - - // 回调 - var onConnectionStatusChanged: ((ConnectionStatus) -> Unit)? = null - var onAudioReceived: ((ByteArray) -> Unit)? = null - var onTextReceived: ((String) -> Unit)? = null - var onError: ((String) -> Unit)? = null - - // 协程作用域 - private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - /// 初始化WebSocket管理器 - fun initialize(serverUrl: String) { - Log.d(TAG, "initialize: 开始初始化。当前httpClient是否为null? ${okHttpClient == null}") - this.serverUrl = serverUrl - - // 创建OkHttp客户端 - okHttpClient = OkHttpClient.Builder() - .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) - .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) - .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS) - .retryOnConnectionFailure(true) - .build() - - Log.i(TAG, "initialize: WebSocket管理器初始化完成。服务器地址: $serverUrl") - } - - /// 连接到服务器 - suspend fun connect(): Boolean = withContext(Dispatchers.IO) { - Log.d(TAG, "connect: 尝试连接。当前状态 - isConnected: $isConnected, isConnecting: $isConnecting") - if (isConnected || isConnecting) { - Log.w(TAG, "connect: 连接请求被拒绝,因为已有连接或正在连接中。") - return@withContext false - } - - val client = okHttpClient ?: run { - Log.e(TAG, "connect: 连接失败,因为OkHttp客户端为null。请确保已调用initialize()。") - onError?.invoke("客户端未初始化") - return@withContext false - } - - Log.d(TAG, "connect: OkHttpClient实例存在,准备发起新连接。") - - try { - isConnecting = true - onConnectionStatusChanged?.invoke(ConnectionStatus.CONNECTING) - - val request = Request.Builder() - .url(serverUrl) - .build() - - Log.d(TAG, "connect: 正在创建新的WebSocket实例...") - webSocket = client.newWebSocket(request, this@RealtimeWebSocketManager) - - // 等待连接建立 - var waitTime = 0 - while (isConnecting && waitTime < CONNECT_TIMEOUT * 1000) { - delay(100) - waitTime += 100 - } - - if (isConnected) { - Log.i(TAG, "connect: WebSocket连接成功") - return@withContext true - } else { - Log.e(TAG, "connect: WebSocket连接超时") - isConnecting = false - onConnectionStatusChanged?.invoke(ConnectionStatus.ERROR) - return@withContext false - } - } catch (e: Exception) { - Log.e(TAG, "WebSocket连接失败: ${e.message}", e) - isConnecting = false - onConnectionStatusChanged?.invoke(ConnectionStatus.ERROR) - onError?.invoke("连接失败: ${e.message}") - return@withContext false - } - } - - /// 断开连接 - fun disconnect() { - try { - isConnected = false - isConnecting = false - - webSocket?.close(1000, "正常关闭") - webSocket = null - - onConnectionStatusChanged?.invoke(ConnectionStatus.DISCONNECTED) - Log.i(TAG, "WebSocket已断开连接") - } catch (e: Exception) { - Log.e(TAG, "断开WebSocket连接失败: ${e.message}", e) - } - } - - /// 发送音频数据 - fun sendAudioData(data: ByteArray) { - if (!isConnected) { - return - } - - try { - // 将音频数据编码为Base64并包装成Vocode WebSocket格式 - val base64AudioData = android.util.Base64.encodeToString(data, android.util.Base64.NO_WRAP) - val audioMessage = org.json.JSONObject().apply { - put("type", "websocket_audio") - put("data", base64AudioData) - } - - val success = webSocket?.send(audioMessage.toString()) ?: false - - if (!success) { - Log.w(TAG, "发送音频数据失败") - } - } catch (e: Exception) { - Log.e(TAG, "发送音频数据异常: ${e.message}", e) - } - } - - /// 发送文本消息 - fun sendTextMessage(message: String): Boolean { - if (!isConnected) { - Log.w(TAG, "WebSocket未连接,无法发送文本消息") - return false - } - - return try { - val success = webSocket?.send(message) ?: false - if (!success) { - Log.w(TAG, "发送文本消息失败") - } - success - } catch (e: Exception) { - Log.e(TAG, "发送文本消息异常: ${e.message}", e) - false - } - } - - /// 释放资源 - fun dispose() { - disconnect() - - // 立即取消scope,不等待 - try { - scope.cancel() - } catch (e: Exception) { - Log.w(TAG, "取消scope时出现异常: ${e.message}") - } - - // 立即强制关闭WebSocket - try { - webSocket?.close(1001, "强制关闭") - webSocket = null - } catch (e: Exception) { - Log.w(TAG, "强制关闭WebSocket时出现异常: ${e.message}") - } - - // 异步关闭线程池,设置更短的超时时间 - okHttpClient?.dispatcher?.executorService?.let { executor -> - Thread { - try { - executor.shutdownNow() // 立即强制关闭 - // 最多等待1秒,避免长时间等待 - if (!executor.awaitTermination(1, TimeUnit.SECONDS)) { - Log.w(TAG, "线程池强制关闭超时,放弃等待") - } - } catch (e: Exception) { - Log.w(TAG, "关闭线程池时出现异常: ${e.message}") - } - }.start() - } - okHttpClient = null - - Log.i(TAG, "WebSocket管理器已释放") - } - - // WebSocketListener 回调实现 - override fun onOpen(webSocket: WebSocket, response: Response) { - Log.i(TAG, "WebSocket连接已建立") - isConnected = true - isConnecting = false - onConnectionStatusChanged?.invoke(ConnectionStatus.CONNECTED) - } - - override fun onMessage(webSocket: WebSocket, text: String) { - // 添加收到文本消息的日志输出,显示消息类型和前20字符 - val textPreview = if (text.length > 50) text.substring(0, 50) + "..." else text - Log.d(TAG, "收到文本消息前20字符: $textPreview") - - try { - // 尝试解析为JSON来检查消息类型 - val json = org.json.JSONObject(text) - val messageType = json.optString("type", "") - - // 添加消息类型的日志输出 - Log.d(TAG, "消息类型: $messageType") - - when (messageType) { - "websocket_audio" -> { - // 处理音频消息 - val audioDataString = json.optString("data", "") - if (audioDataString.isNotEmpty()) { - // 将Base64编码的音频数据解码为字节数组 - val audioData = android.util.Base64.decode(audioDataString, android.util.Base64.DEFAULT) - onAudioReceived?.invoke(audioData) - } - } - "websocket_transcript" -> { - // 处理统一的文本消息格式 - // 包含sender、is_final、timestamp等字段 - onTextReceived?.invoke(text) - } - "websocket_ready" -> { - // 连接就绪消息 - Log.i(TAG, "WebSocket连接就绪") - } - "websocket_stop" -> { - // 会话停止消息 - Log.i(TAG, "会话已结束") - } - "websocket_start" -> { - // 会话开始消息 - Log.i(TAG, "会话已开始") - } - else -> { - // 其他消息类型,当作普通文本消息处理 - onTextReceived?.invoke(text) - } - } - } catch (e: Exception) { - // 如果不是JSON格式,当作普通文本消息处理 - Log.d(TAG, "非JSON格式消息,当作普通文本处理") - onTextReceived?.invoke(text) - } - } - - override fun onMessage(webSocket: WebSocket, bytes: ByteString) { - val audioData = bytes.toByteArray() - onAudioReceived?.invoke(audioData) - } - - override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { - Log.i(TAG, "WebSocket正在关闭,代码: $code, 原因: $reason") - webSocket.close(1000, null) - } - - override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { - Log.i(TAG, "WebSocket已关闭,代码: $code, 原因: $reason") - isConnected = false - isConnecting = false - onConnectionStatusChanged?.invoke(ConnectionStatus.DISCONNECTED) - } - - override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { - val errorMessage = when { - t.message?.contains("Unable to parse TLS packet header") == true -> - "SSL错误:请检查服务器是否支持wss://,本地开发请使用ws://" - t.message?.contains("Connection refused") == true -> - "连接被拒绝:请检查服务器是否运行在指定端口" - t.message?.contains("timeout") == true -> - "连接超时:请检查网络连接和服务器地址" - else -> "连接失败: ${t.message}" - } - - Log.e(TAG, "WebSocket连接失败: ${t.message}", t) - isConnected = false - isConnecting = false - onConnectionStatusChanged?.invoke(ConnectionStatus.ERROR) - onError?.invoke(errorMessage) - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/example.md b/apps/client/local_plugins/realtime/example.md deleted file mode 100644 index 74b8ebd4..00000000 --- a/apps/client/local_plugins/realtime/example.md +++ /dev/null @@ -1,274 +0,0 @@ -# Realtime Plugin 使用示例 - -## 基本用法 - -```dart -import 'package:realtime/realtime.dart'; - -class VoiceCallPage extends StatefulWidget { - @override - _VoiceCallPageState createState() => _VoiceCallPageState(); -} - -class _VoiceCallPageState extends State { - final RealtimeService _realtimeService = RealtimeService(); - StreamSubscription? _eventSubscription; - - ConnectionStatus _connectionStatus = ConnectionStatus.disconnected; - VoiceStatus _voiceStatus = VoiceStatus.idle; - - @override - void initState() { - super.initState(); - _setupEventListener(); - _initializeService(); - } - - void _setupEventListener() { - _eventSubscription = _realtimeService.eventStream.listen((event) { - switch (event.type) { - case 'connectionStatusChanged': - setState(() { - _connectionStatus = _parseConnectionStatus(event.data); - }); - break; - case 'voiceStatusChanged': - setState(() { - _voiceStatus = _parseVoiceStatus(event.data); - }); - break; - case 'textReceived': - print('收到文本: ${event.data}'); - break; - case 'error': - print('错误: ${event.data}'); - break; - } - }); - } - - Future _initializeService() async { - try { - final success = await _realtimeService.initialize( - serverUrl: 'wss://your-vocode-server.com/ws', - sampleRate: 16000, - channels: 1, - bitsPerSample: 16, - ); - - if (success) { - print('Realtime服务初始化成功'); - } else { - print('Realtime服务初始化失败'); - } - } catch (e) { - print('初始化错误: $e'); - } - } - - Future _connect() async { - try { - final success = await _realtimeService.connect(); - if (!success) { - print('连接失败'); - } - } catch (e) { - print('连接错误: $e'); - } - } - - Future _disconnect() async { - try { - await _realtimeService.disconnect(); - } catch (e) { - print('断开连接错误: $e'); - } - } - - Future _startRecording() async { - try { - final success = await _realtimeService.startRecording(); - if (!success) { - print('开始录音失败'); - } - } catch (e) { - print('录音错误: $e'); - } - } - - Future _stopRecording() async { - try { - final success = await _realtimeService.stopRecording(); - if (!success) { - print('停止录音失败'); - } - } catch (e) { - print('停止录音错误: $e'); - } - } - - ConnectionStatus _parseConnectionStatus(String status) { - switch (status) { - case 'connected': return ConnectionStatus.connected; - case 'connecting': return ConnectionStatus.connecting; - case 'disconnected': return ConnectionStatus.disconnected; - case 'error': return ConnectionStatus.error; - default: return ConnectionStatus.disconnected; - } - } - - VoiceStatus _parseVoiceStatus(String status) { - switch (status) { - case 'recording': return VoiceStatus.recording; - case 'processing': return VoiceStatus.processing; - case 'playing': return VoiceStatus.playing; - case 'idle': return VoiceStatus.idle; - default: return VoiceStatus.idle; - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('实时语音聊天'), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 连接状态显示 - Text('连接状态: ${_connectionStatus.name}'), - SizedBox(height: 20), - - // 语音状态显示 - Text('语音状态: ${_voiceStatus.name}'), - SizedBox(height: 40), - - // 连接按钮 - ElevatedButton( - onPressed: _connectionStatus == ConnectionStatus.disconnected - ? _connect - : _disconnect, - child: Text(_connectionStatus == ConnectionStatus.disconnected - ? '连接' - : '断开'), - ), - SizedBox(height: 20), - - // 录音按钮 - ElevatedButton( - onPressed: _connectionStatus == ConnectionStatus.connected - ? (_voiceStatus == VoiceStatus.recording - ? _stopRecording - : _startRecording) - : null, - child: Text(_voiceStatus == VoiceStatus.recording - ? '停止录音' - : '开始录音'), - ), - ], - ), - ), - ); - } - - @override - void dispose() { - _eventSubscription?.cancel(); - _realtimeService.dispose(); - super.dispose(); - } -} -``` - -## GetX控制器中的使用 - -```dart -import 'package:get/get.dart'; -import 'package:realtime/realtime.dart'; - -class VoiceController extends GetxController { - final RealtimeService _realtimeService = RealtimeService(); - - final RxBool isConnected = false.obs; - final RxBool isRecording = false.obs; - - StreamSubscription? _eventSubscription; - - @override - void onInit() { - super.onInit(); - _setupEventListener(); - _initializeService(); - } - - void _setupEventListener() { - _eventSubscription = _realtimeService.eventStream.listen((event) { - switch (event.type) { - case 'connectionStatusChanged': - isConnected.value = event.data == 'connected'; - break; - case 'voiceStatusChanged': - isRecording.value = event.data == 'recording'; - break; - } - }); - } - - Future _initializeService() async { - await _realtimeService.initialize( - serverUrl: 'wss://your-server/ws', - ); - } - - Future toggleConnection() async { - if (isConnected.value) { - await _realtimeService.disconnect(); - } else { - await _realtimeService.connect(); - } - } - - Future toggleRecording() async { - if (isRecording.value) { - await _realtimeService.stopRecording(); - } else { - await _realtimeService.startRecording(); - } - } - - @override - void onClose() { - _eventSubscription?.cancel(); - _realtimeService.dispose(); - super.onClose(); - } -} -``` - -## 权限配置 - -### iOS -在 `ios/Runner/Info.plist` 中添加: - -```xml -NSMicrophoneUsageDescription -应用需要麦克风权限进行语音录制 -``` - -## 服务器配置示例 - -需要一个支持WebSocket的Vocode服务器,具体实现可参考Vocode官方文档。 - -服务器需要: -1. 接收16kHz/16-bit/单声道的PCM音频数据 -2. 返回相同格式的音频数据 -3. 支持文本消息交换 - -## 注意事项 - -1. 确保网络连接稳定 -2. 音频格式必须与服务器保持一致 -3. 及时处理事件流中的错误 -4. 在适当时机释放资源 \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/ios/realtime/Package.swift b/apps/client/local_plugins/realtime/ios/realtime/Package.swift deleted file mode 100644 index 3191b50b..00000000 --- a/apps/client/local_plugins/realtime/ios/realtime/Package.swift +++ /dev/null @@ -1,18 +0,0 @@ -// swift-tools-version: 5.9 -import PackageDescription - -let package = Package( - name: "realtime", - platforms: [.iOS("16.0")], - products: [ - .library(name: "realtime", targets: ["realtime"]) - ], - dependencies: [], - targets: [ - .target( - name: "realtime", - dependencies: [], - path: "Sources/realtime" - ) - ] -) \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeAudioManager.swift b/apps/client/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeAudioManager.swift deleted file mode 100644 index acca2477..00000000 --- a/apps/client/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeAudioManager.swift +++ /dev/null @@ -1,377 +0,0 @@ -import AVFoundation -import os.log -import Accelerate - -/** - * 实时音频管理器 - * - * 负责音频的录制和播放,对应Android示例中的AudioRecord和AudioTrack功能 - */ -class RealtimeAudioManager { - - private let log = OSLog(subsystem: "com.yunqiinnovation.realtime", category: "RealtimeAudioManager") - - // 音频配置 - private var audioConfig: AudioConfig? - - // 录音相关 - private var audioEngine: AVAudioEngine? - private var inputNode: AVAudioInputNode? - private var recordingFormat: AVAudioFormat? - private var isRecording = false - - // 播放相关 - private var audioPlayer: AVAudioPlayerNode? - private var playbackFormat: AVAudioFormat? - private var playbackQueue: DispatchQueue - private var audioBufferQueue: [AVAudioPCMBuffer] = [] - private var isPlaying = false - - // 回调 - var onAudioData: ((Data) -> Void)? - var onStatusChanged: ((VoiceStatus) -> Void)? - var onRmsChanged: ((Float) -> Void)? - - // 录音缓冲区 - private var recordingBuffer: AVAudioPCMBuffer? - private var frameSize: Int = 320 // 20ms @ 16kHz - - init() { - playbackQueue = DispatchQueue(label: "com.realtime.playback", qos: .userInitiated) - } - - /// 激活会话,抢占音频焦点(暂停外部和内部其他音频) - private func activateAudioSession() { - do { - let session = AVAudioSession.sharedInstance() - try session.setCategory(.playAndRecord, - mode: .voiceChat, - options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP]) - try session.setActive(true, options: .notifyOthersOnDeactivation) - os_log("音频会话已激活(抢占焦点)", log: log, type: .info) - } catch { - os_log("音频会话激活失败: %@", log: log, type: .error, error.localizedDescription) - } - } - - /// 释放会话,归还音频焦点(让外部和内部其他音频恢复) - private func deactivateAudioSession() { - do { - try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) - os_log("音频会话已释放(归还焦点)", log: log, type: .info) - } catch { - os_log("音频会话释放失败: %@", log: log, type: .error, error.localizedDescription) - } - } - - /// 初始化音频管理器 - func initialize(config: AudioConfig) -> Bool { - self.audioConfig = config - self.frameSize = config.frameSize - - guard setupAudioEngine(config: config) else { - os_log("音频引擎初始化失败", log: log, type: .error) - return false - } - - guard setupAudioPlayer(config: config) else { - os_log("音频播放器初始化失败", log: log, type: .error) - return false - } - - os_log("音频管理器初始化成功", log: log, type: .info) - return true - } - - /// 设置音频引擎(录音) - private func setupAudioEngine(config: AudioConfig) -> Bool { - audioEngine = AVAudioEngine() - - guard let audioEngine = audioEngine else { return false } - - inputNode = audioEngine.inputNode - - // 创建录音格式:16kHz, 16-bit, 单声道 - recordingFormat = AVAudioFormat(commonFormat: .pcmFormatInt16, - sampleRate: Double(config.sampleRate), - channels: AVAudioChannelCount(config.channels), - interleaved: true) - - guard let recordingFormat = recordingFormat else { - os_log("无法创建录音格式", log: log, type: .error) - return false - } - - // 创建录音缓冲区 - let frameCount = AVAudioFrameCount(config.frameSize / (config.bitsPerSample / 8)) - recordingBuffer = AVAudioPCMBuffer(pcmFormat: recordingFormat, frameCapacity: frameCount) - - return true - } - - /// 设置音频播放器 - private func setupAudioPlayer(config: AudioConfig) -> Bool { - guard let audioEngine = audioEngine else { return false } - - audioPlayer = AVAudioPlayerNode() - - // 创建播放格式:16kHz, 16-bit, 单声道 - playbackFormat = AVAudioFormat(commonFormat: .pcmFormatInt16, - sampleRate: Double(config.sampleRate), - channels: AVAudioChannelCount(config.channels), - interleaved: true) - - guard let audioPlayer = audioPlayer, - let playbackFormat = playbackFormat else { - os_log("无法创建播放格式", log: log, type: .error) - return false - } - - // 连接播放器到音频引擎 - audioEngine.attach(audioPlayer) - audioEngine.connect(audioPlayer, to: audioEngine.outputNode, format: playbackFormat) - - return true - } - - /// 开始录音 - func startRecording() -> Bool { - guard let audioEngine = audioEngine, - let inputNode = inputNode, - let recordingFormat = recordingFormat, - !isRecording else { - return false - } - - // 抢占音频焦点(暂停外部和内部其他音乐) - activateAudioSession() - - // 安装录音tap - inputNode.installTap(onBus: 0, bufferSize: AVAudioFrameCount(frameSize / 2), format: recordingFormat) { [weak self] buffer, time in - self?.processRecordedAudio(buffer: buffer) - } - - do { - try audioEngine.start() - isRecording = true - onStatusChanged?(.recording) - os_log("开始录音", log: log, type: .info) - return true - } catch { - os_log("开始录音失败: %@", log: log, type: .error, error.localizedDescription) - return false - } - } - - /// 处理录制的音频数据 - private func processRecordedAudio(buffer: AVAudioPCMBuffer) { - guard let channelData = buffer.int16ChannelData?[0], - buffer.frameLength > 0 else { - return - } - - // 确保数据长度匹配frameSize - let dataLength = min(Int(buffer.frameLength) * 2, frameSize) // 2 bytes per sample - - // 转换为Data - let data = Data(bytes: channelData, count: dataLength) - - // 计算RMS值 - let rms = calculateRMS(buffer: buffer) - onRmsChanged?(rms) - - // 回调音频数据 - onAudioData?(data) - } - - /// 停止录音 - func stopRecording() -> Bool { - guard let audioEngine = audioEngine, - let inputNode = inputNode, - isRecording else { - return false - } - - inputNode.removeTap(onBus: 0) - audioEngine.stop() - isRecording = false - onStatusChanged?(.idle) - os_log("停止录音", log: log, type: .info) - - // 录音和播放都停止时归还焦点,让外部音乐恢复 - if !isPlaying { - deactivateAudioSession() - } - return true - } - - /// 播放音频数据 - func playAudioData(_ data: Data) { - guard let playbackFormat = playbackFormat, - let audioPlayer = audioPlayer else { - return - } - - playbackQueue.async { [weak self] in - self?.enqueueAudioData(data, format: playbackFormat, player: audioPlayer) - } - } - - /// 将音频数据加入播放队列 - private func enqueueAudioData(_ data: Data, format: AVAudioFormat, player: AVAudioPlayerNode) { - // 创建PCM缓冲区 - let frameCount = AVAudioFrameCount(data.count / 2) // 16-bit = 2 bytes per sample - - guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount) else { - os_log("无法创建播放缓冲区", log: log, type: .error) - return - } - - buffer.frameLength = frameCount - - // 复制数据到缓冲区 - guard let channelData = buffer.int16ChannelData?[0] else { - return - } - - data.withUnsafeBytes { bytes in - let int16Pointer = bytes.bindMemory(to: Int16.self) - channelData.update(from: int16Pointer.baseAddress!, count: Int(frameCount)) - } - - // 计算播放数据的RMS值 - let rms = calculateRMS(buffer: buffer) - DispatchQueue.main.async { [weak self] in - self?.onRmsChanged?(rms) - } - - // 加入播放队列 - audioBufferQueue.append(buffer) - - // 如果没在播放,开始播放 - if !isPlaying { - startPlayback() - } - } - - /// 开始播放 - private func startPlayback() { - guard let audioEngine = audioEngine, - let audioPlayer = audioPlayer, - !audioBufferQueue.isEmpty else { - return - } - - if !audioEngine.isRunning { - do { - try audioEngine.start() - } catch { - os_log("启动音频引擎失败: %@", log: log, type: .error, error.localizedDescription) - return - } - } - - if !audioPlayer.isPlaying { - audioPlayer.play() - } - - isPlaying = true - onStatusChanged?(.playing) - - // 播放队列中的音频 - playNextBuffer() - } - - /// 播放下一个缓冲区 - private func playNextBuffer() { - guard let audioPlayer = audioPlayer, - !audioBufferQueue.isEmpty else { - isPlaying = false - onStatusChanged?(.idle) - return - } - - let buffer = audioBufferQueue.removeFirst() - - audioPlayer.scheduleBuffer(buffer) { [weak self] in - DispatchQueue.main.async { - self?.playNextBuffer() - } - } - } - - /// 停止播放 - func stopPlaying() -> Bool { - guard let audioPlayer = audioPlayer else { - return false - } - - audioPlayer.stop() - audioBufferQueue.removeAll() - isPlaying = false - onStatusChanged?(.idle) - os_log("停止播放", log: log, type: .info) - - // 录音和播放都停止时归还焦点,让外部音乐恢复 - if !isRecording { - deactivateAudioSession() - } - return true - } - - /// 更新配置 - func updateConfig(config: AudioConfig) -> Bool { - // 如果正在录音或播放,先停止 - if isRecording { - _ = stopRecording() - } - if isPlaying { - _ = stopPlaying() - } - - return initialize(config: config) - } - - /// 释放资源 - func dispose() { - _ = stopRecording() - _ = stopPlaying() - - audioEngine?.stop() - audioEngine = nil - audioPlayer = nil - audioBufferQueue.removeAll() - - os_log("音频管理器已释放", log: log, type: .info) - } - - /// 计算音频缓冲区的RMS值 -private func calculateRMS(buffer: AVAudioPCMBuffer) -> Float { - guard let channelData = buffer.int16ChannelData?[0], - buffer.frameLength > 0 else { - return 0.0 - } - - let frameCount = Int(buffer.frameLength) - let channelDataPointer = UnsafeMutablePointer(mutating: channelData) - - // 创建浮点缓冲区用于转换 - var floatData = [Float](repeating: 0.0, count: frameCount) - - // 1. 将16位整型转换为浮点型 - vDSP_vflt16(channelDataPointer, 1, &floatData, 1, vDSP_Length(frameCount)) - - // 2. 归一化到[-1.0, 1.0]范围 - var normalizedData = [Float](repeating: 0.0, count: frameCount) - var divisor: Float = 32768.0 - vDSP_vsdiv(floatData, 1, &divisor, &normalizedData, 1, vDSP_Length(frameCount)) - - // 3. 计算均方值 - var meanSquare: Float = 0.0 - vDSP_measqv(normalizedData, 1, &meanSquare, vDSP_Length(frameCount)) - - // 4. 计算RMS值 - let rms = sqrt(meanSquare) - return rms -} -} \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimePlugin.swift b/apps/client/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimePlugin.swift deleted file mode 100644 index 23f6720b..00000000 --- a/apps/client/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimePlugin.swift +++ /dev/null @@ -1,319 +0,0 @@ -import Flutter -import UIKit -import AVFoundation -import os.log - -/** - * 实时语音聊天插件 - * - * 通过WebSocket连接Vocode服务器实现实时语音交互 - * 包含录音、播放、WebSocket通信等功能 - */ -@objc public class RealtimePlugin: NSObject, FlutterPlugin { - - private let log = OSLog(subsystem: "com.yunqiinnovation.realtime", category: "RealtimePlugin") - - // Flutter通道 - private var methodChannel: FlutterMethodChannel? - private var eventChannel: FlutterEventChannel? - private var eventSink: FlutterEventSink? - - // 核心组件 - private let audioManager = RealtimeAudioManager() - private let websocketManager = RealtimeWebSocketManager() - - // 配置参数 - private var serverUrl: String = "" - private var sampleRate: Int = 16000 - private var channels: Int = 1 - private var bitsPerSample: Int = 16 - - // 状态 - private var connectionStatus: ConnectionStatus = .disconnected - private var voiceStatus: VoiceStatus = .idle - - /// 插件注册 - public static func register(with registrar: FlutterPluginRegistrar) { - let instance = RealtimePlugin() - - // 方法通道 - let methodChannel = FlutterMethodChannel(name: "realtime/methods", binaryMessenger: registrar.messenger()) - registrar.addMethodCallDelegate(instance, channel: methodChannel) - instance.methodChannel = methodChannel - - // 事件通道 - let eventChannel = FlutterEventChannel(name: "realtime/events", binaryMessenger: registrar.messenger()) - eventChannel.setStreamHandler(instance) - instance.eventChannel = eventChannel - - // 设置组件回调 - instance.setupCallbacks() - } - - /// 设置各组件的回调 - private func setupCallbacks() { - // 音频管理器回调 - audioManager.onAudioData = { [weak self] data in - // 发送到WebSocket - self?.websocketManager.sendAudioData(data) - // 同时发送PCM数据到Flutter用于可视化 - self?.sendPCMDataToFlutter(data) - } - - audioManager.onStatusChanged = { [weak self] status in - self?.voiceStatus = status - self?.sendEvent(type: "voiceStatusChanged", data: status.rawValue) - } - - // 新增RMS回调 - audioManager.onRmsChanged = { [weak self] rms in - self?.sendEvent(type: "audioRms", data: rms) - } - - // WebSocket管理器回调 - websocketManager.onConnectionStatusChanged = { [weak self] status in - self?.connectionStatus = status - self?.sendEvent(type: "connectionStatusChanged", data: status.rawValue) - } - - websocketManager.onAudioReceived = { [weak self] data in - // 添加音频数据前50字节的日志输出 - let dataCount = min(data.count, 50) - let dataPreview = data.prefix(dataCount).map { String(format: "%02x", $0) }.joined(separator: " ") - let previewText = data.count > 50 ? dataPreview + "..." : dataPreview - self?.os_log("收到音频数据前50字节: %@", log: self?.log ?? OSLog.default, type: .debug, previewText) - self?.os_log("音频数据总长度: %d 字节", log: self?.log ?? OSLog.default, type: .debug, data.count) - - self?.audioManager.playAudioData(data) - // 同时发送接收到的PCM数据到Flutter用于可视化 - self?.sendPCMDataToFlutter(data) - } - - websocketManager.onTextReceived = { [weak self] text in - // 直接发送原始JSON文本到Flutter层处理 - self?.sendEvent(type: "textReceived", data: text) - } - - websocketManager.onError = { [weak self] error in - self?.sendEvent(type: "error", data: error) - } - } - - /// 发送事件到Flutter端 - private func sendEvent(type: String, data: Any?) { - guard let eventSink = eventSink else { return } - - let event: [String: Any] = [ - "type": type, - "data": data ?? NSNull() - ] - - DispatchQueue.main.async { - eventSink(event) - } - } - - /// 将PCM数据发送到Flutter用于可视化 - private func sendPCMDataToFlutter(_ data: Data) { - // 确保数据长度是偶数(每2个字节一个16位采样) - let validSize = data.count & ~1 // 清除最低位,确保偶数 - - guard validSize > 0 else { return } - - // 将Data转换为Int16数组 (PCM16格式) - let pcm16Array = data.prefix(validSize).withUnsafeBytes { bytes in - let int16Pointer = bytes.bindMemory(to: Int16.self) - return Array(UnsafeBufferPointer(start: int16Pointer.baseAddress, count: validSize / 2)) - } - - // 转换为Int数组以便传递给Flutter - let pcmData = pcm16Array.map { Int($0) } - sendEvent(type: "pcmData", data: pcmData) - } - - /// 辅助日志方法 - private func os_log(_ message: String, log: OSLog, type: OSLogType, _ args: CVarArg...) { - if args.isEmpty { - os_log("%@", log: log, type: type, message) - } else { - os_log(message, log: log, type: type, args) - } - } -} - -// MARK: - FlutterPlugin -extension RealtimePlugin { - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "initialize": - handleInitialize(call, result) - case "connect": - handleConnect(result) - case "disconnect": - handleDisconnect(result) - case "startRecording": - handleStartRecording(result) - case "stopRecording": - handleStopRecording(result) - case "stopPlaying": - handleStopPlaying(result) - case "getConnectionStatus": - result(connectionStatus.rawValue) - case "getVoiceStatus": - result(voiceStatus.rawValue) - case "sendTextMessage": - handleSendTextMessage(call, result) - case "setAudioConfig": - handleSetAudioConfig(call, result) - case "dispose": - handleDispose(result) - default: - result(FlutterMethodNotImplemented) - } - } - - private func handleInitialize(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - guard let args = call.arguments as? [String: Any], - let serverUrl = args["serverUrl"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "服务器地址不能为空", details: nil)) - return - } - - self.serverUrl = serverUrl - self.sampleRate = args["sampleRate"] as? Int ?? 16000 - self.channels = args["channels"] as? Int ?? 1 - self.bitsPerSample = args["bitsPerSample"] as? Int ?? 16 - - // 初始化音频管理器 - let audioConfig = AudioConfig( - sampleRate: sampleRate, - channels: channels, - bitsPerSample: bitsPerSample - ) - - let success = audioManager.initialize(config: audioConfig) - if success { - websocketManager.initialize(serverUrl: serverUrl) - os_log("实时语音插件初始化成功", log: log, type: .info) - } else { - os_log("实时语音插件初始化失败", log: log, type: .error) - } - - result(success) - } - - private func handleConnect(_ result: @escaping FlutterResult) { - websocketManager.connect { [weak self] success in - DispatchQueue.main.async { - result(success) - } - } - } - - private func handleDisconnect(_ result: @escaping FlutterResult) { - websocketManager.disconnect() - result(true) - } - - private func handleStartRecording(_ result: @escaping FlutterResult) { - let success = audioManager.startRecording() - result(success) - } - - private func handleStopRecording(_ result: @escaping FlutterResult) { - let success = audioManager.stopRecording() - result(success) - } - - private func handleStopPlaying(_ result: @escaping FlutterResult) { - let success = audioManager.stopPlaying() - result(success) - } - - private func handleSendTextMessage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - guard let args = call.arguments as? [String: Any], - let message = args["message"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "消息内容不能为空", details: nil)) - return - } - - let success = websocketManager.sendTextMessage(message) - result(success) - } - - private func handleSetAudioConfig(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - guard let args = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "参数无效", details: nil)) - return - } - - if let sampleRate = args["sampleRate"] as? Int { - self.sampleRate = sampleRate - } - if let channels = args["channels"] as? Int { - self.channels = channels - } - if let bitsPerSample = args["bitsPerSample"] as? Int { - self.bitsPerSample = bitsPerSample - } - - let audioConfig = AudioConfig( - sampleRate: sampleRate, - channels: channels, - bitsPerSample: bitsPerSample - ) - - let success = audioManager.updateConfig(config: audioConfig) - result(success) - } - - private func handleDispose(_ result: @escaping FlutterResult) { - audioManager.dispose() - websocketManager.dispose() - result(nil) - } -} - -// MARK: - FlutterStreamHandler -extension RealtimePlugin: FlutterStreamHandler { - - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - self.eventSink = events - return nil - } - - public func onCancel(withArguments arguments: Any?) -> FlutterError? { - self.eventSink = nil - return nil - } -} - -// MARK: - 枚举定义 - -enum ConnectionStatus: String { - case disconnected = "disconnected" - case connecting = "connecting" - case connected = "connected" - case error = "error" -} - -enum VoiceStatus: String { - case idle = "idle" - case recording = "recording" - case processing = "processing" - case playing = "playing" -} - -// MARK: - 音频配置 - -struct AudioConfig { - let sampleRate: Int - let channels: Int - let bitsPerSample: Int - - var frameSize: Int { - // 20ms的帧大小 - return (sampleRate * channels * bitsPerSample / 8 * 20) / 1000 - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeWebSocketManager.swift b/apps/client/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeWebSocketManager.swift deleted file mode 100644 index 03910198..00000000 --- a/apps/client/local_plugins/realtime/ios/realtime/Sources/realtime/RealtimeWebSocketManager.swift +++ /dev/null @@ -1,323 +0,0 @@ -import Foundation -import Network -import os.log - -/** - * 实时WebSocket管理器 - * - * 负责与Vocode服务器的WebSocket通信,对应Android示例中的OkHttp WebSocket功能 - */ -class RealtimeWebSocketManager: NSObject { - - private let log = OSLog(subsystem: "com.yunqiinnovation.realtime", category: "RealtimeWebSocketManager") - - // WebSocket连接 - private var webSocketTask: URLSessionWebSocketTask? - private var urlSession: URLSession? - - // 服务器URL - private var serverUrl: String = "" - - // 连接状态 - private var isConnected = false - private var isConnecting = false - - // 回调 - var onConnectionStatusChanged: ((ConnectionStatus) -> Void)? - var onAudioReceived: ((Data) -> Void)? - var onTextReceived: ((String) -> Void)? - var onError: ((String) -> Void)? - - // 发送队列 - private let sendQueue = DispatchQueue(label: "com.realtime.websocket.send", qos: .userInitiated) - private let receiveQueue = DispatchQueue(label: "com.realtime.websocket.receive", qos: .userInitiated) - - override init() { - super.init() - setupURLSession() - } - - /// 设置URL会话 - private func setupURLSession() { - let config = URLSessionConfiguration.default - config.timeoutIntervalForRequest = 30 - config.timeoutIntervalForResource = 60 - urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil) - } - - /// 初始化WebSocket管理器 - func initialize(serverUrl: String) { - self.serverUrl = serverUrl - os_log("WebSocket管理器初始化,服务器地址: %@", log: log, type: .info, serverUrl) - } - - /// 连接到服务器 - func connect(completion: @escaping (Bool) -> Void) { - guard !isConnected && !isConnecting else { - completion(false) - return - } - - guard let url = URL(string: serverUrl) else { - os_log("无效的服务器地址: %@", log: log, type: .error, serverUrl) - onError?("无效的服务器地址") - completion(false) - return - } - - isConnecting = true - onConnectionStatusChanged?(.connecting) - - // 创建WebSocket任务 - var request = URLRequest(url: url) - request.timeoutInterval = 30 - - webSocketTask = urlSession?.webSocketTask(with: request) - - // 开始监听消息 - startListening() - - // 开始连接 - webSocketTask?.resume() - - // 等待连接建立 - DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { [weak self] in - self?.checkConnection(completion: completion) - } - } - - /// 检查连接状态 - private func checkConnection(completion: @escaping (Bool) -> Void) { - guard let webSocketTask = webSocketTask else { - isConnecting = false - onConnectionStatusChanged?(.error) - completion(false) - return - } - - switch webSocketTask.state { - case .running: - isConnected = true - isConnecting = false - onConnectionStatusChanged?(.connected) - os_log("WebSocket连接成功", log: log, type: .info) - completion(true) - case .suspended: - // 等待连接完成 - DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.checkConnection(completion: completion) - } - default: - isConnecting = false - onConnectionStatusChanged?(.error) - os_log("WebSocket连接失败", log: log, type: .error) - completion(false) - } - } - - /// 开始监听消息 - private func startListening() { - receiveMessage() - } - - /// 接收消息 - private func receiveMessage() { - webSocketTask?.receive { [weak self] result in - switch result { - case .success(let message): - self?.handleReceivedMessage(message) - // 继续监听下一条消息 - self?.receiveMessage() - case .failure(let error): - self?.handleReceiveError(error) - } - } - } - - /// 处理接收到的消息 - private func handleReceivedMessage(_ message: URLSessionWebSocketTask.Message) { - switch message { - case .data(let data): - // 保留对二进制数据的支持以防万一 - onAudioReceived?(data) - case .string(let text): - // 添加收到文本消息的日志输出,显示前20字符 - let textPreview = text.count > 20 ? String(text.prefix(20)) + "..." : text - os_log("收到文本消息前20字符: %@", log: log, type: .debug, textPreview) - - // 尝试解析为JSON来检查消息类型 - do { - if let jsonData = text.data(using: .utf8), - let json = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any], - let messageType = json["type"] as? String { - - // 添加消息类型的日志输出 - os_log("消息类型: %@", log: log, type: .debug, messageType) - - switch messageType { - case "websocket_audio": - // 处理音频消息 - if let audioDataString = json["data"] as? String, - let audioData = Data(base64Encoded: audioDataString) { - onAudioReceived?(audioData) - } - case "websocket_transcript": - // 处理统一的文本消息格式 - // 包含sender、is_final、timestamp等字段 - onTextReceived?(text) - case "websocket_ready": - // 连接就绪消息 - os_log("WebSocket连接就绪", log: log, type: .info) - case "websocket_stop": - // 会话停止消息 - os_log("会话已结束", log: log, type: .info) - case "websocket_start": - // 会话开始消息 - os_log("会话已开始", log: log, type: .info) - default: - // 其他消息类型,当作普通文本消息处理 - onTextReceived?(text) - } - } else { - // 如果不是JSON格式,当作普通文本消息处理 - os_log("非JSON格式消息,当作普通文本处理", log: log, type: .debug) - onTextReceived?(text) - } - } catch { - // 如果解析失败,当作普通文本消息处理 - os_log("非JSON格式消息,当作普通文本处理", log: log, type: .debug) - onTextReceived?(text) - } - @unknown default: - os_log("收到未知类型消息", log: log, type: .error) - } - } - - /// 处理接收错误 - private func handleReceiveError(_ error: Error) { - os_log("WebSocket接收错误: %@", log: log, type: .error, error.localizedDescription) - - // 如果连接断开,尝试重连 - if isConnected { - isConnected = false - onConnectionStatusChanged?(.error) - onError?("连接断开: \(error.localizedDescription)") - } - } - - /// 发送音频数据 - func sendAudioData(_ data: Data) { - guard isConnected else { return } - - sendQueue.async { [weak self] in - // 将音频数据编码为Base64并包装成Vocode WebSocket格式 - let base64AudioData = data.base64EncodedString() - let audioMessage: [String: Any] = [ - "type": "websocket_audio", - "data": base64AudioData - ] - - do { - let jsonData = try JSONSerialization.data(withJSONObject: audioMessage) - if let jsonString = String(data: jsonData, encoding: .utf8) { - self?.webSocketTask?.send(.string(jsonString)) { error in - if let error = error { - self?.os_log("发送音频数据失败: %@", log: self?.log ?? OSLog.default, type: .error, error.localizedDescription) - } - } - } - } catch { - self?.os_log("音频数据JSON序列化失败: %@", log: self?.log ?? OSLog.default, type: .error, error.localizedDescription) - } - } - } - - /// 发送文本消息 - func sendTextMessage(_ message: String) -> Bool { - guard isConnected else { - os_log("WebSocket未连接,无法发送文本消息", log: log, type: .error) - return false - } - - sendQueue.async { [weak self] in - self?.webSocketTask?.send(.string(message)) { error in - if let error = error { - self?.os_log("发送文本消息失败: %@", log: self?.log ?? OSLog.default, type: .error, error.localizedDescription) - } - } - } - - return true - } - - /// 断开连接 - func disconnect() { - guard isConnected || isConnecting else { return } - - webSocketTask?.cancel(with: .goingAway, reason: nil) - webSocketTask = nil - - isConnected = false - isConnecting = false - onConnectionStatusChanged?(.disconnected) - os_log("WebSocket已断开连接", log: log, type: .info) - } - - /// 释放资源 - func dispose() { - disconnect() - urlSession?.invalidateAndCancel() - urlSession = nil - os_log("WebSocket管理器已释放", log: log, type: .info) - } - - // 修复方法访问性问题 - private func os_log(_ message: String, log: OSLog, type: OSLogType, _ args: CVarArg...) { - if args.isEmpty { - os_log("%@", log: log, type: type, message) - } else { - os_log(message, log: log, type: type, args) - } - } -} - -// MARK: - URLSessionWebSocketDelegate -extension RealtimeWebSocketManager: URLSessionWebSocketDelegate { - - func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { - os_log("WebSocket连接已建立", log: log, type: .info) - isConnected = true - isConnecting = false - onConnectionStatusChanged?(.connected) - } - - func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { - os_log("WebSocket连接已关闭,关闭码: %d", log: log, type: .info, closeCode.rawValue) - isConnected = false - isConnecting = false - onConnectionStatusChanged?(.disconnected) - } -} - -// MARK: - URLSessionDelegate -extension RealtimeWebSocketManager: URLSessionDelegate { - - func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { - if let error = error { - os_log("URLSession失效: %@", log: log, type: .error, error.localizedDescription) - onError?("会话失效: \(error.localizedDescription)") - } - } - - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - if let error = error { - os_log("URLSessionTask完成时出错: %@", log: log, type: .error, error.localizedDescription) - if isConnected || isConnecting { - isConnected = false - isConnecting = false - onConnectionStatusChanged?(.error) - onError?("连接错误: \(error.localizedDescription)") - } - } - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/lib/realtime.dart b/apps/client/local_plugins/realtime/lib/realtime.dart deleted file mode 100644 index 237e9e3e..00000000 --- a/apps/client/local_plugins/realtime/lib/realtime.dart +++ /dev/null @@ -1,243 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:flutter/services.dart'; - -/// 实时语音服务异常 -class RealtimeException implements Exception { - final String message; - - RealtimeException(this.message); - - @override - String toString() => 'RealtimeException: $message'; -} - -/// 连接状态 -enum ConnectionStatus { - disconnected, - connecting, - connected, - error, -} - -/// 语音状态 -enum VoiceStatus { - idle, - recording, - processing, - playing, -} - -/// 实时语音事件 -class RealtimeEvent { - final String type; - final dynamic data; - - RealtimeEvent({required this.type, this.data}); - - factory RealtimeEvent.fromMap(Map map) { - return RealtimeEvent( - type: map['type'] as String, - data: map['data'], - ); - } -} - -/// 实时语音聊天服务 -class RealtimeService { - static const MethodChannel _channel = MethodChannel('realtime/methods'); - static const EventChannel _eventChannel = EventChannel('realtime/events'); - - StreamController? _eventStreamController; - Stream? _eventStream; - - /// 获取事件流 - Stream get eventStream { - if (_eventStream == null) { - _eventStreamController = StreamController.broadcast(); - _eventStream = _eventStreamController!.stream; - - // 监听原生事件 - _eventChannel.receiveBroadcastStream().listen( - (dynamic event) { - if (event is Map) { - final eventMap = Map.from(event); - final realtimeEvent = RealtimeEvent.fromMap(eventMap); - _eventStreamController!.add(realtimeEvent); - } - }, - onError: (error) { - _eventStreamController!.addError(RealtimeException('事件流错误: $error')); - }, - ); - } - - return _eventStream!; - } - - /// 初始化实时语音服务 - /// - /// [serverUrl] WebSocket服务器地址 - /// [sampleRate] 采样率,默认16000 - /// [channels] 声道数,默认1(单声道) - /// [bitsPerSample] 位深,默认16 - Future initialize({ - required String serverUrl, - int sampleRate = 16000, - int channels = 1, - int bitsPerSample = 16, - }) async { - try { - final result = await _channel.invokeMethod( - 'initialize', - { - 'serverUrl': serverUrl, - 'sampleRate': sampleRate, - 'channels': channels, - 'bitsPerSample': bitsPerSample, - }, - ); - - return result ?? false; - } catch (e) { - throw RealtimeException('初始化失败: $e'); - } - } - - /// 连接到服务器 - Future connect() async { - try { - final result = await _channel.invokeMethod('connect'); - return result ?? false; - } catch (e) { - throw RealtimeException('连接失败: $e'); - } - } - - /// 断开连接 - Future disconnect() async { - try { - final result = await _channel.invokeMethod('disconnect'); - return result ?? false; - } catch (e) { - throw RealtimeException('断开连接失败: $e'); - } - } - - /// 开始录音 - Future startRecording() async { - try { - final result = await _channel.invokeMethod('startRecording'); - return result ?? false; - } catch (e) { - throw RealtimeException('开始录音失败: $e'); - } - } - - /// 停止录音 - Future stopRecording() async { - try { - final result = await _channel.invokeMethod('stopRecording'); - return result ?? false; - } catch (e) { - throw RealtimeException('停止录音失败: $e'); - } - } - - /// 停止播放 - Future stopPlaying() async { - try { - final result = await _channel.invokeMethod('stopPlaying'); - return result ?? false; - } catch (e) { - throw RealtimeException('停止播放失败: $e'); - } - } - - /// 获取连接状态 - Future getConnectionStatus() async { - try { - final result = await _channel.invokeMethod('getConnectionStatus'); - switch (result) { - case 'disconnected': - return ConnectionStatus.disconnected; - case 'connecting': - return ConnectionStatus.connecting; - case 'connected': - return ConnectionStatus.connected; - case 'error': - return ConnectionStatus.error; - default: - return ConnectionStatus.disconnected; - } - } catch (e) { - throw RealtimeException('获取连接状态失败: $e'); - } - } - - /// 获取语音状态 - Future getVoiceStatus() async { - try { - final result = await _channel.invokeMethod('getVoiceStatus'); - switch (result) { - case 'idle': - return VoiceStatus.idle; - case 'recording': - return VoiceStatus.recording; - case 'processing': - return VoiceStatus.processing; - case 'playing': - return VoiceStatus.playing; - default: - return VoiceStatus.idle; - } - } catch (e) { - throw RealtimeException('获取语音状态失败: $e'); - } - } - - /// 发送文本消息 - Future sendTextMessage(String message) async { - try { - final result = await _channel.invokeMethod( - 'sendTextMessage', - {'message': message}, - ); - return result ?? false; - } catch (e) { - throw RealtimeException('发送文本消息失败: $e'); - } - } - - /// 设置音频参数 - Future setAudioConfig({ - int? sampleRate, - int? channels, - int? bitsPerSample, - }) async { - try { - final params = {}; - if (sampleRate != null) params['sampleRate'] = sampleRate; - if (channels != null) params['channels'] = channels; - if (bitsPerSample != null) params['bitsPerSample'] = bitsPerSample; - - final result = await _channel.invokeMethod('setAudioConfig', params); - return result ?? false; - } catch (e) { - throw RealtimeException('设置音频参数失败: $e'); - } - } - - /// 释放资源 - Future dispose() async { - try { - await _channel.invokeMethod('dispose'); - _eventStreamController?.close(); - _eventStreamController = null; - _eventStream = null; - } catch (e) { - throw RealtimeException('释放资源失败: $e'); - } - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/realtime/pubspec.yaml b/apps/client/local_plugins/realtime/pubspec.yaml deleted file mode 100644 index 0eed88a7..00000000 --- a/apps/client/local_plugins/realtime/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: realtime -description: 实时语音聊天插件,通过WebSocket连接Vocode服务器实现语音交互 -version: 0.0.1 -homepage: - -environment: - sdk: ">=2.17.0 <3.0.0" - flutter: ">=2.5.0" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^2.0.0 - -# Flutter插件配置 -flutter: - plugin: - platforms: - android: - package: com.yunqiinnovation.realtime - pluginClass: RealtimePlugin - ios: - pluginClass: RealtimePlugin - swiftPackage: - path: ios/realtime \ No newline at end of file diff --git a/apps/client/local_plugins/volcano_speech/LICENSE b/apps/client/local_plugins/volcano_speech/LICENSE deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/client/local_plugins/volcano_speech/README.md b/apps/client/local_plugins/volcano_speech/README.md deleted file mode 100644 index 572317b5..00000000 --- a/apps/client/local_plugins/volcano_speech/README.md +++ /dev/null @@ -1,206 +0,0 @@ -# 火山引擎语音服务插件 - -基于火山引擎语音SDK封装的Flutter插件,支持Android平台。 - -## 功能 -- 语音合成(TTS) - - 支持在线/离线/混合合成模式 - - 支持SSML格式文本 - - 支持情感合成和情感预测 - - 支持音量、语速、音高等多种参数调节 - - 复刻音色支持 - - 离线资源管理 -- 语音识别(ASR) - - 一次性识别 - - 连续识别 - - 长按识别 - - 热词优化 - - 多语言支持 - -## 使用说明 - -### TTS 语音合成 - -#### 基础用法 -```dart -import 'package:volcano_speech/volcano_speech.dart'; - -// 初始化 -await VolcanoSpeech.initialize(appId: 'YOUR_APP_ID', apiKey: 'YOUR_API_KEY'); - -// 设置音量、语速等参数 -await VolcanoSpeech.setSpeechParams( - rate: 0, // 语速: -500到500,0为正常速度 - volume: 100, // 音量: 0到100,默认100 - pitch: 0, // 音高: -500到500,0为正常音高 - silenceDuration: 500, // 静音段时长: 毫秒 -); - -// 设置音色 -await VolcanoSpeech.setVoice( - voiceName: 'zh_female_qingxin', - voiceType: 'qingxin' -); - -// 开始合成并播放 -await VolcanoSpeech.speakText(text: '这是一段测试文本'); - -// 暂停播放 -await VolcanoSpeech.pausePlayback(); - -// 继续播放 -await VolcanoSpeech.resumePlayback(); - -// 停止播放 -await VolcanoSpeech.stopSpeaking(); - -// 销毁引擎 -await VolcanoSpeech.dispose(); -``` - -#### 进阶用法 -```dart -// 设置工作模式 -await VolcanoSpeech.setWorkMode(mode: TtsWorkMode.alternate); // 先在线,断网时切换离线 - -// 设置离线发音人 -await VolcanoSpeech.setOfflineVoice( - voiceName: 'xifei', - voiceType: 'qingxin' -); - -// 下载离线资源 -await VolcanoSpeech.downloadOfflineResource( - voiceTypes: ['qingxin', 'zhenjiang'], - languages: ['zh-CN'] -); - -// 使用SSML格式文本 -await VolcanoSpeech.setTextType(type: TtsTextType.ssml); -await VolcanoSpeech.speakText( - text: '这是一段2023-10-01的语音合成' -); - -// 设置情感 -await VolcanoSpeech.setEmotion(emotion: 'happy'); - -// 启用情感预测 -await VolcanoSpeech.setEnableEmotionPredict(enable: true); - -// 启用服务端缓存 -await VolcanoSpeech.setEnableCache(enable: true); - -// 监听TTS进度事件 -VolcanoSpeech.ttsProgressEvents.listen((event) { - print('播放进度: ${(event.progress * 100).toStringAsFixed(1)}%'); -}); - -// 复刻音色支持 -await VolcanoSpeech.setEnableVoiceClone( - enable: true, - backendCluster: 'your_cluster_name' -); -``` - -### ASR 语音识别 -#### 一次性识别 -```dart -import 'package:volcano_speech/volcano_speech.dart'; - -// 初始化 -await VolcanoSpeech.initializeAsr( - appId: 'YOUR_APP_ID', - apiKey: 'YOUR_API_KEY', - supportedLanguages: ['zh-CN'], -); - -// 设置识别语言 -await VolcanoSpeech.setAsrLanguage(language: 'zh-CN'); - -// 设置热词(可选) -await VolcanoSpeech.setAsrHotWords( - hotWords: '{"hotwords":[{"word":"火山引擎","scale":2.0}]}', -); - -// 开始一次性识别 -try { - final result = await VolcanoSpeech.recognizeOnce(); - print('识别结果: ${result['text']}, 语言: ${result['language']}'); -} catch (e) { - print('识别出错: $e'); -} - -// 销毁引擎 -await VolcanoSpeech.disposeAsr(); -``` - -#### 连续识别 -```dart -import 'package:volcano_speech/volcano_speech.dart'; - -// 初始化 -await VolcanoSpeech.initializeAsr( - appId: 'YOUR_APP_ID', - apiKey: 'YOUR_API_KEY', -); - -// 监听识别事件 -VolcanoSpeech.asrEvents.listen((event) { - switch (event.type) { - case AsrEventType.sessionStarted: - print('识别会话开始'); - break; - case AsrEventType.sessionStopped: - print('识别会话结束'); - break; - case AsrEventType.recognizing: - print('正在识别: ${event.text}'); - break; - case AsrEventType.result: - print('识别结果: ${event.text}'); - break; - case AsrEventType.volumeChanged: - print('音量: ${event.volume}'); - break; - case AsrEventType.error: - print('识别错误: ${event.errorMessage}'); - break; - } -}); - -// 开始连续识别 -await VolcanoSpeech.startContinuousRecognition(); - -// 检查是否正在识别 -final isActive = await VolcanoSpeech.isContinuousRecognitionActive(); -print('是否正在识别: $isActive'); - -// 停止连续识别 -await VolcanoSpeech.stopContinuousRecognition(); -``` - -#### 长按识别 -```dart -import 'package:volcano_speech/volcano_speech.dart'; - -// 初始化 -await VolcanoSpeech.initializeAsr( - appId: 'YOUR_APP_ID', - apiKey: 'YOUR_API_KEY', -); - -// 监听识别事件 -VolcanoSpeech.asrEvents.listen((event) { - // 处理事件... -}); - -// 用户按下按钮时开始识别 -onPressed: () async { - await VolcanoSpeech.startListening(); -}, - -// 用户释放按钮时停止识别 -onReleased: () async { - await VolcanoSpeech.stopListening(); -}, -``` \ No newline at end of file diff --git a/apps/client/local_plugins/volcano_speech/android/build.gradle.kts b/apps/client/local_plugins/volcano_speech/android/build.gradle.kts deleted file mode 100644 index 54987bb9..00000000 --- a/apps/client/local_plugins/volcano_speech/android/build.gradle.kts +++ /dev/null @@ -1,64 +0,0 @@ -buildscript { - repositories { - google() - mavenCentral() - } - dependencies { - classpath("com.android.tools.build:gradle:7.3.1") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.21") - } -} - -plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") -} - -android { - namespace = "com.yunqiinnovation.volcano_speech" - compileSdk = 35 - - defaultConfig { - minSdk = 21 - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - kotlinOptions { - jvmTarget = "11" - } - - sourceSets { - getByName("main") { - manifest.srcFile("src/main/AndroidManifest.xml") - java.srcDirs("src/main/kotlin") - } - } - - // 添加lint选项 - lintOptions { - isCheckReleaseBuilds = false - } - -} - -dependencies { - // Flutter插件依赖 - // compileOnly("io.flutter:flutter_embedding_debug:1.0.0-a794cf2681") - debugImplementation("io.flutter:flutter_embedding_debug:1.0.0-a794cf2681") - releaseImplementation("io.flutter:flutter_embedding_release:1.0.0-a794cf2681") - - // 添加火山引擎语音合成SDK - 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 { - google() - mavenCentral() -} \ No newline at end of file diff --git a/apps/client/local_plugins/volcano_speech/android/settings.gradle.kts b/apps/client/local_plugins/volcano_speech/android/settings.gradle.kts deleted file mode 100644 index 7f534286..00000000 --- a/apps/client/local_plugins/volcano_speech/android/settings.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = "volcano_speech" \ No newline at end of file diff --git a/apps/client/local_plugins/volcano_speech/android/src/main/AndroidManifest.xml b/apps/client/local_plugins/volcano_speech/android/src/main/AndroidManifest.xml deleted file mode 100644 index 338e8293..00000000 --- a/apps/client/local_plugins/volcano_speech/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt b/apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt deleted file mode 100644 index aeaae51b..00000000 --- a/apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt +++ /dev/null @@ -1,764 +0,0 @@ -package com.yunqiinnovation.volcano_speech - -import android.content.Context -import android.os.Handler -import android.os.Looper -import com.bytedance.speech.speechengine.SpeechEngine -import com.bytedance.speech.speechengine.SpeechEngineDefines -import com.bytedance.speech.speechengine.SpeechEngineGenerator -import android.util.Log -import org.json.JSONException -import org.json.JSONObject -import org.json.JSONArray -import java.nio.ByteBuffer -import java.nio.ByteOrder - -/** - * 火山语音识别帮助类 (大模型版本) - */ -class VolcanoAsrHelper(private val context: Context) { - private val TAG = "VolcanoAsrHelper" - private val mainHandler = Handler(Looper.getMainLooper()) - - // 语音引擎相关 - private var engine: SpeechEngine? = null - private var engineHandler: Long = -1 - private var isInitialized = false - - // 当前回调 - private var recognizeCallback: RecognizeCallback? = null - private var continuousCallback: ContinuousRecognizeCallback? = null - - // 识别状态 - private var isRecognizing = false - private var isEngineStarted = false - private var isContinuousMode = false - - // 配置参数 - private var language = "zh-CN" - private var recorderType = SpeechEngineDefines.RECORDER_TYPE_RECORDER - - // 音频参数 - private var sampleRate = 16000 - private var channels = 1 - private var needResample = false - - /** - * ASR识别状态 - */ - enum class AsrState { - IDLE, // 空闲状态 - INITIALIZING, // 初始化中 - READY, // 就绪(已初始化) - RECOGNIZING, // 识别中 - ERROR // 错误状态 - } - - /** - * 当前ASR状态 - */ - var state: AsrState = AsrState.IDLE - private set - - /** - * 一次性识别回调接口 - */ - interface RecognizeCallback { - fun onResult(text: String, detectedLanguage: String) - fun onError(error: String) - } - - /** - * 连续识别回调接口 - */ - interface ContinuousRecognizeCallback { - fun onResult(text: String, detectedLanguage: String) - fun onRecognizing(recognizing: String, detectedLanguage: String) - fun onSessionStarted() - fun onSessionStopped() - fun onError(error: String) - } - - /** - * 初始化语音识别引擎 - */ - fun initialize(appId: String, token: String, resourceId: String = "volc.bigasr.sauc.duration"): Boolean { - if (isInitialized) { - Log.i(TAG, "引擎已经初始化") - return true - } - - state = AsrState.INITIALIZING - try { - // 准备环境 - try { - SpeechEngineGenerator.PrepareEnvironment(context, null) - } catch (e: Exception) { - Log.w(TAG, "准备环境时发生异常: ${e.message},尝试继续初始化") - } - - // 创建引擎 - engine = SpeechEngineGenerator.getInstance() - engineHandler = engine?.createEngine() ?: -1 - - if (engineHandler == -1L) { - Log.e(TAG, "创建引擎失败") - state = AsrState.ERROR - return false - } - - // 设置上下文 - engine?.setContext(context) - - // 设置引擎类型为ASR - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ENGINE_NAME_STRING, SpeechEngineDefines.ASR_ENGINE) - - // 设置日志级别 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING, SpeechEngineDefines.LOG_LEVEL_WARN) - - // 设置用户ID和设备ID - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_UID_STRING, "user_id") - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_DEVICE_ID_STRING, "device_id") - - // 设置鉴权信息 - 大模型版本不需要Bearer前缀 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, appId) - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, token) - - // 设置资源ID - 大模型必需 - // 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?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_ADDRESS_STRING, "wss://openspeech.bytedance.com") - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_URI_STRING, "/api/v3/sauc/bigmodel") - - // 设置超时时间 - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_CONN_TIMEOUT_INT, 12000) - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_RECV_TIMEOUT_INT, 8000) - - // 默认设置音频来源为录音机 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RECORDER_TYPE_STRING, recorderType) - - // 设置最大录音时长 (默认60秒) - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_VAD_MAX_SPEECH_DURATION_INT, 60000) - - // 设置VAD静音判停时间,用户说话后停顿多久视为说话结束 - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_VAD_END_SILENCE_TIME_INT, 600) - - // 启用utterances返回 - // engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_SHOW_UTTERANCES_BOOL, true) - // 设置VAD头部静音时间,用于过滤开始的杂音 - // engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_VAD_START_SILENCE_TIME_INT, 500) - - // 设置回声消除 - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_RECORDER_PRESET_INT, SpeechEngineDefines.RECORDER_PRESET_VOICE_COMMUNICATION) - - // 设置音频参数 - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_SAMPLE_RATE_INT, sampleRate) - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_CHANNEL_NUM_INT, channels) - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_UP_CHANNEL_NUM_INT, channels) - - // 启用语言检测 - engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_SHOW_LANG_BOOL, true) - - // 启用标点符号返回(默认启用) - engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_SHOW_NLU_PUNC_BOOL, true) - engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_ENABLE_DDC_BOOL, true); - - // 使用增量返回模式,避免内存增长 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_RESULT_TYPE_STRING, - SpeechEngineDefines.ASR_RESULT_TYPE_SINGLE) - - // 初始化引擎 - val result = engine?.initEngine(engineHandler) - isInitialized = result == SpeechEngineDefines.ERR_NO_ERROR - - if (isInitialized) { - Log.i(TAG, "引擎初始化成功") - state = AsrState.READY - - // 设置回调监听 - engine?.setListener(object : SpeechEngine.SpeechListener { - override fun onSpeechMessage(type: Int, data: ByteArray, len: Int) { - val stdData = String(data) - handleEngineEvent(type, stdData) - } - }) - - return true - } else { - Log.e(TAG, "引擎初始化失败: $result") - state = AsrState.ERROR - return false - } - } catch (e: Exception) { - Log.e(TAG, "初始化异常: ${e.message}", e) - state = AsrState.ERROR - return false - } - } - - /** - * 设置音频输入类型 - */ - fun setAudioInputType(type: String): Boolean { - if (!isInitialized) { - Log.e(TAG, "引擎未初始化") - return false - } - - if (isEngineStarted) { - Log.e(TAG, "引擎已启动,无法更改配置") - return false - } - - try { - when (type) { - "mic" -> recorderType = SpeechEngineDefines.RECORDER_TYPE_RECORDER - "stream" -> recorderType = SpeechEngineDefines.RECORDER_TYPE_STREAM - else -> { - Log.e(TAG, "不支持的音频输入类型: $type") - return false - } - } - - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RECORDER_TYPE_STRING, recorderType) - return true - } catch (e: Exception) { - Log.e(TAG, "设置音频输入类型失败: ${e.message}", e) - return false - } - } - - /** - * 设置音频参数 - */ - fun setAudioParams(sampleRate: Int, channels: Int): Boolean { - if (!isInitialized) { - Log.e(TAG, "引擎未初始化") - return false - } - - if (isEngineStarted) { - Log.e(TAG, "引擎已启动,无法更改配置") - return false - } - - try { - this.sampleRate = sampleRate - this.channels = channels - - // 设置采样率和通道数 - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_SAMPLE_RATE_INT, sampleRate) - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_CHANNEL_NUM_INT, channels) - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_UP_CHANNEL_NUM_INT, channels) - - // 配置重采样参数 - needResample = sampleRate != 16000 || channels != 1 - if (needResample) { - engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_ENABLE_RESAMPLER_BOOL, true) - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT, sampleRate) - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_CUSTOM_CHANNEL_INT, channels) - } else { - engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_ENABLE_RESAMPLER_BOOL, false) - } - - return true - } catch (e: Exception) { - Log.e(TAG, "设置音频参数失败: ${e.message}", e) - return false - } - } - - /** - * 设置语言 - */ - fun setLanguage(language: String): Boolean { - if (!isInitialized) { - Log.e(TAG, "引擎未初始化") - return false - } - - try { - this.language = language - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_LANGUAGE_STRING, language) - return true - } catch (e: Exception) { - Log.e(TAG, "设置语言失败: ${e.message}", e) - return false - } - } - - /** - * 设置热词 - */ - fun setHotWords(hotWordsId: String): Boolean { - if (!isInitialized) { - Log.e(TAG, "引擎未初始化") - return false - } - - try { - if (hotWordsId.isNotEmpty()) { - // 大模型ASR需要通过请求参数设置热词 - val reqParams = "{\"corpus\":{\"boosting_table_id\":\"$hotWordsId\"}}" - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_REQ_PARAMS_STRING, reqParams) - } - return true - } catch (e: Exception) { - Log.e(TAG, "设置热词失败: ${e.message}", e) - return false - } - } - - /** - * 一次性识别(异步) - */ - fun recognizeOnce(callback: RecognizeCallback): Boolean { - Log.i(TAG, "开始一次性识别") - if (!isInitialized) { - Log.e(TAG, "引擎未初始化") - return false - } - - if (isRecognizing) { - Log.e(TAG, "当前已在识别中") - return false - } - - try { - this.recognizeCallback = callback - this.continuousCallback = null - this.isContinuousMode = false - - // 停止当前引擎 - engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "") - - // 开启自动判停,达到静音时间后自动停止识别 - engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_AUTO_STOP_BOOL, true) - - - - // 启动引擎开始识别 - val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") - - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - Log.e(TAG, "启动识别失败: $ret") - return false - } - - isRecognizing = true - isEngineStarted = true - state = AsrState.RECOGNIZING - return true - } catch (e: Exception) { - Log.e(TAG, "识别异常: ${e.message}", e) - return false - } - } - - /** - * 停止一次性识别 - */ - fun stopRecognize(): Boolean { - if (!isInitialized) { - Log.e(TAG, "引擎未初始化") - return false - } - - if (!isEngineStarted) { - Log.e(TAG, "引擎未启动") - return false - } - - try { - if (recorderType == SpeechEngineDefines.RECORDER_TYPE_RECORDER) { - // 麦克风输入-告知引擎音频输入完成 - val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_FINISH_TALKING, "") - - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - Log.e(TAG, "停止识别失败: $ret") - return false - } - } else { - // 流式输入-需要完全停止引擎 - val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - Log.e(TAG, "停止识别失败: $ret") - return false - } - - isRecognizing = false - isEngineStarted = false - state = AsrState.READY - } - - return true - } catch (e: Exception) { - Log.e(TAG, "停止识别异常: ${e.message}", e) - return false - } - } - - /** - * 开始连续识别 - */ - fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { - Log.i(TAG, "开始连续识别") - if (!isInitialized) { - Log.e(TAG, "引擎未初始化") - return false - } - - if (isRecognizing) { - Log.e(TAG, "当前已在识别中") - return false - } - - try { - this.continuousCallback = callback - this.recognizeCallback = null - this.isContinuousMode = true - - // 停止当前引擎 - engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "") - - // 关闭自动判停,持续接收语音直到主动关闭 - engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_AUTO_STOP_BOOL, false) - - - - - // 启动引擎开始识别 - val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") - - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - Log.e(TAG, "启动连续识别失败: $ret") - return false - } - - isRecognizing = true - isEngineStarted = true - state = AsrState.RECOGNIZING - Log.i(TAG, "开始连续识别") - return true - } catch (e: Exception) { - Log.e(TAG, "连续识别异常: ${e.message}", e) - return false - } - } - - /** - * 停止连续识别 - */ - fun stopContinuousRecognition(): Boolean { - if (!isInitialized || !isRecognizing) { - Log.e(TAG, "引擎未初始化或未在识别中") - return false - } - - try { - // 停止引擎 - val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - Log.e(TAG, "停止连续识别失败: $ret") - return false - } - - isRecognizing = false - isEngineStarted = false - state = AsrState.READY - return true - } catch (e: Exception) { - Log.e(TAG, "停止连续识别异常: ${e.message}", e) - return false - } - } - - /** - * 发送PCM音频数据到引擎 - * 注意:必须在setAudioInputType("stream")后,才能调用此方法 - */ - fun feedAudio(data: ByteArray): Boolean { - if (!isInitialized) { - Log.e(TAG, "引擎未初始化") - return false - } - - if (!isEngineStarted) { - Log.e(TAG, "引擎未启动") - return false - } - - if (recorderType != SpeechEngineDefines.RECORDER_TYPE_STREAM) { - Log.e(TAG, "当前录音类型不是stream模式,无法使用此方法") - return false - } - - try { - // 调用引擎feedAudio方法 - val ret = engine?.feedAudio(engineHandler, data, data.size) - - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - Log.e(TAG, "发送音频数据失败: $ret") - return false - } - - return true - } catch (e: Exception) { - Log.e(TAG, "发送音频数据异常: ${e.message}", e) - return false - } - } - - /** - * 发送任意格式音频的最后一块数据 - * 告知引擎音频输入已结束 - */ - fun feedAudioEnd(): Boolean { - if (!isInitialized) { - Log.e(TAG, "引擎未初始化") - return false - } - - if (!isEngineStarted) { - Log.e(TAG, "引擎未启动") - return false - } - - if (recorderType != SpeechEngineDefines.RECORDER_TYPE_STREAM) { - Log.e(TAG, "当前录音类型不是stream模式,无法使用此方法") - return false - } - - try { - // 空Buffer告知引擎输入结束 - val ret = engine?.feedAudio(engineHandler, ByteArray(0), 0) - - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - Log.e(TAG, "发送音频结束标记失败: $ret") - return false - } - - return true - } catch (e: Exception) { - Log.e(TAG, "发送音频结束标记异常: ${e.message}", e) - return false - } - } - - /** - * 发送Int16数组音频数据 - * 输入格式: PCM 16bit 小端 单声道或双声道 - */ - fun feedAudioInt16(audioData: ShortArray): Boolean { - // 将Int16数组转换为字节数组 - val byteBuffer = ByteBuffer.allocate(audioData.size * 2) - byteBuffer.order(ByteOrder.LITTLE_ENDIAN) - - for (sample in audioData) { - byteBuffer.putShort(sample) - } - - return feedAudio(byteBuffer.array()) - } - - /** - * 判断引擎是否启动 - */ - fun isEngineStarted(): Boolean { - return isEngineStarted - } - - /** - * 判断是否在识别中 - */ - fun isRecognizing(): Boolean { - return isRecognizing - } - - /** - * 释放资源 - */ - fun release() { - if (!isInitialized) { - return - } - - try { - // 停止引擎 - if (isEngineStarted) { - engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - isRecognizing = false - isEngineStarted = false - } - - // 销毁引擎 - engine?.destroyEngine(engineHandler) - engineHandler = -1 - engine = null - isInitialized = false - state = AsrState.IDLE - - Log.i(TAG, "引擎已释放") - } catch (e: Exception) { - Log.e(TAG, "释放引擎异常: ${e.message}", e) - } - } - - /** - * 处理引擎事件 - */ - private fun handleEngineEvent(type: Int, data: String) { - when (type) { - SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> { - // 引擎启动成功 - Log.i(TAG, "引擎启动成功") - mainHandler.post { - continuousCallback?.onSessionStarted() - } - } - - SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> { - // 引擎停止 - Log.i(TAG, "引擎停止") - isRecognizing = false - isEngineStarted = false - state = AsrState.READY - - mainHandler.post { - continuousCallback?.onSessionStopped() - } - } - - SpeechEngineDefines.MESSAGE_TYPE_PARTIAL_RESULT -> { - // 中间识别结果(高频,降级为 debug) - try { - val json = JSONObject(data) - if (!json.has("result")) { - return - } - val text = extractTextFromResult(json) - val detectedLanguage = json.optString("lang", language) - - if (text.isNotEmpty()) { - mainHandler.post { - continuousCallback?.onRecognizing(text, detectedLanguage) - } - } - } catch (e: Exception) { - Log.e(TAG, "解析中间识别结果异常: ${e}") - } - } - - SpeechEngineDefines.MESSAGE_TYPE_FINAL_RESULT -> { - try { - val json = JSONObject(data) - if (!json.has("result")) { - Log.w(TAG, "最终结果没有 result 字段") - return - } - - val text = extractTextFromResult(json) - val detectedLanguage = json.optString("lang", language) - - if (text.isNotEmpty()) { - mainHandler.post { - if (isContinuousMode) { - Log.i(TAG, "最终识别结果(连续): $text") - continuousCallback?.onResult(text, detectedLanguage) - } else { - Log.i(TAG, "最终识别结果(单次): $text") - recognizeCallback?.onResult(text, detectedLanguage) - } - } - } - } catch (e: Exception) { - val errMsg = "解析最终识别结果异常: ${e.message}" - Log.e(TAG, errMsg) - Log.e(TAG, "原始数据: $data") - Log.e(TAG, "详细异常信息", e) - } - } - - SpeechEngineDefines.MESSAGE_TYPE_VOLUME_LEVEL -> { - // 忽略音量回调 - } - - SpeechEngineDefines.MESSAGE_TYPE_ENGINE_ERROR -> { - // 错误信息 - try { - val json = JSONObject(data) - val errMsg = json.optString("err_msg", "未知错误") - val errCode = json.optInt("err_code", -1) - - Log.e(TAG, "引擎错误: code=$errCode, msg=$errMsg") - state = AsrState.ERROR - - mainHandler.post { - if (isContinuousMode) { - continuousCallback?.onError(errMsg) - } else { - recognizeCallback?.onError(errMsg) - } - isRecognizing = false - isEngineStarted = false - } - } catch (e: JSONException) { - Log.e(TAG, "解析错误信息异常: ${e.message}", e) - } - } - } - } - - /** - * 从多种可能的JSON格式中提取文本 - */ - private fun extractTextFromResult(json: JSONObject): String { - try { - if (!json.has("result")) { - return "" - } - - // 获取result字段(可能是JSONObject或JSONArray) - val resultObj = json.get("result") - - // 根据result的实际类型处理 - val text = when (resultObj) { - // 情况1: result是JSONObject,直接获取text字段 - is JSONObject -> { - resultObj.optString("text", "") - } - - // 情况2: result是JSONArray,获取第一个元素的text字段 - is JSONArray -> { - if (resultObj.length() == 0) { - "" - } else { - resultObj.getJSONObject(0).optString("text", "") - } - } - - // 其他情况 - else -> { - resultObj.toString() - } - } - - // 过滤空结果 - if (text.isEmpty()) { - return "" - } - - return text - } catch (e: Exception) { - Log.e(TAG, "提取文本异常: ${e.message}", e) - Log.e(TAG, "详细异常信息", e) - return "" - } - } -} diff --git a/apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt b/apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt deleted file mode 100644 index a53567cd..00000000 --- a/apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt +++ /dev/null @@ -1,434 +0,0 @@ -package com.yunqiinnovation.volcano_speech - -import android.content.Context -import android.os.Handler -import android.os.Looper -import androidx.annotation.NonNull -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.EventChannel -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 { - private val TAG = "VolcanoSpeechPlugin" - private lateinit var context: Context - private val mainHandler = Handler(Looper.getMainLooper()) - - // ASR相关 - private lateinit var asrChannel: MethodChannel - private lateinit var asrEventChannel: EventChannel - private var asrEventSink: EventChannel.EventSink? = null - - // TTS相关 - private lateinit var ttsChannel: MethodChannel - private lateinit var ttsEventChannel: EventChannel - private var ttsEventSink: EventChannel.EventSink? = null - - // TTS音频数据相关 - private lateinit var ttsAudioDataChannel: EventChannel - private var ttsAudioDataSink: EventChannel.EventSink? = null - - // TTS帮助类 - private lateinit var volcanoTtsHelper: VolcanoTtsHelper - - // ASR帮助类 - private lateinit var volcanoAsrHelper: VolcanoAsrHelper - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - context = flutterPluginBinding.applicationContext - - // 初始化ASR通道 - asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "volcano_speech/asr") - asrChannel.setMethodCallHandler(AsrMethodHandler()) - - // 初始化TTS通道 - ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "volcano_speech/tts") - ttsChannel.setMethodCallHandler(TtsMethodHandler()) - - // 初始化ASR事件通道 - asrEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "volcano_speech/asr_events") - asrEventChannel.setStreamHandler(object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - asrEventSink = events - } - - override fun onCancel(arguments: Any?) { - asrEventSink = null - } - }) - - // 初始化TTS事件通道 - ttsEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "volcano_speech/tts_events") - ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - ttsEventSink = events - } - - override fun onCancel(arguments: Any?) { - ttsEventSink = null - } - }) - - // 初始化TTS音频数据通道 - ttsAudioDataChannel = EventChannel(flutterPluginBinding.binaryMessenger, "volcano_speech/tts_audio_data") - ttsAudioDataChannel.setStreamHandler(object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - ttsAudioDataSink = events - } - - override fun onCancel(arguments: Any?) { - ttsAudioDataSink = null - } - }) - - // 初始化TTS帮助类 - volcanoTtsHelper = VolcanoTtsHelper(context) - // 添加TTS事件监听 - volcanoTtsHelper.addListener(object : TtsEventListener { - override fun onEvent(event: TtsEvent) { - when (event.type) { - TtsEventType.SYNTHESIS_STARTED -> { - sendTtsEvent(mapOf("type" to "started")) - } - TtsEventType.SYNTHESIS_COMPLETED -> { - sendTtsEvent(mapOf("type" to "completed")) - } - TtsEventType.SYNTHESIS_CANCELED -> { - sendTtsEvent(mapOf("type" to "canceled")) - } - TtsEventType.ERROR -> { - val errorCode = event.params["errorCode"] as? String ?: "-1" - val errorMessage = event.params["errorMessage"] as? String ?: "未知错误" - sendTtsEvent(mapOf( - "type" to "error", - "errorCode" to errorCode, - "errorMessage" to errorMessage - )) - } - } - } - }) - - // 添加TTS音频数据监听 - volcanoTtsHelper.addAudioDataListener(object : AudioDataListener { - override fun onAudioData(data: ByteArray) { - sendTtsAudioData(data) - } - }) - - // 初始化ASR帮助类 - volcanoAsrHelper = VolcanoAsrHelper(context) - } - - // 发送ASR事件 - private fun sendAsrEvent(event: Map) { - if (asrEventSink == null) { - Log.w(TAG, "无法发送ASR事件:事件通道未准备好") - return - } - - mainHandler.post { - try { - asrEventSink?.success(event) - } catch (e: Exception) { - Log.e(TAG, "发送ASR事件失败: ${e.message}") - } - } - } - - // 发送TTS事件 - private fun sendTtsEvent(event: Map) { - Log.d(TAG, "发送TTS事件: $event") - if (ttsEventSink == null) { - Log.w(TAG, "无法发送TTS事件:事件通道未准备好") - return - } - - mainHandler.post { - try { - ttsEventSink?.success(event) - Log.d(TAG, "TTS事件发送成功") - } catch (e: Exception) { - Log.e(TAG, "发送TTS事件失败: ${e.message}") - } - } - } - - // 发送TTS音频数据 - private fun sendTtsAudioData(data: ByteArray) { - if (ttsAudioDataSink == null) { - return - } - - mainHandler.post { - try { - ttsAudioDataSink?.success(data) - } catch (e: Exception) { - Log.e(TAG, "发送TTS音频数据失败: ${e.message}") - } - } - } - - // ASR方法处理器 - inner class AsrMethodHandler : MethodCallHandler { - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - when (call.method) { - "getPlatformVersion" -> { - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } - "initialize" -> { - val appId = call.argument("appId") ?: "" - val apiKey = call.argument("appToken") ?: "" - val resourceId = call.argument("resourceId") ?: "" - - if (appId.isEmpty() || apiKey.isEmpty() || resourceId.isEmpty()) { - result.error("INVALID_ARGUMENTS", "appId、apiKey和resourceId不能为空", null) - return - } - - val success = volcanoAsrHelper.initialize(appId, apiKey, resourceId) - result.success(success) - } - "setLanguage" -> { - val language = call.argument("language") ?: "zh-CN" - result.success(volcanoAsrHelper.setLanguage(language)) - } - "setHotWords" -> { - val hotWordsId = call.argument("hotWordsId") ?: "" - result.success(volcanoAsrHelper.setHotWords(hotWordsId)) - } - "recognizeOnce" -> { - // 旧方式,不再推荐使用 - // 确保当前不在连续识别中 - if (volcanoAsrHelper.isRecognizing()) { - result.error("ASR_BUSY", "当前正在识别中", null) - return - } - - volcanoAsrHelper.recognizeOnce(object : VolcanoAsrHelper.RecognizeCallback { - override fun onResult(text: String, detectedLanguage: String) { - mainHandler.post { - result.success(mapOf( - "text" to text, - "language" to detectedLanguage - )) - } - } - - override fun onError(error: String) { - mainHandler.post { - result.error("ASR_ERROR", error, null) - } - } - }) - } - "startOnceRecognition" -> { - // 新方式,自动返回结果 - // 确保当前不在连续识别中 - if (volcanoAsrHelper.isRecognizing()) { - result.error("ASR_BUSY", "当前正在识别中", null) - return - } - - // 设置一次性识别,触发事件而不是直接返回结果 - volcanoAsrHelper.recognizeOnce(object : VolcanoAsrHelper.RecognizeCallback { - override fun onResult(text: String, detectedLanguage: String) { - mainHandler.post { - // 发送最终结果事件 - sendAsrEvent(mapOf( - "type" to "result", - "text" to text, - "language" to detectedLanguage - )) - } - } - - override fun onError(error: String) { - mainHandler.post { - // 发送错误事件 - sendAsrEvent(mapOf( - "type" to "error", - "message" to error - )) - } - } - }) - - // 返回成功启动 - result.success(true) - } - "startContinuousRecognition" -> { - // 确保事件通道已准备好 - if (asrEventSink == null) { - result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null) - return - } - - val success = volcanoAsrHelper.startContinuousRecognition(object : VolcanoAsrHelper.ContinuousRecognizeCallback { - override fun onResult(text: String, detectedLanguage: String) { - sendAsrEvent(mapOf( - "type" to "result", - "text" to text, - "language" to detectedLanguage - )) - } - - override fun onRecognizing(text: String, detectedLanguage: String) { - sendAsrEvent(mapOf( - "type" to "recognizing", - "text" to text, - "language" to detectedLanguage - )) - } - - override fun onSessionStarted() { - sendAsrEvent(mapOf("type" to "sessionStarted")) - } - - override fun onSessionStopped() { - sendAsrEvent(mapOf("type" to "sessionStopped")) - } - - - override fun onError(error: String) { - sendAsrEvent(mapOf( - "type" to "error", - "message" to error - )) - } - }) - - result.success(success) - } - "stopContinuousRecognition" -> { - val success = volcanoAsrHelper.stopContinuousRecognition() - result.success(success) - } - "stopRecognize" -> { - val success = volcanoAsrHelper.stopRecognize() - result.success(success) - } - "isContinuousRecognitionActive" -> { - result.success(volcanoAsrHelper.isRecognizing()) - } - "feedAudioData" -> { - val data = call.argument("data") ?: ByteArray(0) - result.success(volcanoAsrHelper.feedAudio(data)) - } - "feedAudioEnd" -> { - result.success(volcanoAsrHelper.feedAudioEnd()) - } - "release" -> { - volcanoAsrHelper.release() - result.success(true) - } - else -> { - result.notImplemented() - } - } - } - } - - // TTS方法处理器 - inner class TtsMethodHandler : MethodCallHandler { - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - when (call.method) { - "getPlatformVersion" -> { - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } - "initialize" -> { - val appId = call.argument("appId") ?: "" - val token = call.argument("appToken") ?: "" - - if (appId.isEmpty() || token.isEmpty()) { - result.error("INVALID_ARGUMENTS", "appId和token不能为空", null) - return - } - - val success = volcanoTtsHelper.initialize(appId, token) - result.success(success) - } - "setVoice" -> { - val voice = call.argument("voice") ?: "" - - if (voice.isEmpty()) { - result.error("INVALID_ARGUMENTS", "voice不能为空", null) - return - } - - result.success(volcanoTtsHelper.setVoice(voice)) - } - "speakOnce" -> { - val text = call.argument("text") ?: "" - - if (text.isEmpty()) { - result.error("INVALID_ARGUMENTS", "合成文本不能为空", null) - return - } - - val success = volcanoTtsHelper.speakOnce(text) - result.success(success) - } - "speakStream" -> { - val text = call.argument("text") ?: "" - - if (text.isEmpty()) { - result.error("INVALID_ARGUMENTS", "合成文本不能为空", null) - return - } - - val success = volcanoTtsHelper.speakStream(text) - result.success(success) - } - "flushStream" -> { - val success = volcanoTtsHelper.flushStream() - result.success(success) - } - "stop" -> { - result.success(volcanoTtsHelper.stop()) - } - "isSpeaking" -> { - // 由于ITtsService不再定义isSpeaking方法,我们可以通过检查是否有正在合成的文本来判断 - result.success(false) // 简化处理,直接返回false - } - "getCurrentVoice" -> { - result.success(volcanoTtsHelper.getCurrentVoice()) - } - "enableAudioCallback" -> { - val enable = call.argument("enable") ?: false - volcanoTtsHelper.setEnableAudioCallback(enable) - result.success(true) - } - "release" -> { - volcanoTtsHelper.release() - result.success(true) - } - else -> { - result.notImplemented() - } - } - } - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - asrChannel.setMethodCallHandler(null) - ttsChannel.setMethodCallHandler(null) - asrEventChannel.setStreamHandler(null) - ttsEventChannel.setStreamHandler(null) - ttsAudioDataChannel.setStreamHandler(null) - - volcanoTtsHelper.release() - volcanoAsrHelper.release() - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt b/apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt deleted file mode 100644 index 1802d5d2..00000000 --- a/apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt +++ /dev/null @@ -1,691 +0,0 @@ -package com.yunqiinnovation.volcano_speech - -import android.content.Context -import android.os.Handler -import android.os.Looper -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 - -/** - * 火山语音合成服务 - 实现ITtsService接口 - */ -class VolcanoTtsHelper(private val context: Context) : ITtsService { - private val TAG = "VolcanoTtsHelper" - private val mainHandler = Handler(Looper.getMainLooper()) - - // 语音引擎相关 - private var engine: SpeechEngine? = null - private var engineHandler: Long = -1 - private var isInitialized = false - - // 语音合成状态 - private var isEngineStarted = false - private var currentReqId = "" - - // 配置参数 - private var voice = "zh_female_linjianvhai_moon_bigtts" - - // 文本队列管理 - private val pendingTexts = ConcurrentLinkedQueue() - private var isProcessingQueue = false - private val MAX_PENDING_SENTENCES = 20 // 最大等待句子数,避免内存占用过大 - - // 合成相关 - private var currentText = "" - private var synthesisFromPlayer = false - private var isSynthesisInProgress = false - - // 错误处理 - private val MAX_RETRY_COUNT = 3 - private var retryCount = MAX_RETRY_COUNT - - // 音频数据相关 - private var enableAudioCallback = false - private val audioDataListeners = mutableListOf() - - // 事件监听 - private val eventListeners = mutableListOf() - - // 流式文本处理 - private val streamBuffer = StringBuffer() - private var lastSpeakTime = 0L - - // 跟踪当前合成的播放数 - private var pendingPlayCount = 0 - - /** - * 当前选择的语音 - */ - fun getCurrentVoice(): String = voice - - /** - * 设置是否启用音频数据回调 - */ - fun setEnableAudioCallback(enable: Boolean) { - this.enableAudioCallback = enable - } - - /** - * 添加事件监听器 - */ - override fun addListener(listener: TtsEventListener) { - if (!eventListeners.contains(listener)) { - eventListeners.add(listener) - } - } - - /** - * 移除事件监听器 - */ - override fun removeListener(listener: TtsEventListener) { - eventListeners.remove(listener) - } - - /** - * 添加音频数据监听器 - */ - override fun addAudioDataListener(listener: AudioDataListener) { - if (!audioDataListeners.contains(listener)) { - audioDataListeners.add(listener) - // 如果添加了音频监听器,自动开启音频回调 - enableAudioCallback = true - } - } - - /** - * 移除音频数据监听器 - */ - override fun removeAudioDataListener(listener: AudioDataListener) { - audioDataListeners.remove(listener) - // 如果没有监听器了,可以关闭音频回调 - if (audioDataListeners.isEmpty()) { - enableAudioCallback = false - } - } - - /** - * 分发事件到所有监听器 - */ - private fun dispatchEvent(eventType: TtsEventType, params: Map = emptyMap()) { - val event = TtsEvent(eventType, params) - mainHandler.post { - eventListeners.forEach { it.onEvent(event) } - } - } - - /** - * 分发音频数据到所有监听器 - */ - private fun dispatchAudioData(data: ByteArray) { - if (audioDataListeners.isEmpty()) return - - mainHandler.post { - audioDataListeners.forEach { it.onAudioData(data) } - } - } - - /** - * 初始化语音合成引擎 - */ - override fun initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String, language: String): Boolean { - if (isInitialized) { - return true - } - - try { - // 准备环境 - try { - SpeechEngineGenerator.PrepareEnvironment(context, null) - } catch (e: Exception) { - Log.w(TAG, "准备环境时发生异常: ${e.message}") - } - - // 创建引擎 - engine = SpeechEngineGenerator.getInstance() - engineHandler = engine?.createEngine() ?: -1 - - if (engineHandler == -1L) { - handleError(-1, "创建引擎失败") - return false - } - - // 设置基本参数 - engine?.setContext(context) - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ENGINE_NAME_STRING, SpeechEngineDefines.TTS_ENGINE) - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING, SpeechEngineDefines.LOG_LEVEL_DEBUG) - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_UID_STRING, "user_id") - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_DEVICE_ID_STRING, "device_id") - - // 设置授权信息 - 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) - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_WORK_MODE_INT, SpeechEngineDefines.TTS_WORK_MODE_ONLINE) - - // 设置服务地址和发音人 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_ADDRESS_STRING, "wss://openspeech.bytedance.com") - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_URI_STRING, "/api/v1/tts/ws_binary") - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_CLUSTER_STRING, "volcano_tts") - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_TYPE_ONLINE_STRING, voice) - - // 设置播放进度回调和播放器 - 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 - - if (isInitialized) { - // 设置回调监听 - engine?.setListener(object : SpeechEngine.SpeechListener { - override fun onSpeechMessage(type: Int, data: ByteArray, len: Int) { - handleEngineEvent(type, String(data), data) - } - }) - - return true - } else { - handleError(result ?: -1, "引擎初始化失败") - return false - } - } catch (e: Exception) { - handleError(-1, "初始化异常: ${e.message}") - return false - } - } - - /** - * 统一处理错误 - */ - private fun handleError(code: Int, message: String, e: Exception? = null) { - if (e != null) { - Log.e(TAG, message, e) - } else { - Log.e(TAG, message) - } - dispatchEvent(TtsEventType.ERROR, mapOf( - "errorCode" to code.toString(), - "errorMessage" to message - )) - } - - /** - * 设置语音 - */ - override fun setVoice(voiceName: String): Boolean { - if (!isInitialized) { - Log.e(TAG, "引擎未初始化") - return false - } - - try { - this.voice = voiceName - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_TYPE_ONLINE_STRING, voiceName) - return true - } catch (e: Exception) { - handleError(-1, "设置发音人失败: ${e.message}", e) - return false - } - } - - - - - /** - * 单次播放文本(未实现,推荐使用speakStream) - */ - override fun speakOnce(text: String): Boolean { - return false - } - - /** - * 将文本加入播放队列,支持连续播放多段文本 - */ - override fun speakStream(text: String): Boolean { - if (text.isEmpty()) { - return true - } - - try { - if (!isInitialized) { - Log.e(TAG, "引擎未初始化") - return false - } - - // 处理文本,移除特殊字符 - val processedText = text.replace(Regex("[#*]"), " ") - - // 添加新文本到缓冲区 - 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('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':') - - // 查找最后一个标点符号的位置 - var lastPunctuationIndex = -1 - for (i in currentText.indices.reversed()) { - if (currentText[i] in punctuationMarks) { - lastPunctuationIndex = i - break - } - } - - // 如果找到标点符号,则播放到该标点符号 - if (lastPunctuationIndex >= 0) { - // 提取要播放的文本(包含标点符号) - val textToSpeak = currentText.substring(0, lastPunctuationIndex + 1) - - // 剩余的文本保存在缓冲区中 - streamBuffer.delete(0, lastPunctuationIndex + 1) - - // 只有实际有文本时才处理 - if (textToSpeak.isNotEmpty()) { - // 将文本添加到队列 - Log.d(TAG, "textToSpeak: ${textToSpeak}") - pendingTexts.add(textToSpeak) - - // 根据引擎状态决定下一步操作 - if (!isEngineStarted) { - // 引擎未启动,启动它 - startEngine() - } else if (!isSynthesisInProgress && !synthesisFromPlayer) { - // 引擎已启动但当前空闲,触发下一个合成 - triggerNextSynthesis() - } - // 其他情况下引擎正忙,会在当前任务完成后处理队列 - } - } - // 如果没找到标点符号,保留全部文本在缓冲区,等待更多文本 - - return true - } catch (e: Exception) { - handleError(-1, "流式语音合成失败: ${e.message}", e) - return false - } - } - - /** - * 启动语音引擎 - */ - private fun startEngine(): Boolean { - if (!isInitialized) { - return false - } - - try { - // 设置连续播放场景模式 - 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, "启动引擎失败") - return false - } - - isEngineStarted = true - retryCount = MAX_RETRY_COUNT - - return true - } catch (e: Exception) { - handleError(-1, "启动引擎异常: ${e.message}", e) - return false - } - } - - /** - * 触发下一句合成 - */ - private fun triggerNextSynthesis(): Boolean { - if (!isEngineStarted || pendingTexts.isEmpty() || isSynthesisInProgress) { - return false - } - - val nextText = pendingTexts.poll() - currentText = nextText - - try { - Log.d(TAG, "开始合成: ${nextText}") - // 设置要合成的文本 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, nextText) - - // 标记合成开始 - isSynthesisInProgress = true - pendingPlayCount++ - - // 发送合成指令,不再使用请求ID - 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 - pendingPlayCount-- // 恢复计数 - return true - } else { - isSynthesisInProgress = false - pendingPlayCount-- // 恢复计数 - handleError(ret ?: -1, "合成失败") - return false - } - } - - return true - } catch (e: Exception) { - isSynthesisInProgress = false - pendingPlayCount-- // 恢复计数 - handleError(-1, "触发合成异常: ${e.message}", e) - return false - } - } - - /** - * 播放队列中剩余文本并标记流式输入结束 - */ - override fun flushStream(): Boolean { - if (!isInitialized) { - return false - } - - try { - // 获取缓冲区中剩余的文本 - val remainingText = streamBuffer.toString() - - // 清空缓冲区 - streamBuffer.delete(0, streamBuffer.length) - - // 如果缓冲区有未播放的文本,直接加入队列 - if (remainingText.isNotEmpty()) { - pendingTexts.add(remainingText) - } - - // 如果队列为空且引擎未启动,直接返回 - if (pendingTexts.isEmpty() && !isEngineStarted) { - return true - } - - // 如果引擎未启动但队列有内容,则启动引擎 - if (!isEngineStarted && !pendingTexts.isEmpty()) { - return startEngine() - } - - // 如果引擎已启动但没有正在进行的合成,触发下一个合成 - if (isEngineStarted && !isSynthesisInProgress && !synthesisFromPlayer && !pendingTexts.isEmpty()) { - return triggerNextSynthesis() - } - - return true - } catch (e: Exception) { - handleError(-1, "刷新流式文本失败: ${e.message}", e) - return false - } - } - - /** - * 停止播放 - */ - override fun stop(): Boolean { - if (!isInitialized) { - return false - } - - // 已经不在播放,无需操作 - if (!isEngineStarted) { - return true - } - - try { - val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - isEngineStarted = false - isSynthesisInProgress = false - - // 清空队列和状态 - pendingTexts.clear() - isProcessingQueue = false - synthesisFromPlayer = false - pendingPlayCount = 0 - - // 清空流缓冲区 - streamBuffer.delete(0, streamBuffer.length) - - // 发送取消事件 - dispatchEvent(TtsEventType.SYNTHESIS_CANCELED) - - return ret == SpeechEngineDefines.ERR_NO_ERROR - } catch (e: Exception) { - Log.e(TAG, "停止播放异常", e) - return false - } - } - - /** - * 释放资源 - */ - override fun release() { - if (!isInitialized) { - return - } - - try { - // 停止引擎 - stop() - - // 销毁引擎 - engine?.destroyEngine(engineHandler) - engineHandler = -1 - engine = null - isInitialized = false - pendingTexts.clear() - streamBuffer.delete(0, streamBuffer.length) - eventListeners.clear() - audioDataListeners.clear() - } catch (e: Exception) { - Log.e(TAG, "释放引擎异常", e) - } - } - - /** - * 处理引擎事件 - */ - private fun handleEngineEvent(type: Int, stringData: String, rawData: ByteArray? = null) { - when (type) { - SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> { - // 引擎启动成功 - currentReqId = stringData - isEngineStarted = true - - // 如果队列不为空,在引擎启动后立即触发第一句合成 - if (!pendingTexts.isEmpty() && !isSynthesisInProgress) { - triggerNextSynthesis() - } - } - - SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> { - // 引擎停止 - isEngineStarted = false - isSynthesisInProgress = false - pendingPlayCount = 0 - } - - SpeechEngineDefines.MESSAGE_TYPE_TTS_SYNTHESIS_BEGIN -> { - // 合成开始 - isSynthesisInProgress = true - } - - SpeechEngineDefines.MESSAGE_TYPE_TTS_SYNTHESIS_END -> { - // 合成结束 - isSynthesisInProgress = false - - // 重置重试计数 - retryCount = MAX_RETRY_COUNT - - // 如果不是从播放器触发的合成,尝试处理下一个文本 - if (!synthesisFromPlayer && !pendingTexts.isEmpty()) { - // 在合成结束回调中触发下一句合成 - triggerNextSynthesis() - } - } - - SpeechEngineDefines.MESSAGE_TYPE_TTS_START_PLAYING -> { - // 开始播放 - // Log.d(TAG, "开始播放MESSAGE_TYPE_TTS_START") - dispatchEvent(TtsEventType.SYNTHESIS_STARTED) - } - - SpeechEngineDefines.MESSAGE_TYPE_TTS_FINISH_PLAYING -> { - // 播放结束 - // 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() && pendingPlayCount == 0) { - // 如果队列为空,发送播放完成事件 - // Log.d(TAG, "所有文本播放完成") - dispatchEvent(TtsEventType.SYNTHESIS_COMPLETED) - isProcessingQueue = false - - // 如果没有待播放内容,关闭引擎 - engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - isEngineStarted = false - } else if (!isSynthesisInProgress) { - // 如果队列不为空且当前没有正在进行的合成,触发下一句合成 - triggerNextSynthesis() - } - } - - SpeechEngineDefines.MESSAGE_TYPE_TTS_AUDIO_DATA -> { - // 音频数据回调 - if (rawData != null && rawData.isNotEmpty()) { - dispatchAudioData(rawData) - } - } - - SpeechEngineDefines.MESSAGE_TYPE_ENGINE_ERROR -> { - // 错误信息 - try { - val json = JSONObject(stringData) - val errCode = json.optInt("err_code", -1) - val errMsg = json.optString("err_msg", "未知错误") - - Log.e(TAG, "引擎错误: code=$errCode, msg=$errMsg, ${SpeechEngineDefines.CODE_TTS_INVALID_TEXT}") - - // 标记当前合成结束 - isSynthesisInProgress = false - - // 通知错误 - // dispatchEvent(TtsEventType.ERROR, mapOf( - // "errorCode" to errCode.toString(), - // "errorMessage" to errMsg - // )) - - // 检查是否需要更新计数 - if (pendingPlayCount > 0) { - pendingPlayCount-- - } - - // 尝试恢复或停止 - when (errCode) { - // 轻微合成错误,继续下一个文本 - SpeechEngineDefines.CODE_TTS_LIMIT_QPS, - SpeechEngineDefines.CODE_TTS_LIMIT_COUNT, - SpeechEngineDefines.CODE_TTS_SERVER_BUSY, - SpeechEngineDefines.CODE_TTS_LONG_TEXT, - SpeechEngineDefines.CODE_TTS_INVALID_TEXT, - SpeechEngineDefines.CODE_TTS_SYNTHESIS_TIMEOUT, - SpeechEngineDefines.CODE_TTS_SYNTHESIS_ERROR, - SpeechEngineDefines.CODE_TTS_SYNTHESIS_WAITING_TIMEOUT, - SpeechEngineDefines.CODE_TTS_ERROR_UNKNOWN -> { - if (!pendingTexts.isEmpty()) { - // 轻微错误,直接尝试下一句 - // Log.d(TAG, "轻微错误,直接尝试下一句, ${SpeechEngineDefines.CODE_TTS_INVALID_TEXT}") - triggerNextSynthesis() - } else { - - // stop() - } - } - - // 网络错误,尝试重试 - SpeechEngineDefines.CODE_CONNECT_TIMEOUT, - SpeechEngineDefines.CODE_RECEIVE_TIMEOUT, - SpeechEngineDefines.CODE_NET_LIB_ERROR -> { - retryCount-- - if (retryCount > 0) { - // 1秒后重试当前文本 - mainHandler.postDelayed({ - if (isEngineStarted) { - // 设置当前文本并触发合成 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, currentText) - isSynthesisInProgress = true - engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNTHESIS, "") - } - }, 1000) - } else { - stop() - } - } - - // 其他错误,停止引擎 - else -> { - Log.d(TAG, "其他错误,停止合成") - // stop() - } - } - } catch (e: Exception) { - isSynthesisInProgress = false - handleError(-1, "解析错误信息异常: ${e.message}", e) - stop() - } - } - } - } -} diff --git a/apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/utils/FileLogger.kt b/apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/utils/FileLogger.kt deleted file mode 100644 index 8fdfb122..00000000 --- a/apps/client/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/utils/FileLogger.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.yunqiinnovation.volcano_speech.utils - -import android.util.Log - -/** - * 文件日志记录工具 - */ -object FileLogger { - private const val TAG = "VolcanoSpeech" - private var isDebugEnabled = true - - /** - * 设置是否启用调试日志 - */ - fun setDebugEnabled(enabled: Boolean) { - isDebugEnabled = enabled - } - - /** - * 记录调试日志 - */ - fun d(tag: String, message: String) { - if (isDebugEnabled) { - Log.d("$TAG-$tag", message) - } - } - - /** - * 记录信息日志 - */ - fun i(tag: String, message: String) { - Log.i("$TAG-$tag", message) - } - - /** - * 记录警告日志 - */ - fun w(tag: String, message: String) { - Log.w("$TAG-$tag", message) - } - - /** - * 记录错误日志 - */ - fun e(tag: String, message: String) { - Log.e("$TAG-$tag", message) - } - - /** - * 记录错误日志,带异常 - */ - fun e(tag: String, message: String, throwable: Throwable) { - Log.e("$TAG-$tag", message, throwable) - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/volcano_speech/ios/Classes/.gitkeep b/apps/client/local_plugins/volcano_speech/ios/Classes/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/client/local_plugins/volcano_speech/ios/volcano_speech.podspec b/apps/client/local_plugins/volcano_speech/ios/volcano_speech.podspec deleted file mode 100644 index 42b24bd7..00000000 --- a/apps/client/local_plugins/volcano_speech/ios/volcano_speech.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint volcano_speech.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'volcano_speech' - s.version = '0.0.1' - s.summary = 'Volcano Speech plugin' - s.description = <<-DESC -A Flutter plugin for Volcano Speech services. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.platform = :ios, '16.0' - - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.swift_version = '5.0' -end \ No newline at end of file diff --git a/apps/client/local_plugins/volcano_speech/pubspec.yaml b/apps/client/local_plugins/volcano_speech/pubspec.yaml deleted file mode 100644 index cd90d8b1..00000000 --- a/apps/client/local_plugins/volcano_speech/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: volcano_speech -description: 火山引擎语音合成服务插件 -version: 0.0.1 -homepage: - -environment: - sdk: ">=2.17.0 <3.0.0" - flutter: ">=2.5.0" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^2.0.0 - -# The following section is specific to Flutter packages. -flutter: - # This section identifies this Flutter project as a plugin project. - plugin: - platforms: - android: - package: com.yunqiinnovation.volcano_speech - pluginClass: VolcanoSpeechPlugin - ios: - pluginClass: VolcanoSpeechPlugin \ No newline at end of file diff --git a/apps/client/test.json b/apps/client/test.json deleted file mode 100644 index 50055737..00000000 --- a/apps/client/test.json +++ /dev/null @@ -1,35 +0,0 @@ -intent={cmp=com.yunqiinnovation.deepsound/com.yunqiinnovation.ble_service.BleForegroundService} -com.yunqiinnovation.deepsound/com.yunqiinnovation.ble_service.BleWakeReceiver filter f42f2c2 - -# 1. 启动前台 Service (API 26+ 推荐用 start-foreground-service) -adb shell am start-foreground-service \ - -n com.yunqiinnovation.deepsound/com.yunqiinnovation.ble_service.BleForegroundService - -# 2. 发送广播到 BleWakeReceiver -adb shell am broadcast \ - -a com.yunqiinnovation.ble_service.ACTION_BLE_SCAN \ - -n com.yunqiinnovation.deepsound/com.yunqiinnovation.ble_service.BleWakeReceiver - - - adb shell dumpsys package com.yunqiinnovation.deepsound | grep BleWakeReceiver - - adb shell dumpsys package com.yunqiinnovation.deepsound | grep receiver - - adb logcat -v time | grep -Ei "BleWakeReceiver|BleMediaService" - - - - - mvn install:install-file \ - -Dfile= - -DgroupId=com.jieli.audio \ - -DartifactId=jl_audio_decode \ - -Dversion=2.0.0 \ - -Dpackaging=aar - - mvn install:install-file \ - -Dfile=/Users/wolfplus/Developer/Doc/Jieli_OTA_SDK_Android_1.9.2/libs/jl_bt_ota_V1.9.2-release.aar \ - -DgroupId=com.jieli.jl_bt_ota \ - -DartifactId=jl_bt_ota \ - -Dversion=1.9.2 \ - -Dpackaging=aar \ No newline at end of file diff --git a/apps/client/test2.json b/apps/client/test2.json deleted file mode 100644 index 5c6e10e7..00000000 --- a/apps/client/test2.json +++ /dev/null @@ -1,101 +0,0 @@ -curl -v -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer 168deb3d-fd0c-4912-b9f1-aaee5c6743e6' -H 'Accept: text/event-stream' -d '{ - "model": "doubao-1-5-pro-32k-250115", - "messages": [ - [], - { - "role": "user", - "content": "你好,你能听到我在说话吗?" - } - ], - "temperature": 0.7, - "max_tokens": 2000, - "stream": true, - "tools": [ - { - "type": "function", - "function": { - "name": "exit_interaction", - "description": "退出当前语音交互", - "parameters": { - "type": "object", - "properties": {}, - "required": [] - } - } - }, - { - "type": "function", - "function": { - "name": "enter_translation_mode", - "description": "用户请求进入实时翻译模式时,启动实时翻译功能", - "parameters": { - "type": "object", - "properties": {}, - "required": [] - } - } - }, - { - "type": "function", - "function": { - "name": "send_text_message", - "description": "发送短信", - "parameters": { - "type": "object", - "properties": { - "contact": { - "type": "string", - "description": "联系人姓名或电话号码" - }, - "message": { - "type": "string", - "description": "短信内容" - } - }, - "required": [ - "contact", - "message" - ] - } - } - }, - { - "type": "function", - "function": { - "name": "make_phone_call", - "description": "拨打电话", - "parameters": { - "type": "object", - "properties": { - "contact": { - "type": "string", - "description": "联系人姓名或电话号码" - } - }, - "required": [ - "contact" - ] - } - } - }, - { - "type": "function", - "function": { - "name": "open_app", - "description": "当用户请求打开特定应用时,启动相应应用", - "parameters": { - "type": "object", - "properties": { - "app_name": { - "type": "string", - "description": "应用名称" - } - }, - "required": [ - "app_name" - ] - } - } - } - ] -}' 'https: //ark.cn-beijing.volces.com/api/v3/chat/completions' \ No newline at end of file diff --git a/apps/client/test_mobile_elf.py b/apps/client/test_mobile_elf.py deleted file mode 100644 index 77d4449c..00000000 --- a/apps/client/test_mobile_elf.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -import ssl, time, hashlib, json, websocket - -DEVICE_NO = '199839C0AAAE99653D2E' -DEVICE_ID = '2032013791868043266' -PRODUCT_ID = '2031271967792472065' -PRODUCT_KEY = 'hotepZZwvouWQjHz' -DEVICE_SECRET = 'ijdthZGQELhkmigf' - -ts = str(int(time.time() * 1000)) -sign = hashlib.md5((DEVICE_SECRET + ts).encode()).hexdigest() -url = (f'wss://ivs.chinamobiledevice.com:11443/app-ws/v2/asr' - f'?deviceNo={DEVICE_NO}&deviceId={DEVICE_ID}' - f'&productId={PRODUCT_ID}&productKey={PRODUCT_KEY}' - f'&sign={sign}&sn=py-test&ts={ts}') - -print(f'connecting...\n{url}') -ws = websocket.WebSocket(sslopt={'cert_reqs': ssl.CERT_NONE}) -ws.settimeout(15) -ws.connect(url) -print('connected! sending start frame immediately...') - -import uuid -start_frame = json.dumps({ - 'type': 'start', - 'data': { - 'cuid': DEVICE_ID, - 'format': 'pcm', - 'sample': 16000, - 'support_dcs': 2, - 'chunk_size': 10240, - 'support_tts': True, - 'support_text2dcs': True, - 'user_agent': 'voitrans/1.0', - 'dialog_request_id': str(uuid.uuid4()), - 'access_rc': True, - 'support_part_tts': True, - 'need_stoplisten': True, - 'need_dialogue_finish': True, - 'result_trans2directive': False, - 'rc_version': '5', - 'messages': [], - 'client_context': [], - } -}) -ws.send(start_frame) -print('start frame sent, waiting for ready...') - -# 等待 ready -while True: - msg = ws.recv() - print('received:', msg) - data = json.loads(msg) - if data.get('type') == 'ready': - print('--- ready received, sending text "你好" ---') - break - -# 发送文本 -text_frame = json.dumps({'type': 'text', 'data': {'text': '你好'}}) -ws.send(text_frame) -print('text frame sent') - -# 发送 finish -finish_frame = json.dumps({'type': 'finish'}) -ws.send(finish_frame) -print('finish frame sent, waiting for AI response...') - -# 接收所有回包直到 end=1 -while True: - msg = ws.recv() - print('received:', msg) - try: - data = json.loads(msg) - if data.get('end') == 1: - print('--- session end (end=1), closing ---') - break - except Exception: - pass - -ws.close() -print('done.') diff --git a/deploy/admin/prod-deploy.sh b/deploy/admin/prod-deploy.sh index 1094056a..9020afe0 100755 --- a/deploy/admin/prod-deploy.sh +++ b/deploy/admin/prod-deploy.sh @@ -60,7 +60,7 @@ env_profile() { ENV_DESC="阿龙生产环境 · 阿里云" registry_profile along || return 1 # 镜像仓库:registry.ymaikj.com(实体在阿龙测试机 8.133.166.29,账号 admin) DEPLOY_HOST="47.116.104.181" # ← 阿龙生产服务器 IP - DEPLOY_KEY="/Users/liwei1dao/liwei/密钥/along/releae-guangzhou.pem" # 与阿龙测试机同一把钥匙(2026-08-18 由 ~/.ssh/along.pem 迁到此处;文件名 releae 是原文件笔误) + DEPLOY_KEY="${DEPLOY_KEY:-/Users/liwei1dao/liwei/密钥/along/releae-guangzhou.pem}" # 与阿龙测试机同一把钥匙(2026-08-18 由 ~/.ssh/along.pem 迁到此处;文件名 releae 是原文件笔误) DEPLOY_DIR="/home/work/starpivot/admin" ;; *) red "✗ 未知环境: ${DEPLOY_ENV}(应为 ${PROD_ENVS[*]// /|})"; exit 1 ;; diff --git a/deploy/app/prod-deploy.sh b/deploy/app/prod-deploy.sh index 174b455c..e4adfb89 100755 --- a/deploy/app/prod-deploy.sh +++ b/deploy/app/prod-deploy.sh @@ -106,7 +106,7 @@ region_profile() { # 远端部署目录/容器名 DEPLOY_HOST="47.116.104.181" # ← 阿龙生产服务器 IP DEPLOY_PORT=22 DEPLOY_USER=root - DEPLOY_KEY="/Users/liwei1dao/liwei/密钥/along/releae-guangzhou.pem" # 与阿龙测试机同一把钥匙(2026-08-18 由 ~/.ssh/along.pem 迁到此处;文件名 releae 是原文件笔误) + DEPLOY_KEY="${DEPLOY_KEY:-/Users/liwei1dao/liwei/密钥/along/releae-guangzhou.pem}" # 与阿龙测试机同一把钥匙(2026-08-18 由 ~/.ssh/along.pem 迁到此处;文件名 releae 是原文件笔误) REGION_COMPANY="along" # 镜像从 registry.ymaikj.com 拉(实体在阿龙测试机 8.133.166.29,账号 admin) # 目录不在这里覆盖:一律走 APPS 表(ym → /home/work/ym-a11)。 # 早期这里曾写死 DEPLOY_DIR_OVERRIDE=/home/work/starpivot/app,导致部 ym 时落到该目录、 diff --git a/deploy/console/prod-deploy.sh b/deploy/console/prod-deploy.sh index e717c155..cc49ad3b 100755 --- a/deploy/console/prod-deploy.sh +++ b/deploy/console/prod-deploy.sh @@ -60,7 +60,7 @@ env_profile() { ENV_DESC="阿龙生产环境 · 阿里云" registry_profile along || return 1 # 镜像仓库:registry.ymaikj.com(实体在阿龙测试机 8.133.166.29,账号 admin) DEPLOY_HOST="47.116.104.181" # ← 阿龙生产服务器 IP - DEPLOY_KEY="/Users/liwei1dao/liwei/密钥/along/releae-guangzhou.pem" # 与阿龙测试机同一把钥匙(2026-08-18 由 ~/.ssh/along.pem 迁到此处;文件名 releae 是原文件笔误) + DEPLOY_KEY="${DEPLOY_KEY:-/Users/liwei1dao/liwei/密钥/along/releae-guangzhou.pem}" # 与阿龙测试机同一把钥匙(2026-08-18 由 ~/.ssh/along.pem 迁到此处;文件名 releae 是原文件笔误) DEPLOY_DIR="/home/work/starpivot/console" ;; *) red "✗ 未知环境: ${DEPLOY_ENV}(应为 ${PROD_ENVS[*]// /|})"; exit 1 ;;

    BkpXS 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�IQRaO`VV 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%0D%8Jz-tZ9_z8q3fN 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