Browse Source

v001

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
fae8064ef6
  1. 4
      ios/Runner.xcodeproj/project.pbxproj
  2. 191
      ios/Runner/AudioSessionManager.swift
  3. 57
      ios/Runner/AzureAsrHelper.swift
  4. 42
      ios/Runner/AzureTtsHelper.swift
  5. 70
      ios/Runner/BluetoothMediaButtonHelper.swift
  6. 18
      ios/Runner/ClassicBluetoothHelper.swift
  7. 16
      lib/core/bindings/initial_binding.dart
  8. 29
      lib/data/models/events/voice_interaction_event.dart
  9. 58
      lib/data/models/message_model.dart
  10. 15
      lib/data/services/bluetooth_media_button_service.dart
  11. 341
      lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart
  12. 468
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  13. 345
      lib/data/services/voice_interaction_service.dart
  14. 95
      lib/modules/chat/controllers/chat_controller.dart
  15. 12
      lib/modules/home/controllers/home_controller.dart
  16. 19
      lib/modules/pairing/controllers/pairing_controller.dart
  17. 20
      lib/modules/translation/controllers/translation_controller.dart
  18. 20
      macos/Podfile.lock

4
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 = "<group>"; };
0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureAsrHelper.swift; sourceTree = "<group>"; };
0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureTtsHelper.swift; sourceTree = "<group>"; };
0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionManager.swift; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
@ -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;

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

57
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] 资源已释放")
}
/// 从结果中获取检测到的语言

42
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: - 私有辅助方法

70
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 {

18
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 {}
}

16
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>(() => VolcanoAIService(), fenix: true);
// 注意:VoiceInteractionService 在 HomeController 中初始化
Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(), fenix: true);
// 经典蓝牙服务
Get.lazyPut<ClassicBluetoothService>(() => ClassicBluetoothService(), fenix: true);
// 蓝牙媒体按钮服务
Get.lazyPut<BluetoothMediaButtonService>(() => BluetoothMediaButtonService(), fenix: true);
// 根据平台注册对应的VoiceInteractionService实现
if (Platform.isIOS) {
Get.lazyPut<VoiceInteractionService>(() => IosVoiceInteractionService(), fenix: true);
} else {
Get.lazyPut<VoiceInteractionService>(() => AndroidVoiceInteractionService(), fenix: true);
}
}
}

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

58
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<String, dynamic> 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<String, dynamic> 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,
);
}
}

15
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<dynamic, dynamic> 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<VoiceInteractionService>().onWakeup();
} catch (e) {
Logger.error('唤醒语音交互服务失败: $e');
}
// 广播事件
_buttonEventController.add(buttonEvent);
// 广播事件
_buttonEventController.add(buttonEvent);

341
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<VoiceInteractionEvent>? _eventStreamController;
Stream<VoiceInteractionEvent>? _eventStream;
@override
Stream<VoiceInteractionEvent> get eventStream => _eventStream ?? Stream.empty();
// 事件通道状态
StreamSubscription? _eventSubscription;
// 配置信息
late String _azureSpeechKey;
late String _azureSpeechRegion;
late String _volcanoAiApiKey;
// 初始化状态标志
static bool _isInitialized = false;
// 当前会话历史
final List<Map<String, String>> _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<VoiceInteractionEvent>.broadcast();
_eventStream = _eventStreamController?.stream;
}
/// 初始化服务
@override
Future<VoiceInteractionService> 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<void> _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<bool> _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<bool> _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<bool> 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<dynamic, dynamic> 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<ChatHistoryService>();
// 创建用户消息和助手消息
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<Map<String, String>> getMessageHistory() {
return List<Map<String, String>>.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);
}

468
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<AsrService>();
final TtsService _ttsService = Get.find<TtsService>();
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final ChatHistoryService _chatHistoryService = Get.find<ChatHistoryService>();
// 当前状态
final Rx<VoiceInteractionState> state = VoiceInteractionState.idle.obs;
// 是否识别到用户语音
final RxBool isSpeechDetected = false.obs;
// 是否正在处理AI响应
final RxBool isProcessingAI = false.obs;
// 是否正在播放TTS
final RxBool isSpeaking = false.obs;
// 对话历史记录
final List<Map<String, String>> _messageHistory = [];
// 最大历史记录数
static const int _maxHistorySize = 10;
// 超时计时器
Timer? _inactivityTimer;
// 流订阅
StreamSubscription? _recognitionSubscription;
// 当前识别的文本
String _currentRecognizedText = '';
// 事件流控制器
final StreamController<VoiceInteractionEvent> _eventStreamController =
StreamController<VoiceInteractionEvent>.broadcast();
// 获取事件流
@override
Stream<VoiceInteractionEvent> get eventStream => _eventStreamController.stream;
// 标识符,用于存储聊天历史
final String _agentId = 'ios_assistant';
/// 初始化服务
@override
Future<VoiceInteractionService> 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<void> _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<void> _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<Map<String, String>> _buildMessageHistory() {
return List<Map<String, String>>.from(_messageHistory);
}
// 添加消息到历史记录
void _addToHistory(Map<String, String> 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<Map<String, String>> getMessageHistory() {
return List<Map<String, String>>.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('已发送识别开始事件');
}
}

345
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<VoiceInteractionEvent>? _eventStreamController;
Stream<VoiceInteractionEvent>? _eventStream;
Stream<VoiceInteractionEvent>? get eventStream => _eventStream;
// 事件通道状态
bool _isEventChannelReady = false;
Completer<void>? _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<void>();
_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<VoiceInteractionEvent>.broadcast();
_eventStream = _eventStreamController?.stream;
}
/// 等待事件通道准备好
Future<bool> _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<VoiceInteractionEvent> get eventStream;
/// 初始化服务
Future<VoiceInteractionService> initialize() async {
return this;
try {
if (_isInitialized) {
return this;
}
_isInitialized = true;
///
/// 返回服务实例自身,以支持链式调用
Future<VoiceInteractionService> initialize();
/// 暂停语音交互
///
/// 停止正在进行的语音识别和TTS播放
void pauseVoiceInteraction();
_loadConfig();
_setupEventChannel();
_createEventStream();
// 检查服务是否正在运行
await _checkServiceStatus();
// 等待事件通道准备好
await _waitForEventChannel();
Logger.info('语音交互服务初始化完成');
return this;
} catch (e) {
Logger.error('语音交互服务初始化失败: $e');
return this;
}
}
/// 检查服务状态
Future<void> _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<bool> 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<bool> 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<bool> 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<dynamic, dynamic> 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<Map<String, String>> getMessageHistory();
/// 保存聊天记录
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) {
try {
// 获取ChatHistoryService实例
final chatHistoryService = Get.find<ChatHistoryService>();
// 创建用户消息和助手消息
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();
}

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

12
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:

19
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<ClassicBluetoothService>();
// 语音交互服务
final VoiceInteractionService _voiceInteractionService = Get.find<VoiceInteractionService>();
// 是否开始搜索
final _isSearching = false.obs;
@ -111,14 +113,11 @@ class PairingController extends GetxController {
Future<void> _initialize() async {
try {
// 初始化语音交互服务
VoiceInteractionService.to.initialize();
VoiceInteractionService.to.startService();
_voiceInteractionService.initialize();
isLoading.value = true;
statusDescription.value = '正在初始化...';
// 获取蓝牙服务
_bluetoothService = Get.find<ClassicBluetoothService>();
// 确保蓝牙服务已初始化
if (!await _bluetoothService.initialize()) {

20
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();
}
}
// 翻译项模型

20
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

Loading…
Cancel
Save