wolfplus 2 years ago
parent
commit
02cc915997
  1. 150
      ios/Runner/AppDelegate.swift
  2. 41
      ios/Runner/AudioSessionManager.swift
  3. 95
      ios/Runner/AzureAsrHelper.swift
  4. 85
      ios/Runner/AzureTtsHelper.swift
  5. 62
      ios/Runner/VoiceInteractionService.swift
  6. 2
      lib/data/services/speech_impl/azure_asr_service.dart
  7. 20
      macos/Podfile.lock

150
ios/Runner/AppDelegate.swift

@ -188,10 +188,7 @@ import AVFoundation
)
// 初始化Azure ASR辅助类
azureAsrHelper = AzureAsrHelper { [weak self] eventName, eventData in
// 推送事件到Flutter
self?.sendAsrEvent(eventName: eventName, eventData: eventData)
}
azureAsrHelper = AzureAsrHelper()
// 设置ASR方法处理器
azureAsrChannel.setMethodCallHandler { [weak self] (call, result) in
@ -246,7 +243,7 @@ import AVFoundation
)
// 创建ASR流处理器
let asrStreamHandler = AsrStreamHandler()
let asrStreamHandler = AsrStreamHandler(azureAsrHelper: azureAsrHelper as! AzureAsrHelper)
azureAsrStreamChannel.setStreamHandler(asrStreamHandler)
}
@ -260,10 +257,7 @@ import AVFoundation
)
// 初始化Azure TTS辅助类
azureTtsHelper = AzureTtsHelper { [weak self] eventName, eventData in
// 推送事件到Flutter
self?.sendTtsEvent(eventName: eventName, eventData: eventData)
}
azureTtsHelper = AzureTtsHelper()
// 设置TTS方法处理器
azureTtsChannel.setMethodCallHandler { [weak self] (call, result) in
@ -343,7 +337,7 @@ import AVFoundation
)
// 创建TTS流处理器
let ttsStreamHandler = TtsStreamHandler()
let ttsStreamHandler = TtsStreamHandler(azureTtsHelper: azureTtsHelper as! AzureTtsHelper)
azureTtsStreamChannel.setStreamHandler(ttsStreamHandler)
}
@ -487,44 +481,6 @@ import AVFoundation
let voiceInteractionStreamHandler = VoiceInteractionStreamHandler(voiceInteractionService: voiceInteractionService!)
voiceInteractionStreamChannel.setStreamHandler(voiceInteractionStreamHandler)
}
// 存储ASR事件
private var _cachedAsrEvent: [String: Any]?
// 发送ASR事件
@available(iOS 13.0, *)
private func sendAsrEvent(eventName: String, eventData: [String: Any]) {
// 调整格式:将event改为type,并将data的内容提升到顶层
var eventMap = eventData
eventMap["type"] = eventName
_cachedAsrEvent = eventMap
NotificationCenter.default.post(
name: Notification.Name("AzureAsrEvent"),
object: nil,
userInfo: eventMap
)
}
// 存储TTS事件
private var _cachedTtsEvent: [String: Any]?
// 发送TTS事件
@available(iOS 13.0, *)
private func sendTtsEvent(eventName: String, eventData: [String: Any]) {
// 调整格式:将event改为type,并将data的内容提升到顶层
var eventMap = eventData
eventMap["type"] = eventName
_cachedTtsEvent = eventMap
NotificationCenter.default.post(
name: Notification.Name("AzureTtsEvent"),
object: nil,
userInfo: eventMap
)
}
}
// 蓝牙事件流处理器
@ -596,91 +552,89 @@ class MediaButtonStreamHandler: NSObject, FlutterStreamHandler, MediaButtonEvent
}
}
// ASR事件处理器协议
@available(iOS 13.0, *)
protocol AsrEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
// ASR事件流处理器
@available(iOS 13.0, *)
class AsrStreamHandler: NSObject, FlutterStreamHandler {
class AsrStreamHandler: NSObject, FlutterStreamHandler, AsrEventHandler {
private let azureAsrHelper: AzureAsrHelper
private var eventSink: FlutterEventSink?
private var notificationObserver: Any?
deinit {
if let observer = notificationObserver {
NotificationCenter.default.removeObserver(observer)
init(azureAsrHelper: AzureAsrHelper) {
self.azureAsrHelper = azureAsrHelper
super.init()
// 设置事件处理器
azureAsrHelper.setEventHandler { [weak self] event in
self?.sendEvent(event)
}
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("AsrStreamHandler: 开始监听ASR事件")
self.eventSink = events
// 注册通知观察者
notificationObserver = NotificationCenter.default.addObserver(
forName: Notification.Name("AzureAsrEvent"),
object: nil,
queue: .main) { [weak self] notification in
if let userInfo = notification.userInfo as? [String: Any], let eventSink = self?.eventSink {
// 确保在主线程上发送事件
DispatchQueue.main.async {
eventSink(userInfo)
}
}
}
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("AsrStreamHandler: 取消监听ASR事件")
eventSink = nil
// 移除通知观察者
if let observer = notificationObserver {
NotificationCenter.default.removeObserver(observer)
notificationObserver = nil
return nil
}
return nil
// 实现AsrEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("AsrStreamHandler: 收到事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}
// TTS事件处理器协议
@available(iOS 13.0, *)
protocol TtsEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
// TTS事件流处理器
@available(iOS 13.0, *)
class TtsStreamHandler: NSObject, FlutterStreamHandler {
class TtsStreamHandler: NSObject, FlutterStreamHandler, TtsEventHandler {
private let azureTtsHelper: AzureTtsHelper
private var eventSink: FlutterEventSink?
private var notificationObserver: Any?
deinit {
if let observer = notificationObserver {
NotificationCenter.default.removeObserver(observer)
init(azureTtsHelper: AzureTtsHelper) {
self.azureTtsHelper = azureTtsHelper
super.init()
// 设置事件处理器
azureTtsHelper.setEventHandler { [weak self] event in
self?.sendEvent(event)
}
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("TtsStreamHandler: 开始监听TTS事件")
self.eventSink = events
// 注册通知观察者
notificationObserver = NotificationCenter.default.addObserver(
forName: Notification.Name("AzureTtsEvent"),
object: nil,
queue: .main) { [weak self] notification in
if let userInfo = notification.userInfo as? [String: Any], let eventSink = self?.eventSink {
// 确保在主线程上发送事件
DispatchQueue.main.async {
eventSink(userInfo)
}
}
}
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("TtsStreamHandler: 取消监听TTS事件")
eventSink = nil
// 移除通知观察者
if let observer = notificationObserver {
NotificationCenter.default.removeObserver(observer)
notificationObserver = nil
return nil
}
return nil
// 实现TtsEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("TtsStreamHandler: 收到事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}

41
ios/Runner/AudioSessionManager.swift

@ -419,40 +419,6 @@ import UIKit
// MARK: - 蓝牙媒体按钮配置
/// 配置用于获取蓝牙媒体按钮响应的音频会话
/// 此函数专门针对需要接收蓝牙耳机上媒体按钮事件的场景进行优化
/// - Returns: 配置是否成功
@objc func configureForBluetoothMediaButtons() -> Bool {
do {
// 配置音频会话以支持蓝牙媒体按钮
// 使用playback类别确保可以接收远程控制事件
// 使用spokenAudio模式优化语音交互
try configureSession(
category: .playback,
mode: .default, // 使用default模式,避免与spokenAudio可能的冲突
options: [
.allowBluetooth, // 允许通过蓝牙SCO连接进行输入和输出
.allowBluetoothA2DP, // 允许通过蓝牙A2DP连接进行高质量音频输出
.mixWithOthers // 允许与其他音频混合,避免中断
],
force: true // 强制重新配置,确保设置生效
)
NSLog("[AudioSessionManager] 已配置蓝牙媒体按钮响应的音频会话")
return true
} catch {
NSLog("[AudioSessionManager] 配置蓝牙媒体按钮响应的音频会话失败: \(error.localizedDescription)")
return false
}
}
// MARK: - Azure语音服务配置
/// 配置用于语音交互(同时支持ASR和TTS)的音频会话
/// 此函数综合优化语音识别和语音合成,适用于需要双向交互的场景
@ -466,11 +432,10 @@ import UIKit
category: .playAndRecord,
mode: .spokenAudio,
options: [
// .allowBluetooth, // 允许通过蓝牙SCO连接进行输入和输出
.allowBluetoothA2DP, // 允许通过蓝牙A2DP连接进行高质量音频输出
// .allowBluetooth, // 允许通过蓝牙SCO连接进行输入和输出
// .defaultToSpeaker, // 默认使用扬声器输出
// .duckOthers, // 降低其他应用的音量
// .interruptSpokenAudioAndMixWithOthers // 中断其他语音音频并混合
// .mixWithOthers
],
force: force // 强制重新配置,确保设置生效
)
@ -478,7 +443,7 @@ import UIKit
// 设置首选输入设备
// try configurePreferredInput()
// NSLog("[AudioSessionManager] 已配置语音交互(ASR+TTS)的音频会话")
NSLog("[AudioSessionManager] 已配置语音交互(ASR+TTS)的音频会话")
return true
} catch {
NSLog("[AudioSessionManager] 配置语音交互(ASR+TTS)的音频会话失败: \(error.localizedDescription)")

95
ios/Runner/AzureAsrHelper.swift

@ -8,7 +8,7 @@ class AzureAsrHelper: NSObject {
// MARK: - 属性
/// 事件处理回调
private var eventHandler: (String, [String: Any]) -> Void
private var eventHandler: (([String: Any]) -> Void)?
/// 语音配置信息
private var speechSubscriptionKey: String = ""
@ -33,8 +33,7 @@ class AzureAsrHelper: NSObject {
// MARK: - 初始化
init(eventHandler: @escaping (String, [String: Any]) -> Void) {
self.eventHandler = eventHandler
override init() {
super.init()
}
@ -42,6 +41,7 @@ class AzureAsrHelper: NSObject {
dispose()
}
// MARK: - ASR Service 接口实现
/// 初始化语音识别服务
@ -56,10 +56,15 @@ class AzureAsrHelper: NSObject {
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler("error", ["message": "Azure 配置信息不完整"])
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
// 检查语言参数
if let languages = supportedLanguages, languages.isEmpty {
print("[AzureAsrHelper] 警告: 传入的支持语言列表为空,将使用默认语言")
}
// 释放之前的资源
dispose()
@ -134,7 +139,7 @@ class AzureAsrHelper: NSObject {
return true
} catch {
print("[AzureAsrHelper] 错误: 创建识别器失败: \(error.localizedDescription)")
eventHandler("error", ["message": "创建识别器失败: \(error.localizedDescription)"])
eventHandler?(["type": "error", "message": "创建识别器失败: \(error.localizedDescription)"])
return false
}
}
@ -158,7 +163,7 @@ class AzureAsrHelper: NSObject {
if event.result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
print("[AzureAsrHelper] 识别结果: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
self.eventHandler("result", [
self.eventHandler?(["type": "result",
"text": event.result.text ?? "",
"detectedLanguage": detectedLanguage
])
@ -172,7 +177,7 @@ class AzureAsrHelper: NSObject {
if event.result.reason == SPXResultReason.recognizingSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
// print("[AzureAsrHelper] 识别中: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
self.eventHandler("recognizing", [
self.eventHandler?(["type": "recognizing",
"text": event.result.text ?? "",
"detectedLanguage": detectedLanguage
])
@ -185,7 +190,7 @@ class AzureAsrHelper: NSObject {
print("[AzureAsrHelper] 识别会话已开始")
self._isContinuousRecognitionActive = true
self.eventHandler("sessionStarted", [:])
self.eventHandler?(["type": "sessionStarted"])
}
recognizer.addSessionStoppedEventHandler { [weak self] _, _ in
@ -193,7 +198,7 @@ class AzureAsrHelper: NSObject {
print("[AzureAsrHelper] 识别会话已结束")
self._isContinuousRecognitionActive = false
self.eventHandler("sessionStopped", [:])
self.eventHandler?(["type": "sessionStopped"])
}
// 取消事件
@ -205,10 +210,7 @@ class AzureAsrHelper: NSObject {
print("[AzureAsrHelper] 识别取消: \(errorDetails)")
self.eventHandler("canceled", [
"reason": reason,
"errorDetails": errorDetails
])
self.eventHandler?(["type": "canceled", "reason": reason, "errorDetails": errorDetails])
self._isContinuousRecognitionActive = false
}
@ -217,9 +219,17 @@ class AzureAsrHelper: NSObject {
/// 执行一次性语音识别
/// - Returns: 是否成功启动识别
func recognizeOnce() -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler("error", ["message": "语音服务未初始化"])
eventHandler?(["type": "error", "message": "语音服务未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
@ -235,7 +245,7 @@ class AzureAsrHelper: NSObject {
do {
// 通知会话开始
eventHandler("sessionStarted", [:])
eventHandler?(["type": "sessionStarted"])
// 执行识别
try recognizer?.recognizeOnceAsync { [weak self] result in
@ -243,21 +253,21 @@ class AzureAsrHelper: NSObject {
if result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: result)
self.eventHandler("result", [
self.eventHandler?(["type": "result",
"text": result.text ?? "",
"detectedLanguage": detectedLanguage
])
} else if result.reason == SPXResultReason.noMatch {
print("[AzureAsrHelper] 无匹配结果")
self.eventHandler("noMatch", [:])
self.eventHandler?(["type": "noMatch"])
} else if result.reason == SPXResultReason.canceled {
do {
let details = try SPXCancellationDetails(fromCanceledRecognitionResult: result)
let errorDetails = details.errorDetails ?? "未知错误"
self.eventHandler("error", ["message": "识别取消: \(errorDetails)"])
self.eventHandler?(["type": "error", "message": "识别取消: \(errorDetails)"])
} catch {
print("[AzureAsrHelper] 错误: 获取取消详情失败: \(error.localizedDescription)")
self.eventHandler("error", ["message": "识别取消,无法获取详细原因"])
self.eventHandler?(["type": "error", "message": "识别取消,无法获取详细原因"])
}
}
}
@ -265,7 +275,7 @@ class AzureAsrHelper: NSObject {
return true
} catch {
print("[AzureAsrHelper] 错误: 识别异常: \(error.localizedDescription)")
eventHandler("error", ["message": "识别异常: \(error.localizedDescription)"])
eventHandler?(["type": "error", "message": "识别异常: \(error.localizedDescription)"])
return false
}
}
@ -273,9 +283,17 @@ class AzureAsrHelper: NSObject {
/// 开始连续语音识别
/// - Returns: 是否成功启动识别
func startContinuousRecognition() -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler("error", ["message": "语音服务未初始化"])
eventHandler?(["type": "error", "message": "语音服务未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
@ -289,7 +307,7 @@ class AzureAsrHelper: NSObject {
if recognizer == nil {
print("[AzureAsrHelper] 尝试重新创建识别器...")
if !createRecognizerAndSetupCallbacks() {
eventHandler("error", ["message": "无法创建语音识别器"])
eventHandler?(["type": "error", "message": "无法创建语音识别器"])
return false
}
}
@ -309,7 +327,7 @@ class AzureAsrHelper: NSObject {
print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)")
// 发送错误通知
eventHandler("error", ["message": "开始连续识别失败: \(error.localizedDescription)"])
eventHandler?(["type": "error", "message": "开始连续识别失败: \(error.localizedDescription)"])
return false
}
@ -319,6 +337,13 @@ class AzureAsrHelper: NSObject {
/// 停止连续语音识别
/// - Returns: 是否成功停止识别
func stopContinuousRecognition() -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler?(["type": "error", "message": "语音服务未初始化"])
return false
}
if !_isContinuousRecognitionActive {
print("[AzureAsrHelper] 未进行连续识别,忽略停止请求")
return true
@ -330,7 +355,7 @@ class AzureAsrHelper: NSObject {
guard let recognizer = recognizer else {
print("[AzureAsrHelper] 警告: 识别器为空,但状态显示活跃")
_isContinuousRecognitionActive = false
eventHandler("sessionStopped", [:])
eventHandler?(["type": "sessionStopped"])
return true
}
@ -355,7 +380,7 @@ class AzureAsrHelper: NSObject {
// 主线程回调通知结果
DispatchQueue.main.async {
print("[AzureAsrHelper] 连续识别已停止")
self.eventHandler("sessionStopped", [:])
self.eventHandler?(["type": "sessionStopped"])
}
} catch {
// 主线程回调通知错误
@ -363,7 +388,7 @@ class AzureAsrHelper: NSObject {
print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)")
// 停止失败,恢复状态
self._isContinuousRecognitionActive = true
self.eventHandler("error", ["error": "停止识别失败: \(error.localizedDescription)"])
self.eventHandler?(["type": "error", "message": "停止识别失败: \(error.localizedDescription)"])
}
}
}
@ -380,9 +405,17 @@ class AzureAsrHelper: NSObject {
/// 释放资源
func dispose() {
// 停止所有识别操作
// 尝试停止所有识别操作
if _isContinuousRecognitionActive {
stopContinuousRecognition()
do {
if let recognizer = recognizer {
try recognizer.stopContinuousRecognition()
print("[AzureAsrHelper] 连续识别已停止")
}
} catch {
print("[AzureAsrHelper] 警告: 停止连续识别失败: \(error.localizedDescription)")
}
_isContinuousRecognitionActive = false
}
// 释放识别器
@ -392,7 +425,6 @@ class AzureAsrHelper: NSObject {
// 重置状态
isInitialized = false
_isContinuousRecognitionActive = false
print("[AzureAsrHelper] 资源已释放")
}
@ -411,4 +443,9 @@ class AzureAsrHelper: NSObject {
return currentLanguage
}
}
/// 设置事件处理器
@objc func setEventHandler(_ handler: @escaping ([String: Any]) -> Void) {
self.eventHandler = handler
}
}

85
ios/Runner/AzureTtsHelper.swift

@ -8,7 +8,7 @@ class AzureTtsHelper: NSObject {
// MARK: - 属性
/// 事件处理回调
private var eventHandler: (String, [String: Any]) -> Void
private var eventHandler: (([String: Any]) -> Void)?
/// 语音配置信息
private var speechSubscriptionKey: String = ""
@ -54,8 +54,7 @@ class AzureTtsHelper: NSObject {
// MARK: - 初始化
init(eventHandler: @escaping (String, [String: Any]) -> Void) {
self.eventHandler = eventHandler
override init() {
super.init()
}
@ -63,6 +62,11 @@ class AzureTtsHelper: NSObject {
dispose()
}
/// 设置事件处理器
@objc func setEventHandler(_ handler: @escaping ([String: Any]) -> Void) {
self.eventHandler = handler
}
// MARK: - TTS 接口实现
/// 初始化语音合成服务
@ -77,7 +81,7 @@ class AzureTtsHelper: NSObject {
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler("error", ["error": "Azure 配置信息不完整"])
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
@ -114,7 +118,7 @@ class AzureTtsHelper: NSObject {
return true
} catch {
print("[AzureTtsHelper] 错误: 初始化语音合成服务失败: \(error.localizedDescription)")
eventHandler("error", ["error": "初始化语音合成服务失败: \(error.localizedDescription)"])
eventHandler?(["type": "error", "message": "初始化语音合成服务失败: \(error.localizedDescription)"])
return false
}
}
@ -139,15 +143,23 @@ class AzureTtsHelper: NSObject {
/// - Parameter voiceName: 语音名称 (如 "zh-CN-XiaoxiaoNeural")
/// - Returns: 设置是否成功
func setVoice(voiceName: String) -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
if voiceName.isEmpty {
print("[AzureTtsHelper] 错误: 声音名称为空")
eventHandler("error", ["error": "声音名称不能为空"])
eventHandler?(["type": "error", "message": "声音名称不能为空"])
return false
}
@ -175,9 +187,17 @@ class AzureTtsHelper: NSObject {
/// - volume: 音量,范围 0 到 100,默认为 100
/// - Returns: 是否设置成功
func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
@ -194,9 +214,17 @@ class AzureTtsHelper: NSObject {
/// - Parameter text: 要合成的文本
/// - Returns: 操作是否成功启动
func speakText(text: String) -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
@ -223,12 +251,12 @@ class AzureTtsHelper: NSObject {
private func speakSsmlInternal(text: String) -> Bool {
guard let synthesizer = synthesizer else {
print("[AzureTtsHelper] 错误: 合成器未初始化")
eventHandler("error", ["error": "合成器未初始化"])
eventHandler?(["type": "error", "message": "合成器未初始化"])
return false
}
_isSpeaking = true
eventHandler("started", [:])
eventHandler?(["type": "started"])
// Task {
do {
@ -239,7 +267,7 @@ class AzureTtsHelper: NSObject {
print("[AzureTtsHelper] 错误: 语音合成失败: \(error.localizedDescription)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成失败: \(error.localizedDescription)"])
self.eventHandler?(["type": "error", "message": "语音合成失败: \(error.localizedDescription)"])
}
}
// }
@ -250,7 +278,15 @@ class AzureTtsHelper: NSObject {
/// 停止当前语音合成
/// - Returns: 操作是否成功
func stopSpeaking() -> Bool {
if !isInitialized || !_isSpeaking {
// 检查是否初始化
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
if !_isSpeaking {
print("[AzureTtsHelper] 未进行语音播放,忽略停止请求")
return true
}
@ -258,12 +294,12 @@ class AzureTtsHelper: NSObject {
do {
try synthesizer?.stopSpeaking()
_isSpeaking = false
eventHandler("canceled", [:])
eventHandler?(["type": "canceled"])
print("[AzureTtsHelper] 已停止语音合成")
return true
} catch {
print("[AzureTtsHelper] 错误: 停止语音合成失败: \(error.localizedDescription)")
eventHandler("error", ["error": "停止语音合成失败: \(error.localizedDescription)"])
eventHandler?(["type": "error", "message": "停止语音合成失败: \(error.localizedDescription)"])
return false
}
}
@ -276,9 +312,17 @@ class AzureTtsHelper: NSObject {
/// 释放资源
func dispose() {
// 停止合成
// 尝试停止所有语音合成
if _isSpeaking {
stopSpeaking()
do {
if let synthesizer = synthesizer {
try synthesizer.stopSpeaking()
print("[AzureTtsHelper] 语音合成已停止")
}
} catch {
print("[AzureTtsHelper] 警告: 停止语音合成失败: \(error.localizedDescription)")
}
_isSpeaking = false
}
// 释放合成器
@ -287,7 +331,6 @@ class AzureTtsHelper: NSObject {
// 重置状态
isInitialized = false
_isSpeaking = false
isAudioSessionConfigured = false
print("[AzureTtsHelper] 资源已释放")
@ -310,7 +353,7 @@ class AzureTtsHelper: NSObject {
print("[AzureTtsHelper] 语音合成完成: 音频持续时间: \(e.result.audioDuration)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("completed", [:])
self.eventHandler?(["type": "completed"])
}
}
@ -330,14 +373,14 @@ class AzureTtsHelper: NSObject {
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成取消: \(cancellationDetails.errorDetails ?? "未知错误")"])
self.eventHandler?(["type": "error", "message": "语音合成取消: \(cancellationDetails.errorDetails ?? "未知错误")"])
}
} catch {
print("[AzureTtsHelper] 获取取消详情时出错: \(error)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成被取消"])
self.eventHandler?(["type": "error", "message": "语音合成被取消"])
}
}
}

62
ios/Runner/VoiceInteractionService.swift

@ -162,15 +162,21 @@ class VoiceInteractionService: NSObject {
NSLog("%@: 初始化服务组件", TAG)
// 初始化Azure ASR
azureAsrHelper = AzureAsrHelper { [weak self] eventName, eventData in
azureAsrHelper = AzureAsrHelper()
if let helper = azureAsrHelper {
helper.setEventHandler({ [weak self] eventData in
// 处理ASR事件
self?.handleAsrEvent(eventName: eventName, eventData: eventData)
self?.handleAsrEvent(eventData: eventData)
})
}
// 初始化Azure TTS
azureTtsHelper = AzureTtsHelper { [weak self] eventName, eventData in
azureTtsHelper = AzureTtsHelper()
if let helper = azureTtsHelper {
helper.setEventHandler({ [weak self] eventData in
// 处理TTS事件
self?.handleTtsEvent(eventName: eventName, eventData: eventData)
self?.handleTtsEvent(eventData: eventData)
})
}
// 初始化火山AI服务
@ -237,24 +243,43 @@ class VoiceInteractionService: NSObject {
/**
* 处理ASR事件
*/
private func handleAsrEvent(eventName: String, eventData: [String: Any]) {
// NSLog("%@: 处理ASR事件: %@", TAG, eventName)
switch eventName {
private func handleAsrEvent(eventData: [String: Any]) {
if let type = eventData["type"] as? String {
// NSLog("%@: 处理ASR事件: %@", TAG, type)
switch type {
case "recognizing":
if let text = eventData["text"] as? String, !text.isEmpty {
hasSpeechDetected = true
stopCurrentTTS()
updateLastActivityTime()
} else if let data = eventData["data"] as? [String: Any],
let text = data["text"] as? String,
!text.isEmpty {
hasSpeechDetected = true
stopCurrentTTS()
updateLastActivityTime()
}
case "result":
if let text = eventData["text"] as? String, !text.isEmpty {
var text: String?
// 直接读取text或从data中读取
if let directText = eventData["text"] as? String, !directText.isEmpty {
text = directText
} else if let data = eventData["data"] as? [String: Any],
let dataText = data["text"] as? String,
!dataText.isEmpty {
text = dataText
}
if let finalText = text {
updateLastActivityTime()
// 保存用户消息
lastUserMessage = text
lastUserMessage = finalText
processWithVolcanoAI(text)
processWithVolcanoAI(finalText)
}
// 重置状态,继续识别
@ -271,19 +296,25 @@ class VoiceInteractionService: NSObject {
case "error":
isRecognitionActive = false
if let message = eventData["message"] as? String {
NSLog("%@: ASR错误: %@", TAG, message)
}
playNotification("语音识别出错")
default:
break
}
}
}
/**
* 处理TTS事件
*/
private func handleTtsEvent(eventName: String, eventData: [String: Any]) {
// NSLog("%@: 处理TTS事件: %@", TAG, eventName)
switch eventName {
private func handleTtsEvent(eventData: [String: Any]) {
if let type = eventData["type"] as? String {
// NSLog("%@: 处理TTS事件: %@", TAG, type)
switch type {
case "started":
isTtsSpeaking = true
@ -296,12 +327,15 @@ class VoiceInteractionService: NSObject {
case "error":
isTtsSpeaking = false
NSLog("%@: TTS错误: %@", TAG, eventData["error"] as? String ?? "未知错误")
if let message = eventData["message"] as? String {
NSLog("%@: TTS错误: %@", TAG, message)
}
default:
break
}
}
}
/**
* 处理双击事件

2
lib/data/services/speech_impl/azure_asr_service.dart

@ -36,7 +36,6 @@ class AzureAsrService extends GetxService implements AsrService {
AzureAsrService() {
_loadConfig();
_setupEventChannel();
}
/// 从环境变量加载配置
@ -80,6 +79,7 @@ class AzureAsrService extends GetxService implements AsrService {
'region': _serviceRegion,
'supportedLanguages': _supportedLanguages,
});
_setupEventChannel();
_isInitialized = result;
Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}');

20
macos/Podfile.lock

@ -1,9 +1,4 @@
PODS:
- app_settings (5.1.1):
- FlutterMacOS
- audio_service (0.0.1):
- Flutter
- FlutterMacOS
- audio_session (0.0.1):
- FlutterMacOS
- device_info_plus (0.0.1):
@ -32,16 +27,11 @@ PODS:
- 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`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
@ -54,7 +44,6 @@ DEPENDENCIES:
- 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:
@ -62,10 +51,6 @@ SPEC REPOS:
- 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:
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
device_info_plus:
@ -90,14 +75,10 @@ EXTERNAL SOURCES:
: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
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
@ -110,7 +91,6 @@ SPEC CHECKSUMS:
record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96
window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9

Loading…
Cancel
Save