diff --git a/lib/modules/agent/widgets/sound_bar_widget.dart b/lib/modules/agent/widgets/sound_bar_widget.dart index 11fb9ed86..9028f713b 100644 --- a/lib/modules/agent/widgets/sound_bar_widget.dart +++ b/lib/modules/agent/widgets/sound_bar_widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/agent_controller.dart'; -/// 声音柱动画组件 +/// 声音柱动画组件 (已优化) /// 用于显示语音输入时的音频波形效果 class SoundBarWidget extends StatefulWidget { final double delay; @@ -37,9 +37,8 @@ class _SoundBarWidgetState extends State parent: _controller, curve: Curves.easeInOut, ), - )..addListener(() { - setState(() {}); - }); + ); + // 移除了 .addListener(() { setState(() {}); }) _startOrStopAnimation(); } @@ -54,6 +53,9 @@ class _SoundBarWidgetState extends State /// 开始或停止动画 void _startOrStopAnimation() { + // 确保组件仍然挂载在树上 + if (!mounted) return; + if (widget.isActive) { Future.delayed(Duration(milliseconds: (widget.delay * 1000).toInt()), () { if (mounted && widget.isActive) { @@ -61,9 +63,10 @@ class _SoundBarWidgetState extends State } }); } else { - _controller.reset(); - // 设置为小高度的静态状态 - _controller.animateTo(0.1, duration: const Duration(milliseconds: 300)); + // 停止并平滑地过渡到初始状态 + _controller.stop(); + _controller.animateTo(0.0, + duration: const Duration(milliseconds: 200), curve: Curves.easeOut); } } @@ -77,21 +80,28 @@ class _SoundBarWidgetState extends State Widget build(BuildContext context) { final primaryColor = Theme.of(context).primaryColor; - return Container( - width: 2.5, - height: widget.isActive ? _animation.value : 3, - margin: const EdgeInsets.symmetric(horizontal: 1), - decoration: BoxDecoration( - color: widget.isActive - ? primaryColor.withOpacity(0.8) - : Colors.grey.withOpacity(0.4), - borderRadius: BorderRadius.circular(1.5), - ), + // 使用 AnimatedBuilder 来优化性能 + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Container( + width: 2.5, + // 当动画不活跃时,高度固定为3;活跃时使用动画值 + height: widget.isActive ? _animation.value : 3, + margin: const EdgeInsets.symmetric(horizontal: 1), + decoration: BoxDecoration( + color: widget.isActive + ? primaryColor.withOpacity(0.8) + : Colors.grey.withOpacity(0.4), + borderRadius: BorderRadius.circular(1.5), + ), + ); + }, ); } } -/// 声音柱组合组件 +/// 声音柱组合组件 (无需改动) /// 包含多个声音柱的组合显示 class SoundBarsWidget extends StatelessWidget { final bool isActive; @@ -120,7 +130,7 @@ class SoundBarsWidget extends StatelessWidget { } } -/// 带控制器集成的声音柱组件 +/// 带控制器集成的声音柱组件 (无需改动) /// 自动从AgentController获取状态 class AgentSoundBarsWidget extends StatelessWidget { final int barCount; diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 1287e6c45..902ceb58d 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -6,9 +6,6 @@ import '../../../core/utils/logger.dart'; import 'package:flutter/material.dart'; import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/ble_manager.dart'; -import '../../../data/services/version_update_service.dart'; -import '../../common/widgets/version_update_dialog.dart'; -import 'package:get_storage/get_storage.dart'; /// 连接状态枚举 enum ConnectionStatus { @@ -54,17 +51,9 @@ class HomeController extends GetxController { final _bluetoothManager = Get.find(); final _bleManager = Get.find(); - final _storage = GetStorage(); final noviceGuideService = Get.find(); - // 会话级别的版本检查标记(静态变量,整个应用会话期间保持) - static bool _hasCheckedVersionInSession = false; - - // 版本检查相关常量 - static const String _lastVersionCheckKey = 'last_version_check_timestamp'; - static const int _versionCheckIntervalHours = 24; // 24小时间隔 - @override void onInit() { super.onInit(); @@ -82,12 +71,6 @@ class HomeController extends GetxController { void _initWithCachedInfo() { _setupStateListeners(); _checkConnectionStatus(); - - // 延迟检查版本更新,避免影响页面加载 - // 混合检查机制:会话检查 + 24小时间隔检查 - Future.delayed(const Duration(seconds: 2), () { - _checkVersionUpdateIfNeeded(); - }); } void _setupStateListeners() { @@ -277,105 +260,4 @@ class HomeController extends GetxController { ? deviceInfo.value!.firmwareVersion : 'unknown'.tr; } - - /// 混合检查机制:判断是否需要进行版本检查 - /// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时 - void _checkVersionUpdateIfNeeded() { - try { - final now = DateTime.now(); - final lastCheckTimestamp = _storage.read(_lastVersionCheckKey); - - bool shouldCheck = false; - String reason = ''; - - // 条件1:会话内未检查过 - if (!_hasCheckedVersionInSession) { - shouldCheck = true; - reason = '会话内首次检查'; - } - // 条件2:距离上次检查超过24小时 - else if (lastCheckTimestamp != null) { - final lastCheckTime = - DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp); - final timeDifference = now.difference(lastCheckTime); - - if (timeDifference.inHours >= _versionCheckIntervalHours) { - shouldCheck = true; - reason = '距离上次检查已超过${_versionCheckIntervalHours}小时'; - } - } - // 条件3:从未检查过(首次使用应用) - else { - shouldCheck = true; - reason = '首次使用应用'; - } - - if (shouldCheck) { - Logger.i(_tag, '开始版本检查 - 原因: $reason'); - _performVersionCheck(); - } else { - Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔'); - } - } catch (e) { - Logger.e(_tag, '版本检查判断失败: $e'); - } - } - - /// 执行版本检查(在后台进行,不阻塞启动流程) - Future _performVersionCheck() async { - try { - final versionService = VersionUpdateService.to; - final updateInfo = await versionService.checkForUpdate(); - - if (updateInfo.hasUpdate) { - Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); - // 显示更新对话框 - await VersionUpdateDialog.show(updateInfo); - } else { - Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}'); - } - - // 更新检查记录 - _updateVersionCheckRecord(); - } catch (e) { - Logger.e(_tag, '版本检查失败: $e'); - // 即使检查失败,也更新记录,避免频繁重试 - _updateVersionCheckRecord(); - } - } - - /// 更新版本检查记录 - void _updateVersionCheckRecord() { - try { - // 更新时间戳 - final now = DateTime.now(); - _storage.write(_lastVersionCheckKey, now.millisecondsSinceEpoch); - - // 标记本次会话已检查过版本 - _hasCheckedVersionInSession = true; - - Logger.i(_tag, '版本检查记录已更新: ${now.toIso8601String()}'); - } catch (e) { - Logger.e(_tag, '更新版本检查记录失败: $e'); - } - } - - /// 重置版本检查标记(可用于强制重新检查) - /// 注意:这会清除会话标记,但不会清除时间戳记录 - static void resetVersionCheckFlag() { - _hasCheckedVersionInSession = false; - Logger.i('HomeCtrl', '版本检查会话标记已重置'); - } - - /// 完全重置版本检查状态(包括时间戳) - /// 用于测试或特殊情况下强制重新检查 - void forceResetVersionCheck() { - try { - _hasCheckedVersionInSession = false; - _storage.remove(_lastVersionCheckKey); - Logger.i(_tag, '版本检查状态已完全重置'); - } catch (e) { - Logger.e(_tag, '重置版本检查状态失败: $e'); - } - } } diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index ba834cdc1..4bf70d207 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -14,10 +14,12 @@ import '../../../data/services/network/api.dart'; import '../../../data/services/version_update_service.dart'; import '../../../modules/common/widgets/version_update_dialog.dart'; import '../../../modules/common/widgets/privacy_policy_dialog.dart'; -import '../../common/webview_page.dart'; +import '../../../core/utils/logger.dart'; class SplashController extends GetxController { final GetStorage _storage = GetStorage(); + static const String _tag = 'SplashCtrl'; + // 进度条动画控制 final _loadingProgress = 0.0.obs; double get loadingProgress => _loadingProgress.value; @@ -34,6 +36,13 @@ class SplashController extends GetxController { // 主题服务 final ThemeService _themeService = ThemeService(); + // 会话级别的版本检查标记(静态变量,整个应用会话期间保持) + static bool _hasCheckedVersionInSession = false; + + // 版本检查相关常量 + static const String _lastVersionCheckKey = 'last_version_check_timestamp'; + static const int _versionCheckIntervalHours = 24; // 24小时间隔 + @override void onInit() { super.onInit(); @@ -50,10 +59,19 @@ class SplashController extends GetxController { } }); - //弹出用户追中问题 - WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) async { - final status = - await AppTrackingTransparency.requestTrackingAuthorization(); + // 异步处理用户追踪授权,不阻塞主流程 + _requestTrackingPermissionAsync(); + } + + /// 异步请求追踪权限,不阻塞启动流程 + void _requestTrackingPermissionAsync() { + Future.delayed(const Duration(milliseconds: 500), () async { + try { + await AppTrackingTransparency.requestTrackingAuthorization(); + Logger.d(_tag, '用户追踪权限请求完成'); + } catch (e) { + Logger.e(_tag, '用户追踪权限请求失败: $e'); + } }); } @@ -62,10 +80,23 @@ class SplashController extends GetxController { _loadAppResources(); } + /// 智能调度版本检查,在页面跳转后执行,完全不影响启动流程 + void _scheduleVersionCheck() { + // 在独立的微任务中执行,避免阻塞主线程 + scheduleMicrotask(() async { + try { + Logger.d(_tag, '开始执行版本检查(后台任务)'); + _checkVersionUpdateIfNeeded(); + } catch (e) { + Logger.e(_tag, '版本检查调度失败: $e'); + } + }); + } + void _onPrivacyAccepted() { _storage.write('hasAcceptedPrivacy', true); Get.back(); // 关闭弹窗 - _startNormalFlow(); // 开始正常流程 + _startNormalFlow(); // 开始正常流程(已包含版本检查) } void _onPrivacyRejected() { @@ -84,18 +115,6 @@ class SplashController extends GetxController { ); } - // 初始化地图SDK - // void _initMapSDK() { - // try { - // AmapFlutter().initSdk( - // iosKey: "a4a1394fe817c2f86a424b897b4a9af4", - // androidKey: "78e7946dd602778df82c6ae143a88184", - // ); - // } catch (e) { - // print('地图SDK初始化失败: $e'); - // } - // } - // 进度条动画(持续2秒) void _startLoadingAnimation() { const totalDuration = 1500; // 动画总时长增加到1.5秒 @@ -122,22 +141,23 @@ class SplashController extends GetxController { // await _loadImages(); // await _initDatabase(); - // 检查版本更新(在后台进行,不阻塞启动流程) - //_checkVersionUpdate(); - _isResourceLoaded.value = true; _checkNavigateCondition(); } catch (e) { Get.snackbar('错误', '资源加载失败,请检查网络连接'); _isResourceLoaded.value = true; // 允许用户继续使用 - print('资源加载错误: $e'); + Logger.e(_tag, '资源加载错误: $e'); } } // 检查跳转条件 - void _checkNavigateCondition() { + void _checkNavigateCondition() async { if (_loadingProgress.value >= 1.0 && _isResourceLoaded.value) { - _navigateToLogin(); + // 关键改动:先执行页面跳转,并用 await 等待它完成 + await _navigateToLogin(); + + // 页面跳转完全结束后,再安心地安排版本检查 + _scheduleVersionCheck(); } } @@ -145,7 +165,7 @@ class SplashController extends GetxController { Future _navigateToLogin() async { bool isLoggedIn = false; final token = _storage.read('logintoken'); - print('getLoginToken:进来了${token}'); + Logger.d(_tag, 'getLoginToken:进来了${token}'); if (token != null && token.isNotEmpty) { isLoggedIn = await getLoginToken(); } @@ -159,14 +179,14 @@ class SplashController extends GetxController { Future getLoginToken() async { try { - print('getLoginToken:进来了'); + Logger.d(_tag, 'getLoginToken:进来了'); //if (User.getname() != "") { final response = await Api.tokenlogin(); - print('getLoginToken: $response'); + Logger.d(_tag, 'getLoginToken: $response'); if (response == null) return false; if (response['user'] != '') { - print('getLoginToken:进来了'); + Logger.d(_tag, 'getLoginToken:进来了'); // 根据API设计调整 final user = User.fromJson(response); @@ -178,21 +198,21 @@ class SplashController extends GetxController { //} return false; } catch (e) { - print('Token获取失败: $e'); + Logger.e(_tag, 'Token获取失败: $e'); return false; } } Future getAppConfig() async { try { - print('getAppConfig:进来了'); + Logger.d(_tag, 'getAppConfig:进来了'); //if (User.getname() != "") { final response = await Api.getappconfig(); - print('getAppConfig: $response'); + Logger.d(_tag, 'getAppConfig: $response'); if (response == null) return false; // 根据API设计调整 final resp = UserGetAppConfigResp.fromJson(response); - print('getAppConfig:进来了'); + Logger.d(_tag, 'getAppConfig:进来了'); // 存储用户信息(示例) // _storage.write('ENV', resp.env); // _storage.write('AGENTS', resp.agents); @@ -200,27 +220,116 @@ class SplashController extends GetxController { AppConfig.initialize(resp); return true; } catch (e) { - print('getAppConfig 取失败: $e'); + Logger.e(_tag, 'getAppConfig 取失败: $e'); return false; } } - /// 检查版本更新(后台执行,不阻塞启动流程) - // void _checkVersionUpdate() { - // // 延迟执行版本检查,避免影响启动速度 - // Timer(const Duration(seconds: 1), () async { - // try { - // final versionService = VersionUpdateService.to; - // final updateInfo = await versionService.checkForUpdate(); - - // if (updateInfo.hasUpdate) { - // // 显示更新对话框 - // await VersionUpdateDialog.show(updateInfo); - // } - // } catch (e) { - // print('版本检查失败: $e'); - // // 版本检查失败不影响用户使用,静默处理 - // } - // }); - // } + /// 混合检查机制:判断是否需要进行版本检查 + /// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时 + void _checkVersionUpdateIfNeeded() { + try { + final now = DateTime.now(); + final lastCheckTimestamp = _storage.read(_lastVersionCheckKey); + + bool shouldCheck = false; + String reason = ''; + + // 条件1:会话内未检查过 + if (!_hasCheckedVersionInSession) { + shouldCheck = true; + reason = '会话内首次检查'; + } + // 条件2:距离上次检查超过24小时 + else if (lastCheckTimestamp != null) { + final lastCheckTime = + DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp); + final timeDifference = now.difference(lastCheckTime); + + if (timeDifference.inHours >= _versionCheckIntervalHours) { + shouldCheck = true; + reason = '距离上次检查已超过${_versionCheckIntervalHours}小时'; + } + } + // 条件3:从未检查过(首次使用应用) + else { + shouldCheck = true; + reason = '首次使用应用'; + } + + if (shouldCheck) { + Logger.i(_tag, '开始版本检查 - 原因: $reason'); + _performVersionCheck(); + } else { + Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔'); + } + } catch (e) { + Logger.e(_tag, '版本检查判断失败: $e'); + } + } + + /// 执行版本检查(在后台进行,不阻塞启动流程) + Future _performVersionCheck() async { + try { + final versionService = VersionUpdateService.to; + // 异步执行,不等待结果 + versionService.checkForUpdate().then((updateInfo) { + if (updateInfo.hasUpdate) { + Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); + // 在主线程中显示更新对话框 + WidgetsBinding.instance.addPostFrameCallback((_) { + VersionUpdateDialog.show(updateInfo); + }); + } else { + Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}'); + } + + // 更新检查记录 + _updateVersionCheckRecord(); + }).catchError((e) { + Logger.e(_tag, '版本检查失败: $e'); + // 即使检查失败,也更新记录,避免频繁重试 + _updateVersionCheckRecord(); + }); + } catch (e) { + Logger.e(_tag, '版本检查失败: $e'); + // 即使检查失败,也更新记录,避免频繁重试 + _updateVersionCheckRecord(); + } + } + + /// 更新版本检查记录 + void _updateVersionCheckRecord() { + try { + // 更新时间戳 + final now = DateTime.now(); + _storage.write(_lastVersionCheckKey, now.millisecondsSinceEpoch); + + // 标记本次会话已检查过版本 + _hasCheckedVersionInSession = true; + + Logger.i(_tag, '版本检查记录已更新: ${now.toIso8601String()}'); + } catch (e) { + Logger.e(_tag, '更新版本检查记录失败: $e'); + } + } + + /// 重置版本检查标记(可用于强制重新检查) + /// 注意:这会清除会话标记,但不会清除时间戳记录 + static void resetVersionCheckFlag() { + _hasCheckedVersionInSession = false; + Logger.i('SplashCtrl', '版本检查会话标记已重置'); + } + + /// 完全重置版本检查状态(包括时间戳) + /// 用于测试或特殊情况下强制重新检查 + void forceResetVersionCheck() { + try { + _hasCheckedVersionInSession = false; + _storage.remove(_lastVersionCheckKey); + Logger.i(_tag, '版本检查状态已完全重置'); + } catch (e) { + Logger.e(_tag, '重置版本检查状态失败: $e'); + } + } } diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index 24b60e444..716ea30ae 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -32,6 +32,7 @@ public class AzureAsrHelper: NSObject { private var subscriptionKey = "" private var region = "" + // 音频源配置 public enum AudioSourceType { /** 使用设备麦克风 */ @@ -59,10 +60,14 @@ public class AzureAsrHelper: NSObject { * @param audioSourceType 音频源类型 * @return 初始化是否成功 */ + /** + * 初始化Azure语音服务 + * 预初始化关键组件以提升首次启动性能 + */ public func initialize( subscriptionKey: String, region: String, - supportedLanguages: [String] = ["zh-CN"], + supportedLanguages: [String] = ["zh-CN", "en-US"], audioSourceType: AudioSourceType = .microphone ) -> Bool { do { @@ -95,21 +100,23 @@ public class AzureAsrHelper: NSObject { // 创建语音配置 speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) - speechConfig?.setPropertyTo("800", byName: "SpeechServiceConnection_EndSilenceTimeoutMs") - speechConfig?.setPropertyTo("800", byName: "Speech_SegmentationSilenceTimeoutMs") - speechConfig?.setPropertyTo("Time", byName: "Speech_SegmentationStrategy") - if isAutoDetectLanguage { - // 直接启用语言检测模式 - speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") - os_log("启用语言检测模式", log: log, type: .info) + // 移除过时的属性设置方式 + // speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") + speechConfig?.setPropertyTo(supportedLanguages.joined(separator: ","), byName: "SpeechServiceConnection_AutoDetectSourceLanguages") + + // 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理 + os_log("启用语言检测模式,支持语言: %{public}@", log: log, type: .info, supportedLanguages.joined(separator: ",")) } else { - // 设置指定的识别语言 speechConfig?.speechRecognitionLanguage = currentLanguage } + // 录音文件类 recordfile = RecordFile() - // 创建识别器 + + // 【优化】预初始化音频流,避免首次启动延迟 + preInitializeAudioComponents() + return true } catch { os_log("初始化失败: %{public}@", log: log, type: .error, error.localizedDescription) @@ -117,6 +124,26 @@ public class AzureAsrHelper: NSObject { } } + /** + * 预初始化音频组件 + * 在初始化阶段就准备好音频流和相关组件,减少首次启动延迟 + */ + private func preInitializeAudioComponents() { + if audioStream == nil { + audioStream = AudioStream() + audioStream?.initAudioRecord() + audioStream?.onAudioData = { [weak self] data in + self?.recordfile?.saveAudioDataToWav(data) + } + + // 预创建音频配置 + if let pushStream = audioStream?.pushAudioStream { + audioConfig = SPXAudioConfiguration(streamInput: pushStream) + os_log("预初始化音频组件完成", log: log, type: .info) + } + } + } + /** * 开始连续语音识别 @@ -165,6 +192,28 @@ public class AzureAsrHelper: NSObject { return false } } + // /** + // * 获取检测到的语言 + // */ + // private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String { + // if !isAutoDetectLanguage { + // return "" + // } + + // // 尝试从属性中获取语言 + // if let properties = result.properties, + // let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage") { + // return language + // } + + // // 尝试另一种方式获取语言 + // do { + // let langResult = try SPXAutoDetectSourceLanguageResult(result) + // return langResult.language ?? "" + // } catch { + // return "" + // } + // } /** * 停止连续语音识别 @@ -257,30 +306,32 @@ public class AzureAsrHelper: NSObject { /** * 设置识别器 */ + /** + * 设置识别器 + * 优化:避免重复创建音频流,复用已初始化的组件 + */ private func setupRecognizer() -> Bool { do { print("设置识别器${_isContinuousRecognitionActive}") + // 如果正在进行连续识别,先停止 if _isContinuousRecognitionActive { - // 直接停止,不等待结果 try? recognizer?.stopContinuousRecognition() _isContinuousRecognitionActive = false } - setupMicrophoneStream() + + // 【优化】只有在音频流未初始化时才创建,避免重复初始化 + if audioStream == nil || audioConfig == nil { + setupMicrophoneStream() + } + // 清理旧的识别器 recognizer = nil // 创建识别器 if isAutoDetectLanguage { - // 使用更精确的语言配置方式 - var sourceLanguageConfigs: [SPXSourceLanguageConfiguration] = [] - for language in supportedLanguages { - if let config = try? SPXSourceLanguageConfiguration(language) { - sourceLanguageConfigs.append(config) - } - } - - let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(sourceLanguageConfigurations: sourceLanguageConfigs) + // 使用正确的初始化方法 + let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) recognizer = try SPXSpeechRecognizer( speechConfiguration: speechConfig!, autoDetectSourceLanguageConfiguration: autoDetectConfig, @@ -296,7 +347,7 @@ public class AzureAsrHelper: NSObject { return true } catch { os_log("创建识别器失败: %{public}@", log: log, type: .error, error.localizedDescription) - try? recognizer?.stopContinuousRecognition() + try? recognizer?.stopContinuousRecognition() return false } } @@ -304,31 +355,76 @@ public class AzureAsrHelper: NSObject { /** * 设置麦克风流 - 使用推流方式 + * 优化:避免重复初始化已存在的音频流 */ private func setupMicrophoneStream() { do { - - if (audioStream == nil) { - // 创建外部音频拉流对象 - // 明确指定使用外部音频源(推流模式) + // 【优化】检查是否已经初始化,避免重复创建 + if audioStream == nil { audioStream = AudioStream() audioStream?.initAudioRecord() - audioStream?.onAudioData = { [weak self] data in - self?.recordfile?.saveAudioDataToWav(data) - } + audioStream?.onAudioData = { [weak self] data in + self?.recordfile?.saveAudioDataToWav(data) + } } - // 创建音频配置 - if let pushStream = audioStream?.pushAudioStream { // Safely unwrap optional + // 【优化】检查音频配置是否已存在 + if audioConfig == nil, let pushStream = audioStream?.pushAudioStream { audioConfig = SPXAudioConfiguration(streamInput: pushStream) - os_log("设置麦克风流:") + os_log("设置麦克风流完成", log: log, type: .info) } } catch { os_log("设置麦克风流失败: %{public}@", log: log, type: .error, error.localizedDescription) audioStream = nil } } - + /** + * 从语音识别结果中获取检测到的语言 + * @param result 语音识别结果 + * @return 检测到的语言代码,如果检测失败则返回当前语言 + */ + private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String { + guard isAutoDetectLanguage else { + return currentLanguage + } + + print("result: \(result)") + + // 备用方案:从属性中获取语言 + if let properties = result.properties, + let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage"), + !language.isEmpty { + print("从属性中检测到语言: \(language)") + return language + } + + // 最后的备用方案:从JSON响应中解析 + if let properties = result.properties, + let jsonString = properties.getPropertyByName("SpeechServiceResponse_Json"), + let jsonData = jsonString.data(using: .utf8) { + do { + if let json = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any], + let language = json["Language"] as? String { + print("从JSON中检测到语言: \(language)") + return language + } + } catch { + print("JSON解析失败: \(error)") + } + } + // 使用正确的SPXAutoDetectSourceLanguageResult初始化方法 + do { + let langResult = try SPXAutoDetectSourceLanguageResult(result) + if let language = langResult.language, !language.isEmpty { + print("SDK方法检测到语言: \(language)") + return language + } + } catch { + print("SDK语言检测失败: \(error.localizedDescription)") + } + print("无法检测语言,使用默认语言: \(currentLanguage)") + return currentLanguage + } /** * 设置事件监听器 */ @@ -342,68 +438,28 @@ public class AzureAsrHelper: NSObject { guard let recognizer = recognizer else { return false} // 正在识别事件 recognizer.addRecognizingEventHandler { [weak self] (_, event) in - guard let self = self else { return } + guard let self = self else { return } let result = event.result - print("识别中文本: \(result.text)") - // 优化:只处理非空结果 - if !(result.text?.isEmpty ?? true) { - let detectedLanguage: String - if self.isAutoDetectLanguage { - do { - let langResult = try SPXAutoDetectSourceLanguageResult(result) - let rawLanguage = langResult.language - print("[DEBUG] 原始检测语言: \(rawLanguage ?? "nil"), 文本: \(result.text ?? "nil"), 支持的语言: \(self.supportedLanguages)") - - // 应用启发式语言检测纠正 - detectedLanguage = self.correctLanguageDetection(rawLanguage: rawLanguage ?? "", text: result.text ?? "") - print("[DEBUG] 纠正后语言: \(detectedLanguage)") - } catch { - print("[DEBUG] 语言检测异常: \(error.localizedDescription)") - detectedLanguage = "" - } - } else { - detectedLanguage = self.currentLanguage - print("[DEBUG] 使用当前语言: \(detectedLanguage)") - } - print("识别中事件: \(detectedLanguage)") - DispatchQueue.main.async { - callback.onRecognizing(result.text ?? "", detectedLanguage) - } + guard !(result.text?.isEmpty ?? true) else { return } + + let detectedLanguage = self.getDetectedLanguage(from: result) + DispatchQueue.main.async { + callback.onRecognizing(result.text ?? "", detectedLanguage) } } // 识别完成事件 recognizer.addRecognizedEventHandler { [weak self] (_, event) in - guard let self = self else { return } + guard let self = self else { return } let result = event.result - print("识别完成文本: \(result.text)") - if result.reason == .recognizedSpeech && !(result.text?.isEmpty ?? true) { - let detectedLanguage: String - if self.isAutoDetectLanguage { - do { - let langResult = try SPXAutoDetectSourceLanguageResult(result) - let rawLanguage = langResult.language - print("[DEBUG] 识别完成 - 原始检测语言: \(rawLanguage ?? "nil"), 文本: \(result.text ?? "nil"), 支持的语言: \(self.supportedLanguages)") - - // 应用启发式语言检测纠正 - let correctedLanguage = self.correctLanguageDetection(rawLanguage: rawLanguage ?? "", text: result.text ?? "") - detectedLanguage = correctedLanguage.isEmpty ? (self.supportedLanguages.first ?? self.currentLanguage) : correctedLanguage - print("[DEBUG] 识别完成 - 纠正后语言: \(detectedLanguage)") - } catch { - print("[DEBUG] 识别完成 - 语言检测异常: \(error.localizedDescription)") - detectedLanguage = self.supportedLanguages.first ?? self.currentLanguage - } - } else { - detectedLanguage = self.currentLanguage - print("[DEBUG] 识别完成 - 使用当前语言: \(detectedLanguage)") - } - print("识别完成事件: \(detectedLanguage)") - DispatchQueue.main.async { - callback.onResult(result.text ?? "", detectedLanguage) - } + guard result.reason == .recognizedSpeech, + !(result.text?.isEmpty ?? true) else { return } + + let detectedLanguage = self.getDetectedLanguage(from: result) + DispatchQueue.main.async { + callback.onResult(result.text ?? "", detectedLanguage) } } - // 会话开始事件 recognizer.addSessionStartedEventHandler { (sender, event) in // 直接在当前线程调用回调 @@ -532,15 +588,23 @@ public class AzureAsrHelper: NSObject { /** * 初始化 */ - public func initAudioRecord() { + /** + * 初始化音频录制组件 + * 包括音频格式、推流、音频引擎等核心组件的初始化 + */ + public func initAudioRecord() { print("初始化了") audioFormat = getOptimalAudioFormat() pushAudioStream = SPXPushAudioInputStream() - isRunning=true + + // 初始化音频引擎 + audioEngine = AVAudioEngine() + + isRunning = true // 创建新的写线程 - writeThread = DispatchQueue(label: "audio.stream.writer") + writeThread = DispatchQueue(label: "audio.stream.writer") startWriteThread() - self.setAudioOutputRoute(.receiver) + self.setAudioOutputRoute(.receiver) } /// 获取最佳音频格式 (iOS 通常支持标准采样率) @@ -585,8 +649,13 @@ public class AzureAsrHelper: NSObject { guard let dataToWrite = self.writeQueue.take() else { continue } print("写入数据长度: \(dataToWrite.count)") - self.pushAudioStream?.write(dataToWrite) - self.onAudioData?(dataToWrite) + do { + try self.pushAudioStream?.write(dataToWrite) + self.onAudioData?(dataToWrite) +} catch { + print("推送音频数据失败: \(error.localizedDescription)") +} + } } } @@ -606,65 +675,92 @@ public class AzureAsrHelper: NSObject { writeQueue.put(data) } - private func runMicrophoneCapture() { - do { - audioEngine = AVAudioEngine() - guard let inputNode = audioEngine?.inputNode else { - throw NSError(domain: "AudioSetup", code: 1) - } - // 推荐直接用系统 format - let hardwareFormat = inputNode.inputFormat(forBus: 0) - // // ===== 新增:启用专业级语音处理 ===== - if #available(iOS 13.0, *) { - try inputNode.setVoiceProcessingEnabled(true) - print("Voice processing enabled") - - } - - - // Create converter to target format - guard let targetFormat = audioFormat, - let converter = AVAudioConverter(from: hardwareFormat, to: targetFormat) else { - throw NSError(domain: "AudioSetup", code: 2) - } - - inputNode.installTap(onBus: 0, bufferSize: UInt32(bufferSize), format: hardwareFormat) { - [weak self] buffer, time in - guard let self = self, self.isWriting else { return } - - // Convert to target format - let convertedBuffer = AVAudioPCMBuffer( - pcmFormat: targetFormat, - frameCapacity: AVAudioFrameCount(targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate) - )! - ///print("进入 tap 回调,frameLength: \(buffer.frameLength)") - var error: NSError? - let status = converter.convert( - to: convertedBuffer, - error: &error, - withInputFrom: { inNumPackets, outStatus in - outStatus.pointee = .haveData - return buffer - } - ) - //print("转换状态: \(status.rawValue), 错误: \(String(describing: error))") - - if status == .haveData, error == nil { - let data = self.audioBufferToData(convertedBuffer) - // print("准备写入数据,大小: \(data.count)") - - self.writeQueue.put(data) - - } + // 私有方法:启动麦克风捕获 +/** + * 启动麦克风捕获 + * 优化:复用已初始化的音频引擎,减少启动延迟 + */ +private func runMicrophoneCapture() { + do { + // 【优化】检查音频引擎是否已初始化,避免重复创建 + if audioEngine == nil { + audioEngine = AVAudioEngine() + } + + // 如果音频引擎正在运行,先停止 + if audioEngine?.isRunning == true { + audioEngine?.stop() + } + + // 获取音频输入节点(麦克风) + guard let inputNode = audioEngine?.inputNode else { + throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "无法获取音频输入节点"]) + } + + // 移除之前的音频处理块,避免重复添加 + inputNode.removeTap(onBus: 0) + + // 获取硬件支持的原始音频格式 + let hardwareFormat = inputNode.inputFormat(forBus: 0) + + // iOS 13+ 启用语音处理 + if #available(iOS 13.0, *) { + try inputNode.setVoiceProcessingEnabled(true) + } + + // 检查目标音频格式和转换器是否可用 + guard let targetFormat = audioFormat, // 外部定义的期望音频格式 + let converter = AVAudioConverter(from: hardwareFormat, to: targetFormat) else { + throw NSError(domain: "AudioSetup", code: 2) + } + + // 在输入节点上安装录音回调 + inputNode.installTap(onBus: 0, + bufferSize: UInt32(bufferSize), // 每次回调的缓冲区大小 + format: hardwareFormat) { // 使用原始硬件格式 + [weak self] buffer, time in // 弱引用避免循环引用 + + // 确保实例存在且正在写入状态 + guard let self = self, self.isWriting else { return } + + // 创建目标格式的音频缓冲区 + let convertedBuffer = AVAudioPCMBuffer( + pcmFormat: targetFormat, + // 计算转换后的帧容量(考虑采样率差异) + frameCapacity: AVAudioFrameCount( + targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate + ) + )! + + var error: NSError? + // 执行音频格式转换 + let status = converter.convert( + to: convertedBuffer, + error: &error, + withInputFrom: { inNumPackets, outStatus in + outStatus.pointee = .haveData // 标记有数据可用 + return buffer // 返回原始音频数据 } - // 重新激活 - try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) + ) - try audioEngine?.start() - } catch { - print("麦克风启动失败: \(error)") + // 转换成功且无错误 + if status == .haveData, error == nil { + // 将音频缓冲区转换为二进制数据 + let data = self.audioBufferToData(convertedBuffer) + // 将数据放入写入队列(后续处理) + self.writeQueue.put(data) } } + + // 激活音频会话(允许录音) + try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) + + // 启动音频引擎 + try audioEngine?.start() + } catch { + print("麦克风启动失败: \(error)") + } +} private func audioBufferToData(_ buffer: AVAudioPCMBuffer) -> Data { let frameLength = Int(buffer.frameLength) let channelCount = 1 @@ -870,55 +966,7 @@ public class AzureAsrHelper: NSObject { } } - /** - * 启发式语言检测纠正方法 - * 基于文本内容特征来纠正Azure语音服务的语言检测结果 - */ - private func correctLanguageDetection(rawLanguage: String, text: String) -> String { - // 如果原始检测为空,返回空 - if rawLanguage.isEmpty || text.isEmpty { - return rawLanguage - } - - let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - - // 英文特征检测 - let englishPattern = "^[a-z\\s\\.,!?;:'-]+$" - let englishRegex = try? NSRegularExpression(pattern: englishPattern, options: .caseInsensitive) - let englishMatches = englishRegex?.numberOfMatches(in: trimmedText, options: [], range: NSRange(location: 0, length: trimmedText.count)) ?? 0 - - // 中文特征检测 - let chinesePattern = "[\\u4e00-\\u9fff]" - let chineseRegex = try? NSRegularExpression(pattern: chinesePattern, options: []) - let chineseMatches = chineseRegex?.numberOfMatches(in: trimmedText, options: [], range: NSRange(location: 0, length: trimmedText.count)) ?? 0 - - // 常见英文单词列表 - let commonEnglishWords = ["hello", "hi", "please", "thank", "you", "yes", "no", "good", "morning", "afternoon", "evening", "night", "how", "are", "what", "where", "when", "why", "who", "can", "could", "would", "should", "will", "the", "and", "or", "but", "if", "then", "this", "that", "these", "those"] - - let hasCommonEnglishWords = commonEnglishWords.contains { word in - trimmedText.contains(word) - } - - print("[DEBUG] 语言纠正分析 - 文本: '\(trimmedText)', 英文匹配: \(englishMatches > 0), 中文字符: \(chineseMatches), 常见英文词: \(hasCommonEnglishWords), 原始语言: \(rawLanguage)") - - // 纠正逻辑 - if rawLanguage == "zh-CN" { - // 如果Azure检测为中文,但文本明显是英文特征 - if (englishMatches > 0 && chineseMatches == 0) || hasCommonEnglishWords { - print("[DEBUG] 纠正: \(rawLanguage) -> en-US (检测到英文特征)") - return "en-US" - } - } else if rawLanguage == "en-US" { - // 如果Azure检测为英文,但包含中文字符 - if chineseMatches > 0 { - print("[DEBUG] 纠正: \(rawLanguage) -> zh-CN (检测到中文字符)") - return "zh-CN" - } - } - - // 如果没有明确的纠正理由,保持原始检测结果 - return rawLanguage - } + // MARK: - 回调函数 /**