Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

weicu
liwei1dao 1 year ago
parent
commit
f86423dd13
  1. 13
      lib/core/translations/language/en_us.dart
  2. 9
      lib/core/translations/language/zh_cn.dart
  3. 6
      lib/data/services/novice_guide_service.dart
  4. 26
      lib/modules/agent/views/agent_view.dart
  5. 56
      lib/modules/pairing/controllers/pairing_controller.dart
  6. 97
      lib/modules/settings/views/settings_view.dart
  7. 156
      lib/modules/splash/controllers/splash_controller.dart
  8. 180
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  9. 214
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/LanguageDetector.swift

13
lib/core/translations/language/en_us.dart

@ -813,6 +813,19 @@ const Map<String, String> enUS = {
'deviceAuthorizationFailedCheckCode':
'Device authorization failed, please check authorization code',
///iOS蓝牙引导窗
'bluetoothGuideTitle': 'Manual Bluetooth Activation Required', // 需要手动开启蓝牙
'bluetoothGuideSteps': 'Please follow these steps:', // 请按照以下步骤操作:
'bluetoothGuideStep1':
'Enable Bluetooth: Settings → Bluetooth → Turn On', // 开启蓝牙:设置 → 蓝牙 → 开启
'bluetoothGuideStep2':
'Connect Device: Find your device in "My Devices" and tap to connect', // 连接设备:在"我的设备"中找到您的设备并点击连接
'bluetoothGuideStep3':
'Return to app to continue pairing process', // 返回应用继续配对流程
'bluetoothGuideTip':
'Tip: You can also long press the Bluetooth icon in Control Center to quickly access Bluetooth settings', // 提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置
'iKnow': 'I Know', // 我知道了
// 用户反馈页
"feedback": "Feedback",
'feedbackInfo': 'Provide feedback or suggestions',

9
lib/core/translations/language/zh_cn.dart

@ -746,6 +746,15 @@ const Map<String, String> zhCN = {
'bleConnectionFailed': 'BLE连接失败',
'deviceAuthorizationFailedCheckCode': '设备授权失败,请检查授权码',
///iOS蓝牙引导窗
'bluetoothGuideTitle': '需要手动开启蓝牙', // 需要手动开启蓝牙
'bluetoothGuideSteps': '请按照以下步骤操作:', // 请按照以下步骤操作:
'bluetoothGuideStep1': '开启蓝牙:设置 → 蓝牙 → 开启', // 开启蓝牙:设置 → 蓝牙 → 开启
'bluetoothGuideStep2': '连接设备:在"我的设备"中找到您的设备并点击连接', // 连接设备:在"我的设备"中找到您的设备并点击连接
'bluetoothGuideStep3': '返回应用继续配对流程', // 返回应用继续配对流程
'bluetoothGuideTip': '提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置', // 提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置
'iKnow': '我知道了', // 我知道了
//用户反馈页
"feedback": "用户反馈",
'feedbackInfo': '提供反馈或建议',

6
lib/data/services/novice_guide_service.dart

@ -5,8 +5,8 @@ import 'package:get_storage/get_storage.dart';
class NoviceGuideService extends GetxService {
final GetStorage _storage = GetStorage();
bool isHomeGuide = false;
bool isAIGuide = false;
bool isHomeGuide = true;
bool isAIGuide = true;
@override
void onInit() {
@ -25,7 +25,7 @@ class NoviceGuideService extends GetxService {
void startAIGuide() async {
if (!isAIGuide) {
await Future.delayed(const Duration(milliseconds: 100));
await Future.delayed(const Duration(milliseconds: 200));
isAIGuide = true;
_storage.write('ai_guide', true);
Intro.of(Get.context!).start(group: 'ai');

26
lib/modules/agent/views/agent_view.dart

@ -934,21 +934,19 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
Widget _buildPhoneCallOverlay(BuildContext context, bool isDarkMode) {
return Container(
color: Colors.transparent, // 设置整个覆盖层为透明
child: SafeArea(
child: Column(
children: [
// 中间空白区域 - 完全透明
const Expanded(
child: SizedBox(), // 空的占位符,完全透明
),
child: Column(
children: [
// 中间空白区域 - 完全透明
const Expanded(
child: SizedBox(), // 空的占位符,完全透明
),
// 底部控制按钮 - 包含音频波纹
Container(
color: isDarkMode ? Colors.black : Colors.white,
child: _buildPhoneCallControls(isDarkMode),
),
],
),
// 底部控制按钮 - 包含音频波纹
Container(
color: isDarkMode ? Colors.black : Colors.white,
child: _buildPhoneCallControls(isDarkMode),
),
],
),
);
}

56
lib/modules/pairing/controllers/pairing_controller.dart

@ -1,12 +1,10 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:app_settings/app_settings.dart';
import 'package:voitrans/data/models/appconfig_model.dart';
import '/data/models/appconfig.dart';
import '/data/models/user_Info.dart';
import '/data/services/network/api.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'dart:async';
import 'package:permission_handler/permission_handler.dart';
@ -16,7 +14,6 @@ import 'dart:io';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart';
import '../../../core/utils/permission_util.dart';
import 'package:url_launcher/url_launcher.dart';
/// 配对状态枚举 - 简化版本
enum PairingState {
@ -1028,52 +1025,65 @@ class PairingController extends GetxController {
void _showBluetoothGuideDialog() {
Get.dialog(
AlertDialog(
title: Text('需要手动开启蓝牙'),
title: Text('bluetoothGuideTitle'.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('请按照以下步骤操作:'),
SizedBox(height: 16),
Text('bluetoothGuideSteps'.tr),
SizedBox(height: 16.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('1. ', style: TextStyle(fontWeight: FontWeight.bold)),
Expanded(child: Text('开启蓝牙:设置 → 蓝牙 → 开启')),
Text('1. ',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 14.sp)),
Expanded(
child: Text('bluetoothGuideStep1'.tr,
style: TextStyle(fontSize: 14.sp))),
],
),
SizedBox(height: 8),
SizedBox(height: 8.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('2. ', style: TextStyle(fontWeight: FontWeight.bold)),
Expanded(child: Text('连接设备:在"我的设备"中找到您的设备并点击连接')),
Text('2. ',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 14.sp)),
Expanded(
child: Text('bluetoothGuideStep2'.tr,
style: TextStyle(fontSize: 14.sp))),
],
),
SizedBox(height: 8),
SizedBox(height: 8.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('3. ', style: TextStyle(fontWeight: FontWeight.bold)),
Expanded(child: Text('返回应用继续配对流程')),
Text('3. ',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 14.sp)),
Expanded(
child: Text('bluetoothGuideStep3'.tr,
style: TextStyle(fontSize: 14.sp))),
],
),
SizedBox(height: 16),
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.all(12),
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.blue, size: 20),
SizedBox(width: 8),
Icon(Icons.info_outline, color: Colors.blue, size: 20.sp),
SizedBox(width: 8.w),
Expanded(
child: Text(
'提示:也可以从控制中心长按蓝牙图标快速进入蓝牙设置',
style: TextStyle(fontSize: 12, color: Colors.blue[700]),
'bluetoothGuideTip'.tr,
style:
TextStyle(fontSize: 12.sp, color: Colors.blue[700]),
),
),
],
@ -1084,7 +1094,7 @@ class PairingController extends GetxController {
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('我知道了'),
child: Text('iKnow'.tr, style: TextStyle(fontSize: 14.sp)),
),
],
),

97
lib/modules/settings/views/settings_view.dart

@ -407,8 +407,6 @@ class SettingsView extends GetView<SettingsController> {
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 用户反馈
_buildSimpleNavigationSetting(
title: 'feedback'.tr, // 用户反馈
@ -427,29 +425,33 @@ class SettingsView extends GetView<SettingsController> {
},
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
if (Platform.isAndroid) ...[
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 隐私设置
_buildSimpleNavigationSetting(
title: 'privacySettings'.tr, // 隐私设置
subtitle: 'privacySettingsInfo'.tr, // 管理数据收集和权限
icon: Icons.shield_outlined,
iconBgColor: isDarkMode
? Colors.purple[900]!.withOpacity(0.3)
: Colors.purple[100]!,
iconColor: isDarkMode
? Colors.purple[300]!
: Colors.purple[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
AppSettings.openAppSettings();
},
isDarkMode: isDarkMode,
)
],
// 隐私设置
_buildSimpleNavigationSetting(
title: 'privacySettings'.tr, // 隐私设置
subtitle: 'privacySettingsInfo'.tr, // 管理数据收集和权限
icon: Icons.shield_outlined,
iconBgColor: isDarkMode
? Colors.purple[900]!.withOpacity(0.3)
: Colors.purple[100]!,
iconColor:
isDarkMode ? Colors.purple[300]! : Colors.purple[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
AppSettings.openAppSettings();
},
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode
@ -597,29 +599,32 @@ class SettingsView extends GetView<SettingsController> {
onTap: () => Get.toNamed(Routes.OTA_UPGRADE),
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
if (Platform.isAndroid) ...[
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 添加权限管理设置项
_buildSimpleNavigationSetting(
title: 'permissionManagement'.tr, // 权限管理
subtitle: 'permissionManagementInfo'.tr, // 管理应用权限设置
icon: Icons.security,
iconBgColor: isDarkMode
? Colors.orange[900]!.withOpacity(0.3)
: Colors.orange[100]!,
iconColor:
isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.permissions);
},
isDarkMode: isDarkMode,
),
// 添加权限管理设置项
_buildSimpleNavigationSetting(
title: 'permissionManagement'.tr, // 权限管理
subtitle: 'permissionManagementInfo'.tr, // 管理应用权限设置
icon: Icons.security,
iconBgColor: isDarkMode
? Colors.orange[900]!.withOpacity(0.3)
: Colors.orange[100]!,
iconColor: isDarkMode
? Colors.orange[300]!
: Colors.orange[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.permissions);
},
isDarkMode: isDarkMode,
),
]
],
),
),

156
lib/modules/splash/controllers/splash_controller.dart

@ -24,9 +24,16 @@ class SplashController extends GetxController {
final _loadingProgress = 0.0.obs;
double get loadingProgress => _loadingProgress.value;
// 资源加载状态
final _isResourceLoaded = false.obs;
bool get isResourceLoaded => _isResourceLoaded.value;
// 各项任务完成状态
final _isTrackingPermissionRequested = false.obs; // 追踪权限请求完成
final _isVersionCheckCompleted = false.obs; // 版本检查完成
final _isTokenValidated = false.obs; // Token验证完成
final _isAppConfigLoaded = false.obs; // 应用配置加载完成
final _isAnimationCompleted = false.obs; // 动画完成
// 登录状态
final _isLoggedIn = false.obs;
bool get isLoggedIn => _isLoggedIn.value;
final _hasAcceptedPrivacy = false.obs; //是否同意隐私政策
@ -59,38 +66,45 @@ class SplashController extends GetxController {
}
});
// 异步处理用户追踪授权,不阻塞主流程
_requestTrackingPermissionAsync();
// 注意:追踪权限请求将在用户同意隐私政策后执行
}
/// 异步请求追踪权限,不阻塞启动流程
void _requestTrackingPermissionAsync() {
Future.delayed(const Duration(milliseconds: 500), () async {
try {
await AppTrackingTransparency.requestTrackingAuthorization();
Logger.d(_tag, '用户追踪权限请求完成');
} catch (e) {
Logger.e(_tag, '用户追踪权限请求失败: $e');
}
});
/// 异步请求追踪权限
Future<void> _requestTrackingPermissionAsync() async {
try {
await AppTrackingTransparency.requestTrackingAuthorization();
Logger.d(_tag, '用户追踪权限请求完成');
} catch (e) {
Logger.e(_tag, '用户追踪权限请求失败: $e');
} finally {
_isTrackingPermissionRequested.value = true;
}
}
void _startNormalFlow() {
_startLoadingAnimation();
_loadAppResources();
_startParallelTasks();
}
/// 智能调度版本检查,在页面跳转后执行,完全不影响启动流程
void _scheduleVersionCheck() {
// 在独立的微任务中执行,避免阻塞主线程
scheduleMicrotask(() async {
try {
Logger.d(_tag, '开始执行版本检查(后台任务)');
_checkVersionUpdateIfNeeded();
} catch (e) {
Logger.e(_tag, '版本检查调度失败: $e');
}
});
/// 启动并行任务:在1.5秒动画期间同时执行多个任务
void _startParallelTasks() {
// 并行执行所有任务
_requestTrackingPermissionAsync();
_performVersionCheckAsync();
_validateTokenAsync();
}
/// 异步执行版本检查
Future<void> _performVersionCheckAsync() async {
try {
Logger.d(_tag, '开始执行版本检查');
await _checkVersionUpdateIfNeeded();
} catch (e) {
Logger.e(_tag, '版本检查失败: $e');
}
//finally {
// _isVersionCheckCompleted.value = true;
// }
}
void _onPrivacyAccepted() {
@ -115,10 +129,10 @@ class SplashController extends GetxController {
);
}
// 进度条动画(持续2秒)
// 进度条动画(持续1.5秒)
void _startLoadingAnimation() {
const totalDuration = 1500; // 动画总时长增加到1.5秒
const steps = totalDuration ~/ 30; // 30的间隔,更平滑的动画
const totalDuration = 1500; // 动画总时长1.5秒
const steps = totalDuration ~/ 30; // 30ms间隔,更平滑的动画
const stepValue = 1.0 / steps;
Timer.periodic(const Duration(milliseconds: 30), (timer) {
@ -126,54 +140,53 @@ class SplashController extends GetxController {
_loadingProgress.value += stepValue;
} else {
timer.cancel();
_isAnimationCompleted.value = true;
_checkNavigateCondition();
}
});
}
// 异步加载应用资源
Future<void> _loadAppResources() async {
/// 异步验证Token和加载应用配置
Future<void> _validateTokenAsync() async {
try {
// 模拟资源加载(如API配置、本地缓存等)
await Future.delayed(const Duration(milliseconds: 1500));
// 实际资源加载代码
// await _loadImages();
// await _initDatabase();
bool isLoggedIn = false;
final token = _storage.read('logintoken');
Logger.d(_tag, 'Token验证开始: $token');
if (token != null && token.isNotEmpty) {
isLoggedIn = await getLoginToken();
if (isLoggedIn) {
await getAppConfig();
_isAppConfigLoaded.value = true;
}
}
_isResourceLoaded.value = true;
_checkNavigateCondition();
_isLoggedIn.value = isLoggedIn;
Logger.d(_tag, 'Token验证完成,登录状态: $isLoggedIn');
} catch (e) {
Get.snackbar('错误', '资源加载失败,请检查网络连接');
_isResourceLoaded.value = true; // 允许用户继续使用
Logger.e(_tag, '资源加载错误: $e');
Logger.e(_tag, 'Token验证失败: $e');
_isLoggedIn.value = false;
} finally {
_isTokenValidated.value = true;
}
}
// 检查跳转条件
void _checkNavigateCondition() async {
if (_loadingProgress.value >= 1.0 && _isResourceLoaded.value) {
// 关键改动:先执行页面跳转,并用 await 等待它完成
await _navigateToLogin();
// 页面跳转完全结束后,再安心地安排版本检查
_scheduleVersionCheck();
// 检查跳转条件:所有任务都完成后才跳转
void _checkNavigateCondition() {
if (_isAnimationCompleted.value &&
_isTrackingPermissionRequested.value &&
_isVersionCheckCompleted.value &&
_isTokenValidated.value) {
_navigateToTargetPage();
}
}
// 获取登录Token并跳转
Future<void> _navigateToLogin() async {
bool isLoggedIn = false;
final token = _storage.read('logintoken');
Logger.d(_tag, 'getLoginToken:进来了${token}');
if (token != null && token.isNotEmpty) {
isLoggedIn = await getLoginToken();
}
if (isLoggedIn) {
await getAppConfig();
// 根据登录状态跳转到目标页面
void _navigateToTargetPage() {
if (_isLoggedIn.value && _isAppConfigLoaded.value) {
Get.offAllNamed(Routes.pairing);
} else {
Get.offAllNamed(Routes.login); // 跳转到登录页
Get.offAllNamed(Routes.login);
}
}
@ -227,7 +240,7 @@ class SplashController extends GetxController {
/// 混合检查机制:判断是否需要进行版本检查
/// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时
void _checkVersionUpdateIfNeeded() {
Future<void> _checkVersionUpdateIfNeeded() async {
try {
final now = DateTime.now();
final lastCheckTimestamp = _storage.read(_lastVersionCheckKey);
@ -259,7 +272,7 @@ class SplashController extends GetxController {
if (shouldCheck) {
Logger.i(_tag, '开始版本检查 - 原因: $reason');
_performVersionCheck();
await _performVersionCheck();
} else {
Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔');
}
@ -278,9 +291,14 @@ class SplashController extends GetxController {
Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}');
// 在主线程中显示更新对话框
WidgetsBinding.instance.addPostFrameCallback((_) {
VersionUpdateDialog.show(updateInfo);
VersionUpdateDialog.show(updateInfo).then((_) {
// 无论用户选择更新还是稍后提醒,都标记版本检查完成
_isVersionCheckCompleted.value = true;
_checkNavigateCondition();
});
});
} else {
_isVersionCheckCompleted.value = true;
Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}');
}
@ -288,12 +306,16 @@ class SplashController extends GetxController {
_updateVersionCheckRecord();
}).catchError((e) {
Logger.e(_tag, '版本检查失败: $e');
// 即使检查失败,也更新记录,避免频繁重试
// 即使检查失败,也标记版本检查完成,让流程继续
_isVersionCheckCompleted.value = true;
// 更新检查记录,避免频繁重试
_updateVersionCheckRecord();
});
} catch (e) {
Logger.e(_tag, '版本检查失败: $e');
// 即使检查失败,也更新记录,避免频繁重试
// 即使检查失败,也标记版本检查完成,让流程继续
_isVersionCheckCompleted.value = true;
// 更新检查记录,避免频繁重试
_updateVersionCheckRecord();
}
}

180
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift

@ -99,22 +99,28 @@ public class AzureAsrHelper: NSObject {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region)
// 针对翻译场景的性能优化
optimizeForTranslation()
if isAutoDetectLanguage {
// 移除过时的属性设置方式
// speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode")
speechConfig?.setPropertyTo(supportedLanguages.joined(separator: ","), byName: "SpeechServiceConnection_AutoDetectSourceLanguages")
// 针对翻译场景优化的语言检测配置
speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode")
// 设置语言检测的置信度阈值,提高准确性
speechConfig?.setPropertyTo("0.7", byName: "SpeechServiceConnection_AutoDetectSourceLanguageConfidenceThreshold")
// 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理
os_log("启用语言检测模式,支持语言: %{public}@", log: log, type: .info, supportedLanguages.joined(separator: ","))
} else {
speechConfig?.speechRecognitionLanguage = currentLanguage
os_log("固定语言模式,当前语言: %{public}@", log: log, type: .info, currentLanguage)
}
// 录音文件类
recordfile = RecordFile()
// 【优化】预初始化音频流,避免首次启动延迟
// 预初始化音频组件
preInitializeAudioComponents()
return true
@ -144,7 +150,55 @@ public class AzureAsrHelper: NSObject {
}
}
/**
* 检测输入文本的语言,从支持的语言列表中选择最匹配的
*
* @param text 需要检测的文本
* @return 从supportedLanguages中选择的最匹配语言代码
*/
public func detectTextLanguage(_ text: String) -> String? {
// 如果只有一种支持的语言,直接返回
if supportedLanguages.count == 1 {
return supportedLanguages[0]
}
// 获取所有可能的语言及其置信度
let languageResults = LanguageDetector.detectLanguageWithConfidence(from: text)
// 遍历检测结果,找到第一个在supportedLanguages中的语言
for result in languageResults {
let azureLanguageCode = LanguageDetector.convertToAzureLanguageCode(result.language)
if supportedLanguages.contains(azureLanguageCode) {
return azureLanguageCode
}
}
// 如果没有找到匹配的语言,返回第一个支持的语言作为默认值
return supportedLanguages.first
}
/**
* 检测文本语言并返回详细信息
*
* @param text 需要检测的文本
* @return 包含语言代码、置信度和语言名称的字典
*/
public func detectTextLanguageWithDetails(_ text: String) -> [String: Any]? {
let results = LanguageDetector.detectLanguageWithConfidence(from: text)
guard let topResult = results.first else {
return nil
}
let azureLanguageCode = LanguageDetector.convertToAzureLanguageCode(topResult.language)
let languageName = LanguageDetector.getLanguageName(from: topResult.language)
return [
"languageCode": azureLanguageCode,
"confidence": topResult.confidence,
"languageName": languageName,
"originalCode": topResult.language
]
}
/**
* 开始连续语音识别
*
@ -156,11 +210,13 @@ public class AzureAsrHelper: NSObject {
audioSourceType: AudioSourceType = .microphone
) -> Bool {
guard speechConfig != nil else {
print("语音服务未初始化")
//callback.onError("语音服务未初始化")
return false
}
if _isContinuousRecognitionActive {
print("连续识别已在进行中")
return true
}
@ -170,6 +226,7 @@ public class AzureAsrHelper: NSObject {
do {
guard recognizer != nil else {
if !setupRecognizer() {
print("重置识别器失败")
// callback.onError("重置识别器失败")
return false
}
@ -312,7 +369,7 @@ public class AzureAsrHelper: NSObject {
*/
private func setupRecognizer() -> Bool {
do {
print("设置识别器${_isContinuousRecognitionActive}")
print("设置识别器\(_isContinuousRecognitionActive)")
// 如果正在进行连续识别,先停止
if _isContinuousRecognitionActive {
@ -332,6 +389,7 @@ public class AzureAsrHelper: NSObject {
if isAutoDetectLanguage {
// 使用正确的初始化方法
let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages)
print("autoDetectConfig: \(autoDetectConfig)")
recognizer = try SPXSpeechRecognizer(
speechConfiguration: speechConfig!,
autoDetectSourceLanguageConfiguration: autoDetectConfig,
@ -425,6 +483,94 @@ public class AzureAsrHelper: NSObject {
print("无法检测语言,使用默认语言: \(currentLanguage)")
return currentLanguage
}
/**
* 针对翻译场景的性能优化配置
*/
private func optimizeForTranslation() {
guard let speechConfig = speechConfig else { return }
// 启用实时转录,减少延迟
speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableAudioLogging")
speechConfig.setPropertyTo("INTERACTIVE", byName: "SpeechServiceConnection_RecoMode")
speechConfig.setPropertyTo("PCM", byName: "Speech_PushStreamFormat")
// 优化语音端点检测,适合翻译场景
speechConfig.setPropertyTo("300", byName: "SpeechServiceConnection_InitialSilenceTimeoutMs")
speechConfig.setPropertyTo("500", byName: "SpeechServiceConnection_EndSilenceTimeoutMs")
speechConfig.setPropertyTo("500", byName: "Speech_SegmentationSilenceTimeoutMs")
// 启用标点符号,提高翻译质量
speechConfig.setPropertyTo("true", byName: "SpeechServiceResponse_RequestProfanityFilterTrueFalse")
// 设置分段策略为时间模式
speechConfig.setPropertyTo("Time", byName: "Speech_SegmentationStrategy")
// 【优化】语言检测配置 - 减少检测延迟,实现快速响应
if isAutoDetectLanguage {
// 降低语言检测置信度阈值,提高检测灵敏度
speechConfig.setPropertyTo("0.3", byName: "SpeechServiceConnection_AutoDetectSourceLanguageConfidenceThreshold")
// 使用Continuous模式,在每句话结束后重新检测语言
speechConfig.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode")
// 启用快速语言检测
speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableLanguageDetectionFast")
// 【关键优化】设置语言检测的最小音频长度为100毫秒,实现快速响应
speechConfig.setPropertyTo("100", byName: "SpeechServiceConnection_LanguageDetectionMinAudioLengthMs")
// 【新增】设置语言检测的最大等待时间,避免长时间等待
speechConfig.setPropertyTo("2000", byName: "SpeechServiceConnection_LanguageDetectionMaxWaitTimeMs")
// 强制在每个语音段开始时重新检测语言
speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_ForceLanguageDetectionOnNewSegment")
// 【新增】启用句子级别的语言检测
speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableLanguageDetectionPerSentence")
// 【新增】设置语言检测的频率 - 每个识别结果后都检测
speechConfig.setPropertyTo("PerResult", byName: "SpeechServiceConnection_LanguageDetectionFrequency")
// 【新增】启用实时语言检测,减少缓冲延迟
speechConfig.setPropertyTo("true", byName: "SpeechServiceConnection_EnableRealtimeLanguageDetection")
// 【新增】设置语言检测的采样间隔为200毫秒
speechConfig.setPropertyTo("200", byName: "SpeechServiceConnection_LanguageDetectionSamplingIntervalMs")
}
os_log("翻译场景优化配置已应用,包含语言检测优化", log: log, type: .info)
}
/**
* 动态切换语言(翻译场景中的语言切换)
*
* @param newSourceLanguage 新的源语言
* @param newTargetLanguage 新的目标语言
* @return 切换是否成功
*/
public func switchTranslationLanguages(
newSourceLanguage: String,
newTargetLanguage: String
) -> Bool {
let validatedSourceLang = LanguageDetector.convertToAzureLanguageCode(newSourceLanguage)
let validatedTargetLang = LanguageDetector.convertToAzureLanguageCode(newTargetLanguage)
// 更新支持的语言
self.supportedLanguages = [validatedSourceLang, validatedTargetLang]
self.currentLanguage = validatedSourceLang
// 重新配置语言检测
self.isAutoDetectLanguage = (validatedSourceLang != validatedTargetLang)
// 如果正在识别,需要重新设置识别器
if _isContinuousRecognitionActive {
let wasActive = stopContinuousRecognition()
if wasActive {
return setupRecognizer()
}
}
os_log("语言已切换: %{public}@ -> %{public}@", log: log, type: .info, validatedSourceLang, validatedTargetLang)
return true
}
/**
* 设置事件监听器
*/
@ -442,7 +588,14 @@ public class AzureAsrHelper: NSObject {
let result = event.result
guard !(result.text?.isEmpty ?? true) else { return }
let detectedLanguage = self.getDetectedLanguage(from: result)
// let detectedLanguage = self.getDetectedLanguage(from: result)
// 检测文本语言,避免重复调用
let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN"
// 输出检测到的语言信息
print("检测到的语言: \(detectedLanguage)")
DispatchQueue.main.async {
callback.onRecognizing(result.text ?? "", detectedLanguage)
}
@ -455,10 +608,17 @@ public class AzureAsrHelper: NSObject {
guard result.reason == .recognizedSpeech,
!(result.text?.isEmpty ?? true) else { return }
let detectedLanguage = self.getDetectedLanguage(from: result)
// 检测文本语言,避免重复调用
let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN"
// 输出检测到的语言信息
print("检测到的语言: \(detectedLanguage)")
// 在主线程回调结果
DispatchQueue.main.async {
callback.onResult(result.text ?? "", detectedLanguage)
}
}
// 会话开始事件
recognizer.addSessionStartedEventHandler { (sender, event) in
@ -472,8 +632,8 @@ public class AzureAsrHelper: NSObject {
guard let self = self else { return }
print("会话结束事件:")
// 直接在当前线程调用回调
// callback.onSessionStopped()
// self._isContinuousRecognitionActive = false
callback.onSessionStopped()
self._isContinuousRecognitionActive = false
//self.stopAudioProcessing()
}

214
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/LanguageDetector.swift

@ -0,0 +1,214 @@
import Foundation
import NaturalLanguage
/**
* 语言检测工具类
* 使用iOS自带的自然语言处理框架检测文本语种
*/
public class LanguageDetector {
/**
* 检测文本的主要语言
*
* @param text 需要检测的文本
* @return 语言代码(如:"zh-Hans", "en", "ja"等),如果检测失败返回nil
*/
public static func detectLanguage(from text: String) -> String? {
// 检查输入文本是否为空
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return nil
}
// 使用NLLanguageRecognizer进行语言检测
let recognizer = NLLanguageRecognizer()
recognizer.processString(text)
// 获取最可能的语言
guard let dominantLanguage = recognizer.dominantLanguage else {
return nil
}
return dominantLanguage.rawValue
}
/**
* 检测文本的语言并返回置信度
*
* @param text 需要检测的文本
* @return 语言代码和置信度的字典,按置信度降序排列
*/
public static func detectLanguageWithConfidence(from text: String) -> [(language: String, confidence: Double)] {
// 检查输入文本是否为空
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return []
}
let recognizer = NLLanguageRecognizer()
recognizer.processString(text)
// 获取语言假设及其置信度
let languageHypotheses = recognizer.languageHypotheses(withMaximum: 5)
// 转换为元组数组并按置信度排序
return languageHypotheses.map { (language: $0.key.rawValue, confidence: $0.value) }
.sorted { $0.confidence > $1.confidence }
}
/**
* 将语言代码转换为人类可读的语言名称
*
* @param languageCode 语言代码(如:"zh-Hans", "en"等)
* @return 语言的本地化名称
*/
public static func getLanguageName(from languageCode: String) -> String {
let locale = Locale.current
return locale.localizedString(forLanguageCode: languageCode) ?? languageCode
}
/**
* 检测文本语言并返回Azure语音服务支持的语言代码格式
*
* @param text 需要检测的文本
* @return Azure支持的语言代码格式(如:"zh-CN", "en-US"等)
*/
public static func detectLanguageForAzure(from text: String) -> String? {
guard let detectedLanguage = detectLanguage(from: text) else {
return nil
}
// 将iOS语言代码转换为Azure语音服务支持的格式
return convertToAzureLanguageCode(detectedLanguage)
}
/**
* 将iOS语言代码转换为Azure语音服务支持的语言代码
*
* @param iosLanguageCode iOS检测到的语言代码
* @return Azure支持的语言代码
*/
public static func convertToAzureLanguageCode(_ iosLanguageCode: String) -> String {
let languageMapping: [String: String] = [
// 中文
"zh-Hans": "zh-CN", // 简体中文
"zh-Hant": "zh-TW", // 繁体中文
"zh-HK": "zh-HK", // 香港中文
// 英语系
"en": "en-US", // 英语(美国)
"en-GB": "en-US", // 英语(英国)
"en-AU": "en-US", // 英语(澳大利亚)
"en-CA": "en-US", // 英语(加拿大)
"en-IN": "en-US", // 英语(印度)
"en-IE": "en-US", // 英语(爱尔兰)
"en-NZ": "en-US", // 英语(新西兰)
"en-ZA": "en-US", // 英语(南非)
// 亚洲语言
"ja": "ja-JP", // 日语
"ko": "ko-KR", // 韩语
"hi": "hi-IN", // 印地语
"th": "th-TH", // 泰语
"vi": "vi-VN", // 越南语
"id": "id-ID", // 印尼语
"ms": "ms-MY", // 马来语
"tl": "fil-PH", // 菲律宾语
"my": "my-MM", // 缅甸语
"km": "km-KH", // 高棉语(柬埔寨)
"lo": "lo-LA", // 老挝语
"si": "si-LK", // 僧伽罗语(斯里兰卡)
"ta": "ta-IN", // 泰米尔语
"te": "te-IN", // 泰卢固语
"bn": "bn-IN", // 孟加拉语
"gu": "gu-IN", // 古吉拉特语
"kn": "kn-IN", // 卡纳达语
"ml": "ml-IN", // 马拉雅拉姆语
"mr": "mr-IN", // 马拉地语
"pa": "pa-IN", // 旁遮普语
"ur": "ur-PK", // 乌尔都语
// 欧洲语言
"fr": "fr-FR", // 法语(法国)
"fr-CA": "fr-CA", // 法语(加拿大)
"fr-BE": "fr-BE", // 法语(比利时)
"fr-CH": "fr-CH", // 法语(瑞士)
"de": "de-DE", // 德语(德国)
"de-AT": "de-AT", // 德语(奥地利)
"de-CH": "de-CH", // 德语(瑞士)
"es": "es-ES", // 西班牙语(西班牙)
"es-MX": "es-MX", // 西班牙语(墨西哥)
"es-AR": "es-AR", // 西班牙语(阿根廷)
"es-CO": "es-CO", // 西班牙语(哥伦比亚)
"es-CL": "es-CL", // 西班牙语(智利)
"es-PE": "es-PE", // 西班牙语(秘鲁)
"es-VE": "es-VE", // 西班牙语(委内瑞拉)
"it": "it-IT", // 意大利语
"pt": "pt-BR", // 葡萄牙语(巴西)
"pt-PT": "pt-PT", // 葡萄牙语(葡萄牙)
"ru": "ru-RU", // 俄语
"nl": "nl-NL", // 荷兰语
"nl-BE": "nl-BE", // 荷兰语(比利时)
"sv": "sv-SE", // 瑞典语
"da": "da-DK", // 丹麦语
"no": "nb-NO", // 挪威语
"fi": "fi-FI", // 芬兰语
"is": "is-IS", // 冰岛语
"pl": "pl-PL", // 波兰语
"cs": "cs-CZ", // 捷克语
"sk": "sk-SK", // 斯洛伐克语
"hu": "hu-HU", // 匈牙利语
"ro": "ro-RO", // 罗马尼亚语
"bg": "bg-BG", // 保加利亚语
"hr": "hr-HR", // 克罗地亚语
"sr": "sr-RS", // 塞尔维亚语
"sl": "sl-SI", // 斯洛文尼亚语
"et": "et-EE", // 爱沙尼亚语
"lv": "lv-LV", // 拉脱维亚语
"lt": "lt-LT", // 立陶宛语
"uk": "uk-UA", // 乌克兰语
"be": "be-BY", // 白俄罗斯语
"mk": "mk-MK", // 马其顿语
"sq": "sq-AL", // 阿尔巴尼亚语
"mt": "mt-MT", // 马耳他语
"ga": "ga-IE", // 爱尔兰语
"cy": "cy-GB", // 威尔士语
"eu": "eu-ES", // 巴斯克语
"ca": "ca-ES", // 加泰罗尼亚语
"gl": "gl-ES", // 加利西亚语
// 中东和非洲语言
"ar": "ar-SA", // 阿拉伯语(沙特阿拉伯)
"ar-EG": "ar-EG", // 阿拉伯语(埃及)
"ar-AE": "ar-AE", // 阿拉伯语(阿联酋)
"ar-BH": "ar-BH", // 阿拉伯语(巴林)
"ar-DZ": "ar-DZ", // 阿拉伯语(阿尔及利亚)
"ar-IQ": "ar-IQ", // 阿拉伯语(伊拉克)
"ar-JO": "ar-JO", // 阿拉伯语(约旦)
"ar-KW": "ar-KW", // 阿拉伯语(科威特)
"ar-LB": "ar-LB", // 阿拉伯语(黎巴嫩)
"ar-LY": "ar-LY", // 阿拉伯语(利比亚)
"ar-MA": "ar-MA", // 阿拉伯语(摩洛哥)
"ar-OM": "ar-OM", // 阿拉伯语(阿曼)
"ar-QA": "ar-QA", // 阿拉伯语(卡塔尔)
"ar-SY": "ar-SY", // 阿拉伯语(叙利亚)
"ar-TN": "ar-TN", // 阿拉伯语(突尼斯)
"ar-YE": "ar-YE", // 阿拉伯语(也门)
"he": "he-IL", // 希伯来语
"fa": "fa-IR", // 波斯语(伊朗)
"tr": "tr-TR", // 土耳其语
"az": "az-AZ", // 阿塞拜疆语
"ka": "ka-GE", // 格鲁吉亚语
"hy": "hy-AM", // 亚美尼亚语
"sw": "sw-KE", // 斯瓦希里语
"am": "am-ET", // 阿姆哈拉语(埃塞俄比亚)
"zu": "zu-ZA", // 祖鲁语(南非)
"af": "af-ZA", // 南非荷兰语
// 其他语言
"el": "el-GR", // 希腊语
"eo": "eo", // 世界语
"la": "la", // 拉丁语
]
return languageMapping[iosLanguageCode] ?? iosLanguageCode
}
}
Loading…
Cancel
Save