import Foundation import UIKit import AVFoundation /** * 语音交互服务,iOS原生实现 * * 负责: * 1) 监听蓝牙耳机按键 * 2) 处理语音识别 * 3) 与火山AI服务交互 * 4) 文本转语音播放 */ @available(iOS 13.0, *) class VoiceInteractionService: NSObject { // 常量 private let TAG = "VoiceInteractionService" private let RECOGNITION_TIMEOUT: TimeInterval = 8.0 // 单例 static let shared = VoiceInteractionService() // 服务状态 private var isRunning = false private var isActive = false private var isRecognitionActive = false private var isTimeoutPaused = false private var hasSpeechDetected = false private var isTtsSpeaking = false // 事件处理器 private weak var eventHandler: VoiceInteractionEventHandler? // 按键处理 private var lastKeyEventTime: TimeInterval = 0 private var keyEventCount = 0 // 活动时间 private var lastActivityTime: TimeInterval = 0 // 当前用户输入 private var currentUserInput = "" private var lastUserMessage = "" private var lastAssistantMessage = "" // 服务组件 private var azureAsrHelper: AzureAsrHelper? private var azureTtsHelper: AzureTtsHelper? private var volcanoAIService: VolcanoAIService? private var silenceTimer: Timer? // 系统提示词 private let systemPrompt = """ 你是一个智能语音助手,能够简洁明了地回答用户的问题。 请保持回答简短、准确,避免过长的解释。 如果用户的问题不清楚,请礼貌地请求澄清。 不要使用复杂的术语,除非用户明确要求。 用户用语音和你交互。 """ // 初始化方法 private override init() { super.init() NSLog("%@: VoiceInteractionService 初始化中", TAG) } /** * 启动服务 * * @param apiKey Azure语音服务API密钥 * @param region Azure语音服务区域 * @param volcanoKey 火山AI服务API密钥 * @return 启动是否成功 */ func start(azureKey: String, azureRegion: String, volcanoKey: String) -> Bool { guard !isRunning else { NSLog("%@: 服务已经在运行", TAG) return true } NSLog("%@: 启动服务中", TAG) // 重置状态 resetState() // 初始化各个服务组件 initServices(azureKey: azureKey, azureRegion: azureRegion, volcanoKey: volcanoKey) // 启动monitorService定时器 silenceTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(monitorService), userInfo: nil, repeats: true) NSLog("%@: monitorService定时器已启动", TAG) // 标记服务为运行状态 isRunning = true NSLog("%@: 服务启动完成,等待蓝牙按键事件", TAG) return true } /** * 停止服务 */ func stop() { guard isRunning else { NSLog("%@: 服务未运行", TAG) return } NSLog("%@: 停止服务中", TAG) // 停止monitorService定时器 silenceTimer?.invalidate() silenceTimer = nil // 停止语音识别 if isRecognitionActive { stopVoiceRecognition() } // 停止TTS stopCurrentTTS() // 释放服务实例 azureAsrHelper?.dispose() azureTtsHelper?.dispose() // 更新状态 resetState() isRunning = false NSLog("%@: 服务已停止", TAG) } /** * 获取服务运行状态 */ func isServiceRunning() -> Bool { return isRunning } /** * 重置状态 */ private func resetState() { isActive = false isRecognitionActive = false isTimeoutPaused = false hasSpeechDetected = false isTtsSpeaking = false lastUserMessage = "" lastAssistantMessage = "" } /** * 初始化服务 */ private func initServices(azureKey: String, azureRegion: String, volcanoKey: String) { NSLog("%@: 初始化服务组件", TAG) // 初始化Azure ASR azureAsrHelper = AzureAsrHelper { [weak self] eventName, eventData in // 处理ASR事件 self?.handleAsrEvent(eventName: eventName, eventData: eventData) } // 初始化Azure TTS azureTtsHelper = AzureTtsHelper { [weak self] eventName, eventData in // 处理TTS事件 self?.handleTtsEvent(eventName: eventName, eventData: eventData) } // 初始化火山AI服务 volcanoAIService = VolcanoAIService() // 配置Azure ASR if !azureKey.isEmpty && !azureRegion.isEmpty { azureAsrHelper?.initialize(speechSubscriptionKey: azureKey, serviceRegion: azureRegion, supportedLanguages: ["zh-CN"]) NSLog("%@: Azure ASR初始化完成", TAG) } else { NSLog("%@: Azure配置信息不完整,无法初始化Azure ASR", TAG) } // 配置Azure TTS if !azureKey.isEmpty && !azureRegion.isEmpty { azureTtsHelper?.initialize(speechSubscriptionKey: azureKey, serviceRegion: azureRegion, language: "zh-CN") NSLog("%@: Azure TTS初始化完成", TAG) } else { NSLog("%@: Azure配置信息不完整,无法初始化Azure TTS", TAG) } // 配置火山AI if !volcanoKey.isEmpty { volcanoAIService?.initialize(apiKey: volcanoKey) NSLog("%@: 火山AI服务初始化完成", TAG) } else { NSLog("%@: 火山AI配置信息不完整,无法初始化火山AI服务", TAG) } } /** * 监控服务状态 */ @objc private func monitorService() { // 确保服务保持活跃状态 if !isActive { isActive = true } // 检查语音识别状态 if isRecognitionActive { let currentTime = Date().timeIntervalSince1970 let elapsedTime = currentTime - lastActivityTime // NSLog("%@: 已过去时间: %@, 是否检测到语音: %@, 是否TTS播放: %@", TAG, String(elapsedTime), String(hasSpeechDetected), String(isTtsSpeaking), String(elapsedTime)) // 如果超过超时时间没有检测到语音,且不在TTS播放中,暂停语音识别 if !hasSpeechDetected && !isTtsSpeaking && elapsedTime >= RECOGNITION_TIMEOUT { // NSLog("%@: 超过%@秒未检测到语音,停止识别", TAG, String(RECOGNITION_TIMEOUT)) isTimeoutPaused = true playNotification("没有听到您说话,已暂停对话。双击耳机按钮可重新开始。") // 停止语音识别并确保资源完全释放 stopVoiceRecognition() // 清理识别状态 isRecognitionActive = false hasSpeechDetected = false } } } /** * 处理ASR事件 */ private func handleAsrEvent(eventName: String, eventData: [String: Any]) { // NSLog("%@: 处理ASR事件: %@", TAG, eventName) switch eventName { case "recognizing": if let text = eventData["text"] as? String, !text.isEmpty { hasSpeechDetected = true stopCurrentTTS() updateLastActivityTime() } case "result": if let text = eventData["text"] as? String, !text.isEmpty { updateLastActivityTime() // 保存用户消息 lastUserMessage = text processWithVolcanoAI(text) } // 重置状态,继续识别 hasSpeechDetected = false case "sessionStarted": updateLastActivityTime() case "sessionStopped": isRecognitionActive = false case "canceled": isRecognitionActive = false case "error": isRecognitionActive = false playNotification("语音识别出错") default: break } } /** * 处理TTS事件 */ private func handleTtsEvent(eventName: String, eventData: [String: Any]) { // NSLog("%@: 处理TTS事件: %@", TAG, eventName) switch eventName { case "started": isTtsSpeaking = true case "completed": isTtsSpeaking = false updateLastActivityTime() case "canceled": isTtsSpeaking = false case "error": isTtsSpeaking = false NSLog("%@: TTS错误: %@", TAG, eventData["error"] as? String ?? "未知错误") default: break } } /** * 处理双击事件 */ func handleMediaButtonAction() { NSLog("%@: 处理媒体按钮事件", TAG) // 屏蔽短时间重复响应的问题 let currentTime = Date().timeIntervalSince1970 let timeDiff = currentTime - lastKeyEventTime if timeDiff < 0.5 { NSLog("%@: 短时间重复响应,忽略", TAG) return } lastKeyEventTime = currentTime // 停止当前TTS播放 stopCurrentTTS() // 检查应用是否在后台 if UIApplication.shared.applicationState == .background { NSLog("%@: 应用在后台,无法开始语音识别", TAG) playNotification("请打开应用后重试!") return } // 播放提示音 playPrompt("我在!") // 重置超时暂停标志 isTimeoutPaused = false // 启动或重置语音识别 if !isRecognitionActive { NSLog("%@: 语音识别未激活,开始启动", TAG) startVoiceRecognition() } else { NSLog("%@: 语音识别已激活,更新活动时间", TAG) updateLastActivityTime() hasSpeechDetected = false } } /** * 开始语音识别 */ private func startVoiceRecognition() { if isRecognitionActive { return } // 通知Flutter语音识别已启动 notifyVoiceRecognitionStarted() isActive = true isRecognitionActive = true hasSpeechDetected = false updateLastActivityTime() if let asrHelper = azureAsrHelper { let success = asrHelper.startContinuousRecognition() if !success { isRecognitionActive = false NSLog("%@: 启动语音识别失败", TAG) playNotification("启动语音识别失败") } } else { isRecognitionActive = false NSLog("%@: Azure ASR服务未初始化", TAG) playNotification("语音识别服务未初始化") } } /** * 停止语音识别 */ private func stopVoiceRecognition() { if !isRecognitionActive { return } NSLog("%@: 停止语音识别", TAG) if let asrHelper = azureAsrHelper { let success = asrHelper.stopContinuousRecognition() if !success { NSLog("%@: 停止语音识别失败", TAG) } } isRecognitionActive = false hasSpeechDetected = false } /** * 使用火山AI处理语音识别结果 */ private func processWithVolcanoAI(_ text: String) { // 保存当前用户输入 currentUserInput = text // 在后台线程处理 DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self, let volcanoAIService = self.volcanoAIService else { return } do { // 创建消息 let message = volcanoAIService.createUserMessage(content: text) let messages = [message] // 发送请求 let response = try volcanoAIService.sendMessage(messages: messages, systemPrompt: self.systemPrompt) // 保存助手回复 self.lastAssistantMessage = response // 播放AI回复 self.speakAIResponse(response) // 同步聊天记录到Flutter端 self.notifyChatHistoryUpdated(userMessage: text, assistantMessage: response) } catch { NSLog("%@: AI处理出错: %@", self.TAG, error.localizedDescription) self.playNotification("AI处理出错") } } } /** * 播放AI回复 */ private func speakAIResponse(_ text: String) { isTtsSpeaking = true azureTtsHelper?.speakText(text: text) } /** * 播放提示音 */ private func playPrompt(_ message: String) { isTtsSpeaking = true azureTtsHelper?.speakText(text: message) } /** * 播放通知提示音 */ private func playNotification(_ message: String) { isTtsSpeaking = true azureTtsHelper?.speakText(text: message) } /** * 停止当前TTS播放 */ private func stopCurrentTTS() { if isTtsSpeaking { azureTtsHelper?.stopSpeaking() isTtsSpeaking = false } } /** * 更新最后活动时间 */ private func updateLastActivityTime() { lastActivityTime = Date().timeIntervalSince1970 } /** * 暂停语音交互 */ func pauseVoiceInteraction() { NSLog("%@: 暂停语音交互", TAG) // 停止当前语音播放 stopCurrentTTS() // 停止语音识别 if isRecognitionActive { stopVoiceRecognition() } // 设置状态为暂停,但服务保持运行 isTimeoutPaused = true } /** * 通知语音识别已开始 */ private func notifyVoiceRecognitionStarted() { // 发送识别开始事件 sendEvent(type: "recognition_started", data: [:]) } /** * 通知聊天历史更新 */ private func notifyChatHistoryUpdated(userMessage: String, assistantMessage: String) { // 发送聊天历史更新事件 sendEvent(type: "chat_history_updated", data: [ "agentId": "personal_assistant", "userMessage": userMessage, "assistantMessage": assistantMessage ]) } /** * 设置事件处理器 */ func setEventHandler(_ handler: VoiceInteractionEventHandler) { self.eventHandler = handler } /** * 发送事件到Flutter端 */ private func sendEvent(type: String, data: [String: Any] = [:]) { var eventData = data eventData["type"] = type eventData["timestamp"] = Int(Date().timeIntervalSince1970 * 1000) // 使用事件处理器发送事件 eventHandler?.sendEvent(eventData) } }