From fae8064ef6c99c87368cfd0fc21093574ec55ad6 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Fri, 21 Mar 2025 14:50:23 +0000 Subject: [PATCH] v001 --- ios/Runner.xcodeproj/project.pbxproj | 4 + ios/Runner/AudioSessionManager.swift | 191 +++++++ ios/Runner/AzureAsrHelper.swift | 57 +-- ios/Runner/AzureTtsHelper.swift | 42 +- ios/Runner/BluetoothMediaButtonHelper.swift | 70 ++- ios/Runner/ClassicBluetoothHelper.swift | 18 +- lib/core/bindings/initial_binding.dart | 16 +- .../events/voice_interaction_event.dart | 29 ++ lib/data/models/message_model.dart | 58 +++ .../bluetooth_media_button_service.dart | 15 +- .../android_voice_interaction_service.dart | 341 +++++++++++++ .../ios_voice_interaction_service.dart | 468 ++++++++++++++++++ .../services/voice_interaction_service.dart | 345 +------------ .../chat/controllers/chat_controller.dart | 95 ++-- .../home/controllers/home_controller.dart | 12 +- .../controllers/pairing_controller.dart | 19 +- .../controllers/translation_controller.dart | 20 +- macos/Podfile.lock | 20 + 18 files changed, 1288 insertions(+), 532 deletions(-) create mode 100644 ios/Runner/AudioSessionManager.swift create mode 100644 lib/data/models/events/voice_interaction_event.dart create mode 100644 lib/data/models/message_model.dart create mode 100644 lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart create mode 100644 lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 626e05a1b..80b4d4cf8 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29672D8C25660032BFCB /* sample.mp3 */; }; 0ADE296D2D8C66D60032BFCB /* AzureTtsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */; }; 0ADE296E2D8C66D60032BFCB /* AzureAsrHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */; }; + 0ADE29702D8C6C450032BFCB /* AudioSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */; }; @@ -56,6 +57,7 @@ 0ADE29682D8C25660032BFCB /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = ""; }; 0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureAsrHelper.swift; sourceTree = ""; }; 0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureTtsHelper.swift; sourceTree = ""; }; + 0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionManager.swift; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; @@ -155,6 +157,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */, 0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */, 0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */, 0ADE29672D8C25660032BFCB /* sample.mp3 */, @@ -419,6 +422,7 @@ 0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */, + 0ADE29702D8C6C450032BFCB /* AudioSessionManager.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/Runner/AudioSessionManager.swift b/ios/Runner/AudioSessionManager.swift new file mode 100644 index 000000000..1b1a4df76 --- /dev/null +++ b/ios/Runner/AudioSessionManager.swift @@ -0,0 +1,191 @@ +import Foundation +import AVFoundation + +/// 统一音频会话管理类 +/// 用于统一管理所有组件的音频会话配置 +@objc class AudioSessionManager: NSObject { + // MARK: - 单例实现 + + @objc static let shared = AudioSessionManager() + + // 音频会话 + private let audioSession = AVAudioSession.sharedInstance() + + // 是否已经配置过音频会话 + private var isConfigured = false + + // 当前配置类型 + private var currentCategory: AVAudioSession.Category = .playAndRecord + private var currentMode: AVAudioSession.Mode = .default + private var currentOptions: AVAudioSession.CategoryOptions = [.allowBluetooth, .allowBluetoothA2DP] + + // 私有初始化方法,确保只能通过shared访问 + private override init() { + super.init() + NSLog("[AudioSessionManager] 音频会话管理器已初始化") + } + + // MARK: - 音频会话配置 + + /// 在应用启动时调用一次性配置方法 + /// - Parameters: + /// - category: 音频会话类别 + /// - mode: 音频会话模式 + /// - options: 音频会话选项 + /// - Returns: 是否配置成功 + @objc func initialConfiguration(category: AVAudioSession.Category = .playAndRecord, + mode: AVAudioSession.Mode = .default, + options: AVAudioSession.CategoryOptions = [.allowBluetooth, .allowBluetoothA2DP]) -> Bool { + if isConfigured { + NSLog("[AudioSessionManager] 音频会话已经配置过,忽略初始化请求") + return true + } + + do { + NSLog("[AudioSessionManager] 执行一次性音频会话配置: \(category)") + try audioSession.setCategory(category, mode: mode, options: options) + try audioSession.setActive(true) + + // 记录当前配置 + currentCategory = category + currentMode = mode + currentOptions = options + isConfigured = true + + return true + } catch { + NSLog("[AudioSessionManager] 一次性配置音频会话失败: \(error.localizedDescription)") + return false + } + } + + /// 配置音频会话,适用于语音识别和合成 + @objc func configureForVoiceProcessing() throws { + if !applyConfigurationIfNeeded(category: .playAndRecord, + mode: .spokenAudio, + options: [.allowBluetooth, .allowBluetoothA2DP, .duckOthers]) { + NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过语音处理模式配置") + } + } + + /// 配置音频会话,适用于蓝牙耳机连接和监听 + @objc func configureForBluetoothHeadset() throws { + if !applyConfigurationIfNeeded(category: .playAndRecord, + mode: .default, + options: [.allowBluetooth, .allowBluetoothA2DP]) { + NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过蓝牙耳机模式配置") + } + } + + /// 配置音频会话,适用于媒体播放和蓝牙媒体按钮 + @objc func configureForMediaPlayback() throws { + if !applyConfigurationIfNeeded(category: .playback, + mode: .default, + options: [.allowBluetooth, .allowBluetoothA2DP]) { + NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过媒体播放模式配置") + } + } + + /// 配置自定义音频会话 + @objc func configure(category: AVAudioSession.Category, + mode: AVAudioSession.Mode, + options: AVAudioSession.CategoryOptions) throws { + if !applyConfigurationIfNeeded(category: category, mode: mode, options: options) { + NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过自定义模式配置") + } + } + + /// 内部方法:如果尚未配置,应用新的配置 + private func applyConfigurationIfNeeded(category: AVAudioSession.Category, + mode: AVAudioSession.Mode, + options: AVAudioSession.CategoryOptions) -> Bool { + // 如果已经配置过,不再重复配置 + if isConfigured { + return false + } + + do { + NSLog("[AudioSessionManager] 应用音频会话配置: \(category)") + try audioSession.setCategory(category, mode: mode, options: options) + try audioSession.setActive(true) + + // 记录当前配置 + currentCategory = category + currentMode = mode + currentOptions = options + isConfigured = true + + return true + } catch { + NSLog("[AudioSessionManager] 配置音频会话失败: \(error.localizedDescription)") + return false + } + } + + /// 获取当前音频会话的配置信息 + @objc func getCurrentConfiguration() -> [String: Any] { + return [ + "isConfigured": isConfigured, + "category": "\(currentCategory)", + "mode": "\(currentMode)" + ] + } + + /// 仅在特殊情况下使用:强制重新配置音频会话 + @objc func forceReconfigure() -> Bool { + isConfigured = false + + do { + NSLog("[AudioSessionManager] 强制重新配置音频会话") + try audioSession.setCategory(currentCategory, mode: currentMode, options: currentOptions) + try audioSession.setActive(true) + isConfigured = true + return true + } catch { + NSLog("[AudioSessionManager] 强制重新配置失败: \(error.localizedDescription)") + return false + } + } + + /// 激活音频会话(如果已配置) + @objc func activateSession() throws { + if isConfigured { + try audioSession.setActive(true) + } else { + NSLog("[AudioSessionManager] 警告: 尝试激活未配置的音频会话") + // 应用默认配置并激活 + try applyConfigurationIfNeeded(category: .playAndRecord, + mode: .default, + options: [.allowBluetooth, .allowBluetoothA2DP]) + } + } + + /// 停用音频会话(保留配置) + @objc func deactivateSession() throws { + try audioSession.setActive(false, options: .notifyOthersOnDeactivation) + } + + /// 获取当前音频会话路由描述 + @objc func getCurrentRoute() -> AVAudioSessionRouteDescription { + return audioSession.currentRoute + } + + /// 添加路由变化监听器 + @objc func addRouteChangeListener(_ observer: Any, selector: Selector) { + NotificationCenter.default.addObserver( + observer, + selector: selector, + name: AVAudioSession.routeChangeNotification, + object: nil + ) + } + + /// 移除路由变化监听器 + @objc func removeRouteChangeListener(_ observer: Any) { + NotificationCenter.default.removeObserver( + observer, + name: AVAudioSession.routeChangeNotification, + object: nil + ) + } +} \ No newline at end of file diff --git a/ios/Runner/AzureAsrHelper.swift b/ios/Runner/AzureAsrHelper.swift index 4bd02368f..87619c037 100644 --- a/ios/Runner/AzureAsrHelper.swift +++ b/ios/Runner/AzureAsrHelper.swift @@ -28,6 +28,9 @@ class AzureAsrHelper: NSObject { private var supportedLanguages: [String] = ["zh-CN", "en-US"] private var isAutoDetectLanguage = false + /// 音频会话管理器 + private let audioSessionManager = AudioSessionManager.shared + // MARK: - 初始化 init(eventHandler: @escaping (String, [String: Any]) -> Void) { @@ -138,39 +141,8 @@ class AzureAsrHelper: NSObject { /// 设置音频会话 private func setupAudioSession() throws { - let audioSession = AVAudioSession.sharedInstance() - - // 使用playAndRecord类别允许同时录音和播放 - try audioSession.setCategory(.playAndRecord, - mode: .voiceChat, // 使用voiceChat模式能够更好地支持回音消除 - options: [.allowBluetooth, .defaultToSpeaker, .allowAirPlay]) - - // 设置首选的输入和输出 - let currentRoute = audioSession.currentRoute - - // 获取当前是否连接了耳机或外部麦克风 - let hasHeadphones = currentRoute.outputs.contains { - $0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP - } - - // 如果没有耳机,明确启用内置麦克风和扬声器的回音消除 - if !hasHeadphones { - try audioSession.setMode(.voiceChat) // 语音聊天模式有更强的回音消除 - - // 启用回音消除和噪声抑制 - try audioSession.setInputGain(0.8) // 适当降低输入增益以减少扬声器音频被麦克风捕获的可能性 - } else { - // 耳机模式,可以使用不同的设置 - try audioSession.setMode(.voiceChat) - try audioSession.setInputGain(1.0) - } - - // 设置合适的采样率 - try audioSession.setPreferredSampleRate(16000.0) // Azure语音识别推荐的采样率 - try audioSession.setPreferredIOBufferDuration(0.01) // 较小的缓冲区大小以减少延迟 - - // 激活音频会话 - try audioSession.setActive(true, options: .notifyOthersOnDeactivation) + // 使用音频会话管理器配置语音处理模式 + try audioSessionManager.configureForVoiceProcessing() print("[AzureAsrHelper] 音频会话配置成功,已启用回音消除") } @@ -367,28 +339,21 @@ class AzureAsrHelper: NSObject { /// 释放资源 func dispose() { - print("[AzureAsrHelper] 释放资源") - - // 停止连续识别 + // 停止所有识别操作 if _isContinuousRecognitionActive { stopContinuousRecognition() } - // 释放音频会话 - do { - try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) - } catch { - print("[AzureAsrHelper] 警告: 释放音频会话失败: \(error.localizedDescription)") - } - - // 释放资源 + // 释放识别器 recognizer = nil - speechConfig = nil audioConfig = nil + speechConfig = nil // 重置状态 - _isContinuousRecognitionActive = false isInitialized = false + _isContinuousRecognitionActive = false + + print("[AzureAsrHelper] 资源已释放") } /// 从结果中获取检测到的语言 diff --git a/ios/Runner/AzureTtsHelper.swift b/ios/Runner/AzureTtsHelper.swift index 589df617c..42c9f97db 100644 --- a/ios/Runner/AzureTtsHelper.swift +++ b/ios/Runner/AzureTtsHelper.swift @@ -29,6 +29,9 @@ class AzureTtsHelper: NSObject { /// 音频会话配置 private var isAudioSessionConfigured = false + /// 音频会话管理器 + private let audioSessionManager = AudioSessionManager.shared + // MARK: - 语音设置 /// 当前语音 @@ -118,33 +121,9 @@ class AzureTtsHelper: NSObject { /// 配置音频会话 private func configureAudioSession() -> Bool { - let audioSession = AVAudioSession.sharedInstance() do { - // 使用playback类别,但支持混合和空中播放 - try audioSession.setCategory(.playback, - mode: .spokenAudio, - options: [.mixWithOthers, .allowAirPlay, .duckOthers]) - - // 根据设备类型选择最佳配置 - let currentRoute = audioSession.currentRoute - let hasHeadphones = currentRoute.outputs.contains { - $0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP - } - - // 优化音频路由 - if hasHeadphones { - // 耳机模式,使用默认设置 - try audioSession.setPreferredIOBufferDuration(0.005) // 较小的缓冲区大小以减少延迟 - } else { - // 扬声器模式 - try audioSession.setPreferredIOBufferDuration(0.005) - } - - // 避免完全激活音频会话,因为ASR可能已经激活 - // 这里使用setActive(false)是为了不与ASR冲突 - if !audioSession.isOtherAudioPlaying { - try audioSession.setActive(true, options: .notifyOthersOnDeactivation) - } + // 使用音频会话管理器配置媒体播放模式 + try audioSessionManager.configureForMediaPlayback() isAudioSessionConfigured = true print("[AzureTtsHelper] 音频会话配置成功") @@ -297,16 +276,21 @@ class AzureTtsHelper: NSObject { /// 释放资源 func dispose() { - try? stopSpeaking() + // 停止合成 + if _isSpeaking { + stopSpeaking() + } - // 释放合成器和配置 + // 释放合成器 synthesizer = nil speechConfig = nil + // 重置状态 isInitialized = false _isSpeaking = false isAudioSessionConfigured = false - print("[AzureTtsHelper] TTS 引擎已释放") + + print("[AzureTtsHelper] 资源已释放") } // MARK: - 私有辅助方法 diff --git a/ios/Runner/BluetoothMediaButtonHelper.swift b/ios/Runner/BluetoothMediaButtonHelper.swift index d385567ca..a7c103e5d 100644 --- a/ios/Runner/BluetoothMediaButtonHelper.swift +++ b/ios/Runner/BluetoothMediaButtonHelper.swift @@ -39,6 +39,9 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate { /// 总时长 - 模拟 private let totalDuration: TimeInterval = 300.0 + /// 音频会话管理器 + private let audioSessionManager = AudioSessionManager.shared + // MARK: - 初始化 override init() { @@ -86,7 +89,7 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate { // 尝试启用静音音频播放(但即使失败,远程命令仍然可用) do { - try passivelySetupAudioSession() + // try passivelySetupAudioSession() startSilentAudio() //延时1秒 @@ -320,14 +323,17 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate { NSLog("\(TAG): 当前没有活跃的音频,设置最小化会话配置") - // 使用最不侵入的配置 - try audioSession.setCategory(.ambient, mode: .default) - - // 注意:我们不设置活跃状态,让系统自己管理 + // 使用音频会话管理器配置媒体播放模式 + try audioSessionManager.configure(category: .ambient, mode: .default, options: []) NSLog("\(TAG): 被动音频会话配置完成") } + /// 检查是否有其他应用正在播放音频 + private func isOtherAudioPlaying() -> Bool { + return AVAudioSession.sharedInstance().isOtherAudioPlaying + } + // MARK: - 命令处理方法 /// 处理播放/暂停命令 @@ -344,27 +350,6 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate { // 发送事件到处理器 eventHandler?.sendEvent(eventData) - - - // 检查是否有其他音频播放 - // let audioSession = AVAudioSession.sharedInstance() - // let isOtherAudioPlaying = audioSession.isOtherAudioPlaying - - // if isOtherAudioPlaying { - // NSLog("\(TAG): 检测到其他音频在播放") - // // 注意:不能直接控制其他应用的媒体,iOS不允许这种跨应用控制 - // } - - // // 更新播放状态(切换) - // let isCurrentlyPlaying = (silentAudioPlayer?.isPlaying ?? false) - // if isCurrentlyPlaying { - // stopSilentAudio() - // setupNowPlaying(isPlaying: false) - // } else { - // startSilentAudio() - // setupNowPlaying(isPlaying: true) - // } - return .success } @@ -382,13 +367,6 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate { // 发送事件到处理器 eventHandler?.sendEvent(eventData) - // 检查是否有其他音频播放 - let audioSession = AVAudioSession.sharedInstance() - if audioSession.isOtherAudioPlaying { - NSLog("\(TAG): 检测到其他音频在播放") - // 注意:不能直接控制其他应用的媒体,iOS不允许这种跨应用控制 - } - return .success } @@ -406,13 +384,6 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate { // 发送事件到处理器 eventHandler?.sendEvent(eventData) - // 检查是否有其他音频播放 - let audioSession = AVAudioSession.sharedInstance() - if audioSession.isOtherAudioPlaying { - NSLog("\(TAG): 检测到其他音频在播放") - // 注意:不能直接控制其他应用的媒体,iOS不允许这种跨应用控制 - } - return .success } @@ -491,6 +462,25 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate { startSilentAudio() } } + + /// 清理和释放资源 + func dispose() { + NSLog("\(TAG): 释放BluetoothMediaButtonHelper资源") + // 停止监听 + stopButtonListening() + + // 释放播放器和计时器资源 + silentAudioPlayer = nil + if let timer = playbackTimer { + timer.invalidate() + playbackTimer = nil + } + + // 清除引用 + eventHandler = nil + + NSLog("\(TAG): 资源已释放") + } } extension UInt32 { diff --git a/ios/Runner/ClassicBluetoothHelper.swift b/ios/Runner/ClassicBluetoothHelper.swift index ea28b553b..1f5c3c42d 100644 --- a/ios/Runner/ClassicBluetoothHelper.swift +++ b/ios/Runner/ClassicBluetoothHelper.swift @@ -11,8 +11,8 @@ import UIKit @objc class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate { // MARK: - 属性 - // 音频会话 - private let audioSession = AVAudioSession.sharedInstance() + // 音频会话管理器 + private let audioSessionManager = AudioSessionManager.shared // 蓝牙管理器 private var centralManager: CBCentralManager? @@ -85,12 +85,7 @@ import UIKit // 启动蓝牙设备监控 @objc func startBluetoothDeviceMonitoring() { NotificationCenter.default.removeObserver(self) - NotificationCenter.default.addObserver( - self, - selector: #selector(handleRouteChange(_:)), - name: AVAudioSession.routeChangeNotification, - object: nil - ) + audioSessionManager.addRouteChangeListener(self, selector: #selector(handleRouteChange(_:))) } // 处理音频路由变化 @@ -202,7 +197,7 @@ import UIKit var result: [[String: String]] = [] // 获取当前音频路由 - let outputs = audioSession.currentRoute.outputs + let outputs = audioSessionManager.getCurrentRoute().outputs // 过滤蓝牙相关设备 for output in outputs where isBluetoothOutput(output.portType) { @@ -218,15 +213,14 @@ import UIKit // 释放资源 @objc func dispose() { - NotificationCenter.default.removeObserver(self) + audioSessionManager.removeRouteChangeListener(self) eventHandler = nil } // 配置音频会话 private func configureAudioSession() { do { - try audioSession.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .allowBluetoothA2DP]) - try audioSession.setActive(true) + try audioSessionManager.configureForBluetoothHeadset() } catch {} } diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 96978a573..9752548b5 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -1,4 +1,5 @@ import 'package:get/get.dart'; +import 'dart:io'; import '../../data/services/speech_factory.dart'; import '../../data/services/chat_history_service.dart'; import '../../data/services/volcano_translation_service.dart'; @@ -8,6 +9,9 @@ import '../../data/services/volcano_ai_service.dart'; import '../../data/services/classic_bluetooth_service.dart'; import '../../data/services/bluetooth_media_button_service.dart'; import '../../core/utils/logger.dart'; +import '../../data/services/voice_interaction_impl/ios_voice_interaction_service.dart'; +import '../../data/services/voice_interaction_impl/android_voice_interaction_service.dart'; + /// 初始绑定,用于管理全局依赖 class InitialBinding extends Bindings { @@ -31,13 +35,21 @@ class InitialBinding extends Bindings { // 火山AI服务 Get.lazyPut(() => VolcanoAIService(), fenix: true); - // 注意:VoiceInteractionService 在 HomeController 中初始化 - Get.lazyPut(() => VoiceInteractionService(), fenix: true); // 经典蓝牙服务 Get.lazyPut(() => ClassicBluetoothService(), fenix: true); // 蓝牙媒体按钮服务 Get.lazyPut(() => BluetoothMediaButtonService(), fenix: true); + + + + // 根据平台注册对应的VoiceInteractionService实现 + if (Platform.isIOS) { + Get.lazyPut(() => IosVoiceInteractionService(), fenix: true); + + } else { + Get.lazyPut(() => AndroidVoiceInteractionService(), fenix: true); + } } } \ No newline at end of file diff --git a/lib/data/models/events/voice_interaction_event.dart b/lib/data/models/events/voice_interaction_event.dart new file mode 100644 index 000000000..4bfa636df --- /dev/null +++ b/lib/data/models/events/voice_interaction_event.dart @@ -0,0 +1,29 @@ +import 'package:meta/meta.dart'; + +/// 语音交互事件基类 +abstract class VoiceInteractionEvent { + final int timestamp; + + VoiceInteractionEvent({required this.timestamp}); +} + +/// 聊天历史事件 +class ChatHistoryEvent extends VoiceInteractionEvent { + final String agentId; + final String userMessage; + final String assistantMessage; + + ChatHistoryEvent({ + required this.agentId, + required this.userMessage, + required this.assistantMessage, + required int timestamp, + }) : super(timestamp: timestamp); +} + +/// 语音识别开始事件 +class RecognitionStartedEvent extends VoiceInteractionEvent { + RecognitionStartedEvent({ + required int timestamp, + }) : super(timestamp: timestamp); +} \ No newline at end of file diff --git a/lib/data/models/message_model.dart b/lib/data/models/message_model.dart new file mode 100644 index 000000000..6307989ae --- /dev/null +++ b/lib/data/models/message_model.dart @@ -0,0 +1,58 @@ +class Message { + final String role; // 'user' or 'assistant' + final String content; + final DateTime timestamp; + final bool isLoading; + + Message({ + required this.role, + required this.content, + required this.timestamp, + this.isLoading = false, + }); + + // 从JSON构造函数 + factory Message.fromJson(Map json) { + return Message( + role: json['role'] as String, + content: json['content'] as String, + timestamp: DateTime.parse(json['timestamp'] as String), + isLoading: json['isLoading'] as bool? ?? false, + ); + } + + // 转换为JSON + Map toJson() { + return { + 'role': role, + 'content': content, + 'timestamp': timestamp.toIso8601String(), + 'isLoading': isLoading, + }; + } + + // 创建一个加载中的消息 + factory Message.loading() { + return Message( + role: 'assistant', + content: '', + timestamp: DateTime.now(), + isLoading: true, + ); + } + + // 复制并修改 + Message copyWith({ + String? role, + String? content, + DateTime? timestamp, + bool? isLoading, + }) { + return Message( + role: role ?? this.role, + content: content ?? this.content, + timestamp: timestamp ?? this.timestamp, + isLoading: isLoading ?? this.isLoading, + ); + } +} \ No newline at end of file diff --git a/lib/data/services/bluetooth_media_button_service.dart b/lib/data/services/bluetooth_media_button_service.dart index 984e9140d..559928ab8 100644 --- a/lib/data/services/bluetooth_media_button_service.dart +++ b/lib/data/services/bluetooth_media_button_service.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'dart:io' show Platform; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import '../../core/utils/logger.dart'; - +import '../../core/utils/logger.dart'; +import 'voice_interaction_service.dart'; /// 蓝牙媒体按钮事件类型 enum MediaButtonType { /// 播放/暂停按钮 @@ -175,7 +175,6 @@ class BluetoothMediaButtonService extends GetxService { } final Map eventMap = event; - Logger.info('蓝牙媒体按钮事件Map: $eventMap'); try { if (eventMap['type'] == 'mediaButtonEvent') { @@ -194,7 +193,17 @@ class BluetoothMediaButtonService extends GetxService { timestamp: eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch, ); + // 唤醒语音交互服务 + try { + + Get.find().onWakeup(); + + } catch (e) { + Logger.error('唤醒语音交互服务失败: $e'); + } + // 广播事件 + _buttonEventController.add(buttonEvent); // 广播事件 _buttonEventController.add(buttonEvent); diff --git a/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart b/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart new file mode 100644 index 000000000..203bbf774 --- /dev/null +++ b/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart @@ -0,0 +1,341 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import '../../../core/utils/logger.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import '../../../modules/chat/models/message_model.dart'; +import '../../models/events/voice_interaction_event.dart'; +import '../chat_history_service.dart'; +import '../voice_interaction_service.dart'; + +/// Android语音交互服务 +/// +/// 该服务提供了与Android端的VoiceInteractionService.kt通信的接口, +/// 用于管理后台语音交互服务的生命周期和接收语音交互事件 +class AndroidVoiceInteractionService extends GetxService implements VoiceInteractionService { + static AndroidVoiceInteractionService get to => Get.find(); + // 方法通道和事件通道 + static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction'); + static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events'); + + // 服务状态 + final _isServiceRunning = false.obs; + bool get isServiceRunning => _isServiceRunning.value; + + // 事件流控制器 + StreamController? _eventStreamController; + Stream? _eventStream; + @override + Stream get eventStream => _eventStream ?? Stream.empty(); + + // 事件通道状态 + StreamSubscription? _eventSubscription; + + // 配置信息 + late String _azureSpeechKey; + late String _azureSpeechRegion; + late String _volcanoAiApiKey; + + // 初始化状态标志 + static bool _isInitialized = false; + + // 当前会话历史 + final List> _messageHistory = []; + + // 服务ID + static const String _serviceId = 'android_assistant'; + + /// 构造函数 + AndroidVoiceInteractionService() { + + } + + /// 从环境变量加载配置 + void _loadConfig() { + _azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; + _azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; + _volcanoAiApiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? ''; + + if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) { + Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); + } + + if (_volcanoAiApiKey.isEmpty) { + Logger.warning('未找到火山 AI API 密钥。请在 .env 文件中设置 VOLCANO_AI_API_KEY'); + } + } + + /// 设置事件通道 + void _setupEventChannel() { + _eventSubscription = _eventChannel + .receiveBroadcastStream() + .listen((event) { + if (event is Map) { + final String eventType = event['type'] as String? ?? ''; + + // 直接处理事件,不再等待channelReady + _handleVoiceInteractionEvent(event); + } + }, onError: (error) { + Logger.error('语音交互事件通道错误: $error'); + }); + } + + /// 创建事件流 + void _createEventStream() { + _eventStreamController = StreamController.broadcast(); + _eventStream = _eventStreamController?.stream; + } + + /// 初始化服务 + @override + Future initialize() async { + try { + if (_isInitialized) { + return this; + } + _isInitialized = true; + + _loadConfig(); + _setupEventChannel(); + _createEventStream(); + // 检查服务是否正在运行 + await _checkServiceStatus(); + + await _startService(); + + Logger.info('语音交互服务初始化完成'); + return this; + } catch (e) { + Logger.error('语音交互服务初始化失败: $e'); + return this; + } + } + + @override + void onWakeup() { + Logger.info('语音交互服务唤醒'); + } + + /// 检查服务状态 + Future _checkServiceStatus() async { + try { + final bool isRunning = await _channel.invokeMethod('isVoiceInteractionServiceRunning') ?? false; + _isServiceRunning.value = isRunning; + Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}'); + } catch (e) { + Logger.error('检查语音交互服务状态失败: $e'); + _isServiceRunning.value = false; + } + } + + /// 启动语音交互服务 + Future _startService() async { + if (_isServiceRunning.value) { + Logger.info('语音交互服务已经在运行'); + return true; + } + + try { + final bool result = await _channel.invokeMethod('startVoiceInteractionService', { + 'azure_speech_key': _azureSpeechKey, + 'azure_speech_region': _azureSpeechRegion, + 'volcano_ai_api_key': _volcanoAiApiKey, + }) ?? false; + + if (result) { + _isServiceRunning.value = true; + Logger.info('语音交互服务启动成功'); + } else { + Logger.error('语音交互服务启动失败'); + } + + return result; + } catch (e) { + Logger.error('启动语音交互服务失败: $e'); + return false; + } + } + + /// 停止语音交互服务 + Future _stopService() async { + if (!_isServiceRunning.value) { + Logger.info('语音交互服务未运行'); + return true; + } + + try { + final bool result = await _channel.invokeMethod('stopVoiceInteractionService') ?? false; + + if (result) { + _isServiceRunning.value = false; + Logger.info('语音交互服务停止成功'); + } else { + Logger.error('语音交互服务停止失败'); + } + + return result; + } catch (e) { + Logger.error('停止语音交互服务失败: $e'); + return false; + } + } + + /// 暂停语音交互(停止TTS和语音识别,但保持服务运行) + @override + Future pauseVoiceInteraction() async { + if (!_isServiceRunning.value) { + Logger.info('语音交互服务未运行,无法暂停'); + return false; + } + + try { + final bool result = await _channel.invokeMethod('pauseVoiceInteraction') ?? false; + + if (result) { + Logger.info('语音交互暂停成功'); + } else { + Logger.error('语音交互暂停失败'); + } + + return result; + } catch (e) { + Logger.error('暂停语音交互失败: $e'); + return false; + } + } + + /// 处理来自原生端的语音交互事件 + void _handleVoiceInteractionEvent(dynamic event) { + if (event is! Map || _eventStreamController == null) return; + + final Map eventMap = event; + final String eventType = eventMap['type'] as String? ?? ''; + final int timestamp = eventMap['timestamp'] as int? ?? 0; + + // 添加时间戳日志,帮助调试 + Logger.info('收到原生端事件: $eventType, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}'); + + switch (eventType) { + case 'chatHistory': + final String agentId = eventMap['agentId'] as String? ?? ''; + final String userMessage = eventMap['userMessage'] as String? ?? ''; + final String assistantMessage = eventMap['assistantMessage'] as String? ?? ''; + + // 添加到消息历史 + if (userMessage.isNotEmpty) { + _messageHistory.add({'role': 'user', 'content': userMessage}); + _messageHistory.add({'role': 'assistant', 'content': assistantMessage}); + + // 保持历史记录在一定长度 + while (_messageHistory.length > 10) { + _messageHistory.removeAt(0); + } + } + + _eventStreamController?.add(ChatHistoryEvent( + agentId: agentId, + userMessage: userMessage, + assistantMessage: assistantMessage, + timestamp: timestamp, + )); + + // 保存聊天记录 + _saveChatHistory(agentId, userMessage, assistantMessage, timestamp); + + Logger.info('收到聊天历史事件: agentId=$agentId'); + break; + + case 'recognitionStarted': + _eventStreamController?.add(RecognitionStartedEvent( + timestamp: timestamp, + )); + break; + + default: + Logger.warning('收到未知类型的语音交互事件: $eventType'); + break; + } + } + + @override + void onClose() { + // 清理资源 + _stopService(); + _eventSubscription?.cancel(); + _eventStreamController?.close(); + super.onClose(); + } + + /// 保存聊天记录 + void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) { + try { + // 获取ChatHistoryService实例 + final chatHistoryService = Get.find(); + + // 创建用户消息和助手消息 + final userMsg = Message( + role: 'user', + content: userMessage, + timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp), + ); + + final assistantMsg = Message( + role: 'assistant', + content: assistantMessage, + timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息 + ); + + // 加载现有历史记录 + final existingMessages = chatHistoryService.loadHistory(agentId); + + // 添加新消息 + existingMessages.addAll([userMsg, assistantMsg]); + + // 保存更新后的历史记录 + chatHistoryService.saveHistory(agentId, existingMessages); + } catch (e) { + Logger.error('保存聊天记录失败: $e'); + } + } + + /// 是否处于活跃状态 + @override + bool isActive() { + return _isServiceRunning.value; + } + + /// 获取当前对话历史 + @override + List> getMessageHistory() { + return List>.from(_messageHistory); + } + + /// 获取当前服务ID + @override + String getServiceId() { + return _serviceId; + } +} + + +/// 聊天历史事件 +class ChatHistoryEvent extends VoiceInteractionEvent { + final String agentId; + final String userMessage; + final String assistantMessage; + + ChatHistoryEvent({ + required this.agentId, + required this.userMessage, + required this.assistantMessage, + required int timestamp, + }) : super(timestamp: timestamp); +} + +/// 语音识别开始事件 +class RecognitionStartedEvent extends VoiceInteractionEvent { + RecognitionStartedEvent({ + required int timestamp, + }) : super(timestamp: timestamp); +} \ No newline at end of file diff --git a/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart b/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart new file mode 100644 index 000000000..ac339871d --- /dev/null +++ b/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart @@ -0,0 +1,468 @@ +import 'dart:async'; +import 'package:get/get.dart'; +import '../asr_service.dart'; +import '../tts_service.dart'; +import '../volcano_ai_service.dart'; +import '../chat_history_service.dart'; +import '../../../modules/chat/models/message_model.dart'; +import '../../models/events/voice_interaction_event.dart'; +import '../../../core/utils/logger.dart'; +import '../voice_interaction_service.dart'; + +enum VoiceInteractionState { + idle, // 等待唤醒 + active, // 活跃状态 - 可以同时识别和播放 +} + +class IosVoiceInteractionService extends GetxService implements VoiceInteractionService { + final AsrService _asrService = Get.find(); + final TtsService _ttsService = Get.find(); + final VolcanoAIService _aiService = Get.find(); + final ChatHistoryService _chatHistoryService = Get.find(); + + // 当前状态 + final Rx state = VoiceInteractionState.idle.obs; + + // 是否识别到用户语音 + final RxBool isSpeechDetected = false.obs; + + // 是否正在处理AI响应 + final RxBool isProcessingAI = false.obs; + + // 是否正在播放TTS + final RxBool isSpeaking = false.obs; + + // 对话历史记录 + final List> _messageHistory = []; + + // 最大历史记录数 + static const int _maxHistorySize = 10; + + // 超时计时器 + Timer? _inactivityTimer; + + // 流订阅 + StreamSubscription? _recognitionSubscription; + + // 当前识别的文本 + String _currentRecognizedText = ''; + + // 事件流控制器 + final StreamController _eventStreamController = + StreamController.broadcast(); + + // 获取事件流 + @override + Stream get eventStream => _eventStreamController.stream; + + // 标识符,用于存储聊天历史 + final String _agentId = 'ios_assistant'; + + /// 初始化服务 + @override + Future initialize() async { + try { + // 保持idle状态,不自动启动语音识别 + Logger.info('iOS语音交互服务初始化完成,处于idle状态'); + } catch (e) { + Logger.error('iOS语音交互服务初始化失败: $e'); + } + + return this; + } + + // 处理唤醒事件 + void onWakeup() { + _ttsService.speakOnce('我在呢!'); + isSpeaking.value = true; + + if (state.value == VoiceInteractionState.idle) { + _activateInteraction(); + } + } + + // 激活交互 + void _activateInteraction() { + // 设置状态为活跃 + state.value = VoiceInteractionState.active; + + // 开始连续监听 + _startContinuousListening(); + + // 启动不活动计时器 + _startInactivityTimer(); + + // 发送识别开始事件 + _sendRecognitionStartedEvent(); + } + + // 开始连续监听 + Future _startContinuousListening() async { + // 停止之前的监听 + _recognitionSubscription?.cancel(); + + try { + // 开始连续识别 + final recognitionStream = await _asrService.startContinuousRecognition(); + + // 监听识别结果 + _recognitionSubscription = recognitionStream.listen( + _handleRecognitionEvent, + onError: (error) { + Logger.error('语音识别错误: $error'); + _startContinuousListening(); // 尝试重新启动 + } + ); + + Logger.info('开始连续语音识别'); + } catch (e) { + Logger.error('启动语音识别失败: $e'); + + } + } + + // 处理识别事件 + void _handleRecognitionEvent(RecognitionEvent event) { + // 更新活动时间 + _updateActivityTime(); + + switch (event.type) { + case RecognitionEventType.intermediateResult: + // 检测用户是否开始说话 + if (event.text.isNotEmpty && !isSpeechDetected.value) { + isSpeechDetected.value = true; + + // 用户开始讲话时,立即中断当前响应 + _interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); + } + break; + + case RecognitionEventType.finalResult: + if (event.text.isNotEmpty) { + // 最终结果,处理用户输入 + _processUserInput(event.text); + } + // 重置语音检测状态 + isSpeechDetected.value = false; + break; + + case RecognitionEventType.sessionStarted: + Logger.info('语音识别会话开始'); + break; + + case RecognitionEventType.sessionStopped: + Logger.info('语音识别会话结束'); + + break; + + case RecognitionEventType.error: + case RecognitionEventType.canceled: + Logger.error('语音识别错误: ${event.error}'); + + break; + } + } + + // 中断当前响应 + void _interruptCurrentResponse(String reason) { + Logger.info(reason); + + // 如果正在活跃状态,需要中断当前操作 + if (state.value == VoiceInteractionState.active) { + // 停止TTS播放 + _ttsService.stop(); + isSpeaking.value = false; + isProcessingAI.value = false; + } + } + + // 处理用户输入 + Future _processUserInput(String text) async { + Logger.info('处理用户输入: $text'); + + // 设置为处理状态 + isProcessingAI.value = true; + + // 添加用户消息到历史记录 + final userMessage = {'role': 'user', 'content': text}; + _addToHistory(userMessage); + + // 创建用户消息对象 + final userMsg = Message( + role: 'user', + content: text, + timestamp: DateTime.now(), + ); + + // AI响应处理标志 + bool isProcessingCancelled = false; + + try { + // 构建消息历史 + final messageHistory = _buildMessageHistory(); + + // 调用AI服务获取响应 + final responseStream = _aiService.sendMessageStream( + messages: messageHistory, + systemPrompt: "你是一个智能助手,请简明扼要地回答问题。", + ); + + String aiResponse = ''; + + // 等待AI响应 + await for (final chunk in responseStream) { + // 检查是否被用户打断 + if (isSpeechDetected.value) { + // 用户开始说话,标记处理被取消 + isProcessingCancelled = true; + Logger.info('AI响应生成过程中被用户打断'); + break; + } + + // 如果状态已改变(可能由其他原因导致),停止处理 + if (state.value != VoiceInteractionState.active) { + isProcessingCancelled = true; + break; + } + + aiResponse += chunk; + + // 立即播放当前文本块,实现边生成边播放 + _speakStreamResponse(chunk); + } + + // 如果处理被取消,不继续后续操作 + if (isProcessingCancelled) { + isProcessingAI.value = false; + return; + } + + // 确保完整响应被处理 + if (aiResponse.isNotEmpty) { + // 将AI响应添加到历史记录 + final assistantMessage = {'role': 'assistant', 'content': aiResponse}; + _addToHistory(assistantMessage); + + // 创建助手消息对象 + final assistantMsg = Message( + role: 'assistant', + content: aiResponse, + timestamp: DateTime.now(), + ); + + // 保存聊天历史 + _saveChatHistory(userMsg, assistantMsg); + + // 发送聊天历史事件 + _sendChatHistoryEvent(userMsg.content, assistantMsg.content); + + // 刷新TTS流,确保所有文本都被播放 + await _ttsService.flushStream(); + isSpeaking.value = false; + } + + // 处理完成 + isProcessingAI.value = false; + } catch (e) { + Logger.error('AI响应处理失败: $e'); + // 错误恢复 + isProcessingAI.value = false; + } + } + + // 构建用于AI服务的消息历史 + List> _buildMessageHistory() { + return List>.from(_messageHistory); + } + + // 添加消息到历史记录 + void _addToHistory(Map message) { + _messageHistory.add(message); + + // 限制历史记录大小 + while (_messageHistory.length > _maxHistorySize) { + _messageHistory.removeAt(0); + } + } + + // 播放流式响应 + void _speakStreamResponse(String chunk) { + if (chunk.isEmpty) return; + + // 更新播放状态 + isSpeaking.value = true; + + // 使用流式TTS播放 + _ttsService.speakStream(chunk); + + // 更新活动时间 + _updateActivityTime(); + } + + // 停止TTS + void _stopTts() { + _ttsService.stop(); + isSpeaking.value = false; + } + + // 更新最后活动时间 + DateTime _lastActivityTime = DateTime.now(); + void _updateActivityTime() { + _lastActivityTime = DateTime.now(); + } + + // 启动不活动计时器(长时间无交互会切换到空闲状态) + void _startInactivityTimer() { + _cancelInactivityTimer(); + + _inactivityTimer = Timer.periodic(Duration(seconds: 5), (timer) { + // 计算空闲时间 + final idleTime = DateTime.now().difference(_lastActivityTime).inSeconds; + + // 如果空闲超过30秒,且不在播放或检测到语音,切换到空闲 + if (idleTime > 8 && + !isSpeaking.value && + !isProcessingAI.value && + !isSpeechDetected.value) { + _resetToIdle(); + timer.cancel(); + } + }); + } + + // 取消不活动计时器 + void _cancelInactivityTimer() { + _inactivityTimer?.cancel(); + _inactivityTimer = null; + } + + // 重置到空闲状态 + void _resetToIdle() { + _ttsService.speakOnce('没有听到声音,暂停对话,双击耳机唤醒!'); + _cancelInactivityTimer(); + + // 停止TTS + _stopTts(); + + // 停止语音识别 + _recognitionSubscription?.cancel(); + _asrService.stopContinuousRecognition(); + + // 重置状态 + state.value = VoiceInteractionState.idle; + isSpeechDetected.value = false; + isProcessingAI.value = false; + isSpeaking.value = false; + + // 清空历史记录 + _messageHistory.clear(); + + Logger.info('语音交互已重置为空闲状态'); + } + + // 暂停语音交互 + @override + void pauseVoiceInteraction() { + if (state.value != VoiceInteractionState.idle) { + _stopTts(); + _recognitionSubscription?.cancel(); + _asrService.stopContinuousRecognition(); + + // 重置所有状态 + state.value = VoiceInteractionState.idle; + isSpeechDetected.value = false; + isProcessingAI.value = false; + isSpeaking.value = false; + + // 清空历史记录 + _messageHistory.clear(); + } + } + + // 恢复语音交互 + void resumeVoiceInteraction() { + if (state.value == VoiceInteractionState.idle) { + _activateInteraction(); + } + } + + // 手动停止交互 + void stopInteraction() { + _resetToIdle(); + } + + // 获取当前状态 + VoiceInteractionState getCurrentState() { + return state.value; + } + + // 是否处于活跃状态 + @override + bool isActive() { + return state.value != VoiceInteractionState.idle; + } + + // 获取当前对话历史 + @override + List> getMessageHistory() { + return List>.from(_messageHistory); + } + + // 获取当前服务ID + @override + String getServiceId() { + return _agentId; + } + + // 释放资源 + @override + void onClose() { + _cancelInactivityTimer(); + _recognitionSubscription?.cancel(); + _resetToIdle(); + _eventStreamController.close(); + super.onClose(); + } + + // 保存聊天历史 + void _saveChatHistory(Message userMsg, Message assistantMsg) { + try { + // 加载现有历史记录 + final existingMessages = _chatHistoryService.loadHistory(_agentId); + + // 添加新消息 + existingMessages.addAll([userMsg, assistantMsg]); + + // 保存更新后的历史记录 + _chatHistoryService.saveHistory(_agentId, existingMessages); + + Logger.info('保存聊天历史记录成功'); + } catch (e) { + Logger.error('保存聊天历史记录失败: $e'); + } + } + + // 发送聊天历史事件 + void _sendChatHistoryEvent(String userMessage, String assistantMessage) { + final event = ChatHistoryEvent( + agentId: _agentId, + userMessage: userMessage, + assistantMessage: assistantMessage, + timestamp: DateTime.now().millisecondsSinceEpoch, + ); + + _eventStreamController.add(event); + Logger.info('已发送聊天历史事件'); + } + + // 发送识别开始事件 + void _sendRecognitionStartedEvent() { + final event = RecognitionStartedEvent( + timestamp: DateTime.now().millisecondsSinceEpoch, + ); + + _eventStreamController.add(event); + Logger.info('已发送识别开始事件'); + } +} + diff --git a/lib/data/services/voice_interaction_service.dart b/lib/data/services/voice_interaction_service.dart index e58e19e4c..396fc6df3 100644 --- a/lib/data/services/voice_interaction_service.dart +++ b/lib/data/services/voice_interaction_service.dart @@ -1,334 +1,33 @@ import 'dart:async'; -import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import '../../core/utils/logger.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import '../../modules/chat/models/message_model.dart'; -import 'chat_history_service.dart'; -import 'speech_impl/azure_asr_service.dart'; -import 'speech_impl/azure_tts_service.dart'; -import 'asr_service.dart'; -import 'tts_service.dart'; +import '../models/events/voice_interaction_event.dart'; -/// 语音交互服务 +/// 语音交互服务抽象基类 /// -/// 该服务提供了与Android端的VoiceInteractionService.kt通信的接口, -/// 用于管理后台语音交互服务的生命周期和接收语音交互事件 -class VoiceInteractionService extends GetxService { - static VoiceInteractionService get to => Get.find(); - // 方法通道和事件通道 - static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction'); - static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events'); - - // 服务状态 - final _isServiceRunning = false.obs; - bool get isServiceRunning => _isServiceRunning.value; - - // 事件流控制器 - StreamController? _eventStreamController; - Stream? _eventStream; - Stream? get eventStream => _eventStream; - - // 事件通道状态 - bool _isEventChannelReady = false; - Completer? _eventChannelReadyCompleter; - StreamSubscription? _eventSubscription; - - // 配置信息 - late String _azureSpeechKey; - late String _azureSpeechRegion; - late String _volcanoAiApiKey; - - // 初始化状态标志 - static bool _isInitialized = false; - - /// 构造函数 - VoiceInteractionService() { - - } - - /// 从环境变量加载配置 - void _loadConfig() { - _azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; - _azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; - _volcanoAiApiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? ''; - - if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) { - Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); - } - - if (_volcanoAiApiKey.isEmpty) { - Logger.warning('未找到火山 AI API 密钥。请在 .env 文件中设置 VOLCANO_AI_API_KEY'); - } - } - - /// 设置事件通道 - void _setupEventChannel() { - _eventChannelReadyCompleter = Completer(); - - _eventSubscription = _eventChannel - .receiveBroadcastStream() - .listen((event) { - if (event is Map) { - final String eventType = event['type'] as String? ?? ''; - - // 处理通道准备好的事件 - if (eventType == 'channelReady') { - _isEventChannelReady = true; - if (!_eventChannelReadyCompleter!.isCompleted) { - _eventChannelReadyCompleter!.complete(); - } - return; - } - - _handleVoiceInteractionEvent(event); - } - }, onError: (error) { - Logger.error('语音交互事件通道错误: $error'); - }); - } - - /// 创建事件流 - void _createEventStream() { - _eventStreamController = StreamController.broadcast(); - _eventStream = _eventStreamController?.stream; - } - - /// 等待事件通道准备好 - Future _waitForEventChannel({Duration timeout = const Duration(seconds: 5)}) async { - if (_isEventChannelReady) return true; - - try { - await _eventChannelReadyCompleter!.future.timeout(timeout); - return true; - } on TimeoutException { - Logger.error('等待语音交互事件通道准备好超时'); - return false; - } - } +/// 定义了iOS和Android平台共用的语音交互服务接口 +abstract class VoiceInteractionService { + /// 事件流,用于发布语音交互相关事件 + Stream get eventStream; /// 初始化服务 - Future initialize() async { - return this; - try { - if (_isInitialized) { - return this; - } - _isInitialized = true; + /// + /// 返回服务实例自身,以支持链式调用 + Future initialize(); + + /// 暂停语音交互 + /// + /// 停止正在进行的语音识别和TTS播放 + void pauseVoiceInteraction(); - _loadConfig(); - _setupEventChannel(); - _createEventStream(); - // 检查服务是否正在运行 - await _checkServiceStatus(); - - // 等待事件通道准备好 - await _waitForEventChannel(); - - Logger.info('语音交互服务初始化完成'); - return this; - } catch (e) { - Logger.error('语音交互服务初始化失败: $e'); - return this; - } - } - - /// 检查服务状态 - Future _checkServiceStatus() async { - try { - final bool isRunning = await _channel.invokeMethod('isVoiceInteractionServiceRunning') ?? false; - _isServiceRunning.value = isRunning; - Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}'); - } catch (e) { - Logger.error('检查语音交互服务状态失败: $e'); - _isServiceRunning.value = false; - } - } - - /// 启动语音交互服务 - Future startService() async { - return true; - if (_isServiceRunning.value) { - Logger.info('语音交互服务已经在运行'); - return true; - } - - try { - final bool result = await _channel.invokeMethod('startVoiceInteractionService', { - 'azure_speech_key': _azureSpeechKey, - 'azure_speech_region': _azureSpeechRegion, - 'volcano_ai_api_key': _volcanoAiApiKey, - }) ?? false; - - if (result) { - _isServiceRunning.value = true; - Logger.info('语音交互服务启动成功'); - } else { - Logger.error('语音交互服务启动失败'); - } - - return result; - } catch (e) { - Logger.error('启动语音交互服务失败: $e'); - return false; - } - } - - /// 停止语音交互服务 - Future stopService() async { - if (!_isServiceRunning.value) { - Logger.info('语音交互服务未运行'); - return true; - } - - try { - final bool result = await _channel.invokeMethod('stopVoiceInteractionService') ?? false; - - if (result) { - _isServiceRunning.value = false; - Logger.info('语音交互服务停止成功'); - } else { - Logger.error('语音交互服务停止失败'); - } - - return result; - } catch (e) { - Logger.error('停止语音交互服务失败: $e'); - return false; - } - } - - /// 暂停语音交互(停止TTS和语音识别,但保持服务运行) - Future pauseVoiceInteraction() async { - return true; - if (!_isServiceRunning.value) { - Logger.info('语音交互服务未运行,无法暂停'); - return false; - } - - try { - final bool result = await _channel.invokeMethod('pauseVoiceInteraction') ?? false; - - if (result) { - Logger.info('语音交互暂停成功'); - } else { - Logger.error('语音交互暂停失败'); - } - - return result; - } catch (e) { - Logger.error('暂停语音交互失败: $e'); - return false; - } - } + /// 唤醒语音交互 + void onWakeup(); - /// 处理来自原生端的语音交互事件 - void _handleVoiceInteractionEvent(dynamic event) { - if (event is! Map || _eventStreamController == null) return; - - final Map eventMap = event; - final String eventType = eventMap['type'] as String? ?? ''; - final int timestamp = eventMap['timestamp'] as int? ?? 0; - - // 添加时间戳日志,帮助调试 - Logger.info('收到原生端事件: $eventType, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}'); - - switch (eventType) { - case 'chatHistory': - final String agentId = eventMap['agentId'] as String? ?? ''; - final String userMessage = eventMap['userMessage'] as String? ?? ''; - final String assistantMessage = eventMap['assistantMessage'] as String? ?? ''; - - _eventStreamController?.add(ChatHistoryEvent( - agentId: agentId, - userMessage: userMessage, - assistantMessage: assistantMessage, - timestamp: timestamp, - )); - - // 保存聊天记录 - _saveChatHistory(agentId, userMessage, assistantMessage, timestamp); - - Logger.info('收到聊天历史事件: agentId=$agentId'); - break; - - case 'recognitionStarted': - _eventStreamController?.add(RecognitionStartedEvent( - timestamp: timestamp, - )); - break; - - default: - Logger.warning('收到未知类型的语音交互事件: $eventType'); - break; - } - } + /// 是否处于活跃状态 + bool isActive(); - @override - void onClose() { - // 清理资源 - _eventSubscription?.cancel(); - _eventStreamController?.close(); - super.onClose(); - } + /// 获取当前对话历史 + List> getMessageHistory(); - /// 保存聊天记录 - void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) { - try { - // 获取ChatHistoryService实例 - final chatHistoryService = Get.find(); - - // 创建用户消息和助手消息 - final userMsg = Message( - role: 'user', - content: userMessage, - timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp), - ); - - final assistantMsg = Message( - role: 'assistant', - content: assistantMessage, - timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息 - ); - - // 加载现有历史记录 - final existingMessages = chatHistoryService.loadHistory(agentId); - - // 添加新消息 - existingMessages.addAll([userMsg, assistantMsg]); - - // 保存更新后的历史记录 - chatHistoryService.saveHistory(agentId, existingMessages); - } catch (e) { - Logger.error('保存聊天记录失败: $e'); - } - } -} - -/// 语音交互事件基类 -abstract class VoiceInteractionEvent { - final int timestamp; - - VoiceInteractionEvent({required this.timestamp}); -} - -/// 聊天历史事件 -class ChatHistoryEvent extends VoiceInteractionEvent { - final String agentId; - final String userMessage; - final String assistantMessage; - - ChatHistoryEvent({ - required this.agentId, - required this.userMessage, - required this.assistantMessage, - required int timestamp, - }) : super(timestamp: timestamp); -} - -/// 语音识别开始事件 -class RecognitionStartedEvent extends VoiceInteractionEvent { - RecognitionStartedEvent({ - required int timestamp, - }) : super(timestamp: timestamp); + /// 获取当前服务ID + String getServiceId(); } \ No newline at end of file diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index b9e75b7fa..6fa59a41f 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -2,14 +2,16 @@ import 'dart:async'; import 'package:get/get.dart'; import 'package:flutter/widgets.dart'; import '../models/message_model.dart'; +import '../../../core/utils/logger.dart'; import '../../../data/services/volcano_ai_service.dart'; import '../../../data/services/tts_service.dart'; import '../../../data/providers/agent_provider.dart'; import '../../../data/models/agent.dart'; -import '../../../core/utils/logger.dart'; +import '../../../data/services/asr_service.dart'; import '../../../data/services/chat_history_service.dart'; +import '../../../data/models/events/voice_interaction_event.dart'; import '../../../data/services/voice_interaction_service.dart'; -import '../../../data/services/asr_service.dart'; +import 'dart:io' show Platform; class ChatController extends GetxController { // 服务 @@ -129,87 +131,65 @@ class ChatController extends GetxController { /// 订阅VoiceInteractionService事件 void _subscribeToVoiceInteractionEvents() { if (_voiceInteractionService.eventStream != null) { - _voiceInteractionSubscription = _voiceInteractionService.eventStream!.listen((event) { + _voiceInteractionSubscription = _voiceInteractionService.eventStream.listen((event) { if (event is RecognitionStartedEvent) { // 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS - _handleRecognitionStartedEvent(); + _stopCurrentInteraction(); } else if (event is ChatHistoryEvent) { - // 收到chatHistory事件时,更新聊天界面 - _handleChatHistoryEvent(event); + // 收到聊天历史事件,添加到聊天记录 + _processChatHistoryEvent(event); } }); } } - /// 处理recognitionStarted事件 - void _handleRecognitionStartedEvent() { - Logger.info('收到后台语音识别开始事件,停止当前AI交互'); - - // 停止当前AI交互 - if (isSending.value || _aiResponseSubscription != null) { - _interruptCurrentResponse("收到后台语音识别开始事件,中断当前响应"); - } - - // 停止TTS - _ttsService.stop(); - - // 停止语音识别 - if (isVoiceRecognitionActive.value) { - stopVoiceRecognition(); - } - } - - /// 处理chatHistory事件 - void _handleChatHistoryEvent(ChatHistoryEvent event) { + /// 处理聊天历史事件 + void _processChatHistoryEvent(ChatHistoryEvent event) { Logger.info('收到后台聊天历史事件: agentId=${event.agentId}'); - // 检查是否是当前Agent的消息 - if (event.agentId != agent.id) { - Logger.info('收到的消息不属于当前Agent,忽略'); + // 如果消息不是空的且不是当前Agent的,不处理 + if (event.userMessage.isEmpty || event.agentId != agent.id) { return; } - // 创建用户消息和助手消息 + // 添加用户消息 final userMsg = Message( role: 'user', content: event.userMessage, timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp), ); + // 添加助手消息 final assistantMsg = Message( role: 'assistant', content: event.assistantMessage, - timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp + 1), // 确保助手消息时间戳晚于用户消息 - ); - - // 检查是否已存在相同内容的消息(避免重复添加) - final bool userMsgExists = messages.any((m) => - m.role == userMsg.role && - m.content == userMsg.content && - (m.timestamp.millisecondsSinceEpoch - userMsg.timestamp.millisecondsSinceEpoch).abs() < 5000 + timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp + 1), // 助手消息稍后一毫秒 ); - final bool assistantMsgExists = messages.any((m) => - m.role == assistantMsg.role && - m.content == assistantMsg.content && - (m.timestamp.millisecondsSinceEpoch - assistantMsg.timestamp.millisecondsSinceEpoch).abs() < 5000 - ); - - // 如果消息不存在,则添加到聊天记录中 - if (!userMsgExists) { + // 防止消息重复 + if (!_isDuplicateMessage(userMsg) && !_isDuplicateMessage(assistantMsg)) { messages.add(userMsg); + messages.add(assistantMsg); } + _scrollToBottom(); + } + + /// 停止当前交互 + void _stopCurrentInteraction() { + Logger.info('收到后台语音识别开始事件,停止当前AI交互'); - if (!assistantMsgExists) { - messages.add(assistantMsg); + // 停止当前AI交互 + if (isSending.value || _aiResponseSubscription != null) { + _interruptCurrentResponse("收到后台语音识别开始事件,中断当前响应"); } - // 如果有新消息添加,刷新列表并滚动到底部 - if (!userMsgExists || !assistantMsgExists) { - messages.refresh(); + // 停止TTS + _ttsService.stop(); + + // 停止语音识别 + if (isVoiceRecognitionActive.value) { + stopVoiceRecognition(); } - _scrollToBottom(); - } @override @@ -698,4 +678,13 @@ class ChatController extends GetxController { Logger.info('${timeoutSeconds}秒无活动,自动停止语音识别'); stopVoiceRecognition(); } + + /// 检查是否为重复消息 + bool _isDuplicateMessage(Message message) { + return messages.any((m) => + m.role == message.role && + m.content == message.content && + (m.timestamp.millisecondsSinceEpoch - message.timestamp.millisecondsSinceEpoch).abs() < 5000 + ); + } } diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index cfe397256..55fa2264f 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -1,3 +1,4 @@ +import 'package:deep_voice/data/services/speech_impl/azure_tts_service.dart'; import 'package:get/get.dart'; import 'dart:async'; import '../../../routes/app_routes.dart'; @@ -6,7 +7,7 @@ import '../../../data/services/bluetooth_media_button_service.dart'; import '../../../core/utils/logger.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide BluetoothDevice; import 'package:flutter/material.dart'; -import '../../../data/services/voice_interaction_service.dart'; +import '../../../data/services/voice_interaction_impl/android_voice_interaction_service.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../../data/providers/agent_provider.dart'; // 连接状态枚举 @@ -331,12 +332,9 @@ class HomeController extends GetxController { // 根据按钮类型执行相应操作 switch (event.buttonType) { case MediaButtonType.playPause: - // 跳转到chat页面,使用个人助手模式并开启语音交互 - // Get.toNamed('/chat', arguments: { - // 'isVoiceInteraction': true, - // 'agent': 'personal_assistant' - // }); - Logger.info('通过蓝牙按钮启动个人助手语音交互'); + + // AzureTtsService.to.speakOnce('我在呢'); + break; default: diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 437beaef4..c6d3efe17 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -4,19 +4,21 @@ import 'dart:async'; import '../../../routes/app_routes.dart'; import '../../../data/services/classic_bluetooth_service.dart'; import '../../../core/utils/logger.dart'; -import 'package:flutter/material.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:app_settings/app_settings.dart'; -import 'package:flutter/services.dart'; import 'dart:io'; +import 'package:app_settings/app_settings.dart'; /// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程 class PairingController extends GetxController { static PairingController get to => Get.find(); - + + // 经典蓝牙服务 - late ClassicBluetoothService _bluetoothService; + final ClassicBluetoothService _bluetoothService = Get.find(); + + // 语音交互服务 + final VoiceInteractionService _voiceInteractionService = Get.find(); + // 是否开始搜索 final _isSearching = false.obs; @@ -111,14 +113,11 @@ class PairingController extends GetxController { Future _initialize() async { try { // 初始化语音交互服务 - VoiceInteractionService.to.initialize(); - VoiceInteractionService.to.startService(); + _voiceInteractionService.initialize(); isLoading.value = true; statusDescription.value = '正在初始化...'; - // 获取蓝牙服务 - _bluetoothService = Get.find(); // 确保蓝牙服务已初始化 if (!await _bluetoothService.initialize()) { diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index fd93019bf..df5cfaa14 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -7,6 +7,7 @@ import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/tts_service.dart'; import '../../../data/services/language_manager.dart'; import '../../../core/utils/logger.dart'; +import '../../../data/models/events/voice_interaction_event.dart'; import '../../../data/services/voice_interaction_service.dart'; import '../../../data/services/asr_service.dart'; class TranslationController extends GetxController { @@ -70,8 +71,8 @@ class TranslationController extends GetxController { targetLanguageCode.value = targetAsrCode; } - // 订阅VoiceInteractionService事件 - _subscribeToVoiceInteractionEvents(); + // 订阅原生服务事件 + _subscribeToVoiceInteractionService(); // 加载历史记录 loadTranslationHistory(); @@ -90,14 +91,13 @@ class TranslationController extends GetxController { }); } - /// 订阅VoiceInteractionService事件 - void _subscribeToVoiceInteractionEvents() { + /// 订阅原生服务事件 + void _subscribeToVoiceInteractionService() { if (_voiceInteractionService.eventStream != null) { _voiceInteractionSubscription = _voiceInteractionService.eventStream!.listen((event) { if (event is RecognitionStartedEvent) { - // 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS - stopAll(); - } + _handleVoiceInteractionStarted(); + } }); } } @@ -578,6 +578,12 @@ class TranslationController extends GetxController { void scrollToBottom({bool animate = true}) { _scrollToBottom(); } + + /// 处理原生语音交互开始事件 + void _handleVoiceInteractionStarted() { + // 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS + stopAll(); + } } // 翻译项模型 diff --git a/macos/Podfile.lock b/macos/Podfile.lock index da6276a5d..931bd270a 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - app_settings (5.1.1): + - FlutterMacOS - audio_service (0.0.1): - Flutter - FlutterMacOS @@ -26,13 +28,19 @@ PODS: - FlutterMacOS - share_plus (0.0.1): - FlutterMacOS + - speech_to_text (0.0.1): + - Flutter + - FlutterMacOS + - Try - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS + - Try (2.1.1) - window_size (0.0.2): - FlutterMacOS DEPENDENCIES: + - app_settings (from `Flutter/ephemeral/.symlinks/plugins/app_settings/macos`) - audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/darwin`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) @@ -45,10 +53,17 @@ DEPENDENCIES: - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) + - speech_to_text (from `Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) +SPEC REPOS: + trunk: + - Try + EXTERNAL SOURCES: + app_settings: + :path: Flutter/ephemeral/.symlinks/plugins/app_settings/macos audio_service: :path: Flutter/ephemeral/.symlinks/plugins/audio_service/darwin audio_session: @@ -73,12 +88,15 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/record_darwin/macos share_plus: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos + speech_to_text: + :path: Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin sqflite_darwin: :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin window_size: :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos SPEC CHECKSUMS: + app_settings: cd21e176b56f8172043640ade81322a98896bff4 audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 @@ -91,7 +109,9 @@ SPEC CHECKSUMS: path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96 window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9 PODFILE CHECKSUM: 8c4442624fc136b0afb378a2502958e01c05d073