You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
514 lines
15 KiB
514 lines
15 KiB
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 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)
|
|
|
|
// 播放静音音频,确保音频会话活跃
|
|
// startSilencePlayback()
|
|
|
|
// 启动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
|
|
}
|
|
|
|
/**
|
|
* 初始化服务
|
|
*/
|
|
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()
|
|
|
|
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()
|
|
|
|
// 播放提示音
|
|
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)
|
|
|
|
// 播放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)
|
|
}
|
|
}
|