Browse Source

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

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
def4043b68
  1. 72
      lib/core/widgets/intro_overlay_builder.dart
  2. 15
      lib/core/widgets/select_location_map.dart
  3. 11
      lib/data/models/appconfig.dart
  4. 22
      lib/data/models/user_Info.dart
  5. 44
      lib/data/services/novice_guide_service.dart
  6. 42
      lib/data/services/version_update_service.dart
  7. 344
      lib/modules/agent/views/agent_view.dart
  8. 70
      lib/modules/home/views/home_view.dart
  9. 119
      lib/modules/login/controllers/login_controller.dart
  10. 17
      lib/modules/login/views/login_view.dart
  11. 376
      lib/modules/splash/controllers/splash_controller.dart
  12. 13
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  13. 55
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  14. 227
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  15. 59
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift

72
lib/core/widgets/intro_overlay_builder.dart

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class IntroOverlayBuilder {
static Widget aiBuilder(
StepWidgetParams params,
String text, {
String buttonText = '下一步',
bool alignment = true,
Function? onNext,
Function? onFinish,
}) {
return Column(
crossAxisAlignment:
alignment ? CrossAxisAlignment.start : CrossAxisAlignment.end,
children: [
Padding(
padding: EdgeInsets.only(bottom: 10.w),
child: Text(
text,
style: TextStyle(
fontSize: 15.sp,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
Row(
children: [
if (onNext != null)
_button(
buttonText,
() {
params.onNext!();
onNext();
},
),
if (onFinish != null)
_button(
buttonText,
() {
params.onFinish();
onFinish();
},
),
],
),
],
);
}
static Widget _button(String text, Function() onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.white),
),
child: Text(
text,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
),
),
),
);
}
}

15
lib/core/widgets/select_location_map.dart

@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
class SelectLocationMap extends StatefulWidget {
const SelectLocationMap({super.key});
@override
State<SelectLocationMap> createState() => _SelectLocationMapState();
}
class _SelectLocationMapState extends State<SelectLocationMap> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

11
lib/data/models/appconfig.dart

@ -4,9 +4,20 @@ import '../../../data/models/appconfig_model.dart';
class AppConfig {
static late UserGetAppConfigResp config;
// 新增一个私有的静态布尔值来跟踪初始化状态
static bool _isInitialized = false;
/// 检查AppConfig单例是否已经被初始化
/// 外部通过调用此方法来安全地检查状态
static bool isInitialized() {
return _isInitialized;
}
// 可选:提供初始化方法
static void initialize(UserGetAppConfigResp resp) {
config = resp;
_isInitialized = true; // <-- 新增此行
}
//环境变量

22
lib/data/models/user_Info.dart

@ -1,9 +1,15 @@
import 'dart:ffi';
import 'package:get_storage/get_storage.dart';
import '../../../core/utils/logger.dart';
class User {
// 单例实例
static User? _instance;
/// 安全地检查用户是否已经登录(即单例是否已初始化)
static bool isLoggedIn() {
return _instance != null;
}
// 单例访问器(增加空安全保护)
static User get instance {
if (_instance == null) {
@ -12,6 +18,18 @@ class User {
return _instance!;
}
// --- 新增 ---
/// 清理所有与用户会话相关的数据
static Future<void> clearUserSession() async {
final GetStorage storage = GetStorage();
// 1. 清理本地存储
await storage.remove('logintoken');
await storage.remove('user_info');
// 2. 清理内存中的单例
_instance = null;
Logger.i("User", "用户会话数据已安全清理。");
}
// 最终属性(不可变)
String name;
String uid;
@ -72,7 +90,7 @@ class User {
// 修复1:安全获取嵌套的 user 字段
final userData = json['user'] as Map<String, dynamic>?; // 允许空值
final devicesData = json['devices'] as List<dynamic>?; // 允许空值
print("数据获取fronTokenJson${userData} ${devicesData}");
print("数据获取fronTokenJson$userData $devicesData");
// 更新单例实例
return _instance = User._(
token: '',

44
lib/data/services/novice_guide_service.dart

@ -8,6 +8,47 @@ class NoviceGuideService extends GetxService {
bool isHomeGuide = true;
bool isAIGuide = true;
int aiStep = 0;
List aiStepsList = [
{
'start': '对小言说:“今天天气怎么样”',
'input': '今天天气怎么样',
'send': '小言会为你实时为你播报天气',
'end': '小言能实时为你播报天气,试试其他指令吧',
},
{
'start': '对小言说:“请播放一组摇滚乐”',
'input': '请播放一组摇滚乐',
'send': '小言会为你搜寻到一组音乐',
'end': '小言已成功为您搜寻到一组音乐,试试其他指令吧',
},
{
'start': '对小言说:“导航到深圳机场”',
'input': '导航到深圳机场',
'send': '小言会为你规划路线',
'end': '小言已成功为您导航到深圳机场,试试其他指令吧',
},
{
'start': '对小言说:“明天下午3点创建会议提醒”',
'input': '明天下午3点创建会议提醒',
'send': '小言会为你创建会议提醒',
'end': '小言已成功为您创建会议提醒,试试其他指令吧',
},
{
'start': '对小言说:“拨打妈妈的电话”',
'input': '拨打妈妈的电话',
'send': '小言会为你拨打电话',
'end': '小言已成功为您拨打电话,试试其他指令吧',
},
{
'start': '对小言说:“给爸爸发短信说晚上回家吃饭’”',
'input': '给爸爸发短信说晚上回家吃饭’',
'send': '小言会为你发送短信',
'end': '短信已发送,语音操控更便捷',
},
];
@override
void onInit() {
super.onInit();
@ -15,8 +56,9 @@ class NoviceGuideService extends GetxService {
// isAIGuide = _storage.read('ai_guide') ?? false;
}
void startHomeGuide() {
void startHomeGuide() async {
if (!isHomeGuide) {
await Future.delayed(const Duration(milliseconds: 200));
isHomeGuide = true;
_storage.write('home_guide', true);
Intro.of(Get.context!).start(group: 'home');

42
lib/data/services/version_update_service.dart

@ -270,29 +270,29 @@ class VersionUpdateService extends GetxService {
return false;
}
final platformInfo = await getCurrentPlatformInfo();
//final platformInfo = await getCurrentPlatformInfo();
try {
switch (platformInfo.storeType) {
case AppStoreType.appStore:
return await _openAppStore();
case AppStoreType.googlePlay:
return await _openGooglePlay();
case AppStoreType.huawei:
return await _openHuaweiStore();
case AppStoreType.xiaomi:
return await _openXiaomiStore();
case AppStoreType.oppo:
return await _openOppoStore();
case AppStoreType.vivo:
return await _openVivoStore();
case AppStoreType.honor:
return await _openHonorStore();
case AppStoreType.official:
return await _openOfficialDownload(updateInfo.appInfo!.address!);
default:
return await _openOfficialDownload(updateInfo.appInfo!.address!);
}
// switch (platformInfo.storeType) {
// case AppStoreType.appStore:
// return await _openAppStore();
// case AppStoreType.googlePlay:
// return await _openGooglePlay();
// case AppStoreType.huawei:
// return await _openHuaweiStore();
// case AppStoreType.xiaomi:
// return await _openXiaomiStore();
// case AppStoreType.oppo:
// return await _openOppoStore();
// case AppStoreType.vivo:
// return await _openVivoStore();
// case AppStoreType.honor:
// return await _openHonorStore();
// case AppStoreType.official:
// return await _openOfficialDownload(updateInfo.appInfo!.address!);
// default:
return await _openOfficialDownload(updateInfo.appInfo!.address!);
// }
} catch (e) {
_logger.e('执行更新失败: $e');
return false;

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

@ -4,6 +4,7 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:get/get.dart';
import '../../../core/widgets/intro_overlay_builder.dart';
import '../controllers/agent_controller.dart';
import '../controllers/message_action_controller.dart';
import 'message_bubble.dart';
@ -197,47 +198,79 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
children: [
// 聊天内容
Expanded(
child: Obx(
() => Align(
alignment: Alignment.topCenter,
child: ListView.builder(
controller: controller.scrollController,
shrinkWrap: true,
reverse: true,
padding: EdgeInsets.symmetric(
vertical: 16.h, horizontal: 16.w),
itemCount: controller.messages.length +
(controller.hasMoreMessages.value ? 1 : 0),
itemBuilder: (context, index) {
// 如果是最后一项且有更多消息,显示加载指示器
if (index == controller.messages.length &&
controller.hasMoreMessages.value) {
return _buildLoadMoreIndicator();
}
final message = controller
.messages[controller.messages.length - 1 - index];
return Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onLongPressStart:
(LongPressStartDetails details) {
messageActionController.onLongPressMessage(
context, isDarkMode, details, message);
},
child: MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
),
));
child: Stack(
children: [
IntroStepBuilder(
group: 'ai',
order: 3,
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
controller.noviceGuideService.aiStepsList[
controller.noviceGuideService.aiStep]['end'],
onFinish: () async {
if (controller.noviceGuideService.aiStep < 5) {
await Future.delayed(
const Duration(milliseconds: 300));
controller.noviceGuideService.aiStep++;
Intro.of(Get.context!)
.start(group: 'ai', reset: true);
}
},
);
},
builder: (BuildContext context, GlobalKey key) => Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
key: key,
width: double.infinity,
height: 430.w,
),
),
),
),
Obx(
() => Align(
alignment: Alignment.topCenter,
child: ListView.builder(
controller: controller.scrollController,
shrinkWrap: true,
reverse: true,
padding: EdgeInsets.symmetric(
vertical: 16.h, horizontal: 16.w),
itemCount: controller.messages.length +
(controller.hasMoreMessages.value ? 1 : 0),
itemBuilder: (context, index) {
// 如果是最后一项且有更多消息,显示加载指示器
if (index == controller.messages.length &&
controller.hasMoreMessages.value) {
return _buildLoadMoreIndicator();
}
final message = controller.messages[
controller.messages.length - 1 - index];
return Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onLongPressStart:
(LongPressStartDetails details) {
messageActionController.onLongPressMessage(
context, isDarkMode, details, message);
},
child: MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
),
));
},
),
),
),
],
),
),
@ -392,7 +425,24 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
return IntroStepBuilder(
group: 'ai',
order: 1,
text: '对小言说“今天天气怎么样”',
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
controller.noviceGuideService
.aiStepsList[
controller.noviceGuideService
.aiStep]['start'],
buttonText: '确定输入',
onNext: () {
controller.textController.text =
controller.noviceGuideService
.aiStepsList[
controller
.noviceGuideService
.aiStep]['input'];
},
);
},
onWidgetLoad: () {
controller.noviceGuideService
.startAIGuide();
@ -630,111 +680,143 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
SizedBox(width: 8.w),
// 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮
Obx(() {
final hasText = controller.hasTextInput.value;
final hasImage =
controller.isImageInputActive.value;
final shouldShowSend = hasText || hasImage;
final shouldShowExpand =
controller.shouldShowExpandButton.value;
Builder(builder: (context) {
StepWidgetParams? widgetParams;
return IntroStepBuilder(
group: 'ai',
order: 2,
onHighlightWidgetTap: () {
widgetParams!.onNext!();
controller.sendMessage();
},
overlayBuilder: (params) {
widgetParams = params;
return IntroOverlayBuilder.aiBuilder(
params,
'点击发送,${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['send']}',
alignment: false,
);
},
builder:
(BuildContext context, GlobalKey key) =>
Obx(
key: key,
() {
final hasText =
controller.hasTextInput.value;
final hasImage =
controller.isImageInputActive.value;
final shouldShowSend =
hasText || hasImage;
final shouldShowExpand = controller
.shouldShowExpandButton.value;
if (shouldShowSend) {
// 显示发送按钮(可能还有展开按钮)
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 展开按钮(在发送按钮上方)
if (shouldShowExpand && hasText)
Padding(
padding:
EdgeInsets.only(bottom: 40.h),
child: GestureDetector(
if (shouldShowSend) {
// 显示发送按钮(可能还有展开按钮)
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 展开按钮(在发送按钮上方)
if (shouldShowExpand && hasText)
Padding(
padding: EdgeInsets.only(
bottom: 40.h),
child: GestureDetector(
onTap: () {
// 打开底部弹出编辑模式
controller
.openFullScreenEdit();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor:
Colors.transparent,
builder: (context) =>
const FullScreenEditor(),
).then((_) {
// 弹出页关闭时的回调
controller
.closeFullScreenEdit();
});
},
child: Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[700]
: Colors.grey[300],
borderRadius:
BorderRadius.circular(
16.w),
),
child: Icon(
Icons.expand_less,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 20.sp,
),
),
),
),
// 发送按钮
GestureDetector(
onTap: () =>
controller.sendMessage(),
child: Container(
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: primaryColor,
borderRadius:
BorderRadius.circular(
22.w),
),
child: Icon(
Icons.send,
color: Colors.white,
size: 22.sp,
),
),
),
],
);
} else {
// 显示'+'选择按钮
return Obx(() {
final isExpanded = controller
.isPlusOptionsExpanded.value;
return GestureDetector(
onTap: () {
// 打开底部弹出编辑模式
controller.openFullScreenEdit();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor:
Colors.transparent,
builder: (context) =>
const FullScreenEditor(),
).then((_) {
// 弹出页关闭时的回调
controller
.closeFullScreenEdit();
});
controller.togglePlusOptions();
},
child: Container(
width: 32.w,
height: 32.w,
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[700]
: Colors.grey[300],
? Colors.grey[800]
: Colors.grey[200],
borderRadius:
BorderRadius.circular(16.w),
BorderRadius.circular(22.w),
),
child: Icon(
Icons.expand_less,
isExpanded
? Icons.close
: Icons.add,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 20.sp,
size: 22.sp,
),
),
),
),
// 发送按钮
GestureDetector(
onTap: () => controller.sendMessage(),
child: Container(
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: primaryColor,
borderRadius:
BorderRadius.circular(22.w),
),
child: Icon(
Icons.send,
color: Colors.white,
size: 22.sp,
),
),
),
],
);
} else {
// 显示'+'选择按钮
return Obx(() {
final isExpanded =
controller.isPlusOptionsExpanded.value;
return GestureDetector(
onTap: () {
controller.togglePlusOptions();
},
child: Container(
width: 44.w,
height: 44.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius:
BorderRadius.circular(22.w),
),
child: Icon(
isExpanded ? Icons.close : Icons.add,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 22.sp,
),
),
);
});
}
);
});
}
},
),
);
}),
],
),

70
lib/modules/home/views/home_view.dart

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/widgets/intro_overlay_builder.dart';
import '../controllers/home_controller.dart';
import '../../../routes/app_routes.dart';
import '../../../core/widgets/common_bottom_nav.dart';
@ -84,31 +85,46 @@ class HomeView extends GetView<HomeController> {
_buildAppBar(isDarkMode),
// 顶部图片
_buildEarbudsImage(),
SizedBox(height: 20.h),
// 设备状态卡片
IntroStepBuilder(
group: 'home',
order: 1,
text: '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言',
onWidgetLoad: () {
controller.noviceGuideService.startHomeGuide();
},
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
'欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言',
onNext: () {},
);
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
child: _buildEarbudsImage(),
child: _buildDeviceStatusCard(isDarkMode),
),
),
SizedBox(height: 20.h),
// 设备状态卡片
IntroStepBuilder(
group: 'home',
order: 2,
text: '对着麦克风说“小言小言”',
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
'对着麦克风说“小言小言”',
onNext: () {},
);
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
child: _buildDeviceStatusCard(isDarkMode),
),
),
SizedBox(height: 20.h),
// 热门AI体验
_buildSectionTitle(isDarkMode),
@ -402,6 +418,10 @@ class HomeView extends GetView<HomeController> {
final rightEarCharging = controller.rightEarCharging;
final caseCharging = controller.caseCharging;
// 判断是否显示充电盒:充电盒电量大于0且设备已连接
final shouldShowCase =
caseBattery.value > 0 && controller.isFullyConnectedRx.value;
// 使用更紧凑的布局
return Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 4.w),
@ -435,20 +455,23 @@ class HomeView extends GetView<HomeController> {
isDarkMode: isDarkMode,
),
// // 分隔线
// Container(
// height: 30.h,
// width: 1,
// color: isDarkMode ? Colors.grey[800] : Colors.grey[300],
// ),
// // 充电盒
// _buildCompactBatteryItem(
// label: '充电盒',
// batteryLevel: caseBattery.value,
// isCharging: caseCharging.value,
// isDarkMode: isDarkMode,
// ),
// 只有在充电盒电量大于0且设备已连接时才显示充电盒
if (shouldShowCase) ...[
// 分隔线
Container(
height: 30.h,
width: 1,
color: isDarkMode ? Colors.grey[800] : Colors.grey[300],
),
// 充电盒
_buildCompactBatteryItem(
label: '充电盒',
batteryLevel: caseBattery.value,
isCharging: caseCharging.value,
isDarkMode: isDarkMode,
),
]
],
),
);
@ -645,11 +668,16 @@ class HomeView extends GetView<HomeController> {
child: IntroStepBuilder(
group: 'home',
order: 3,
text: '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧',
onHighlightWidgetTap: () {
Intro.of(Get.context!).dispose();
controller.exploreAIFeatures();
},
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
'太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧',
);
},
builder: (BuildContext context, GlobalKey key) => _buildFeatureCard(
key: key,
title: 'aiAssistant'.tr, // AI全能助手

119
lib/modules/login/controllers/login_controller.dart

@ -1,3 +1,5 @@
import 'dart:async';
import '../../../data/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart';
import 'package:get/get.dart';
@ -815,54 +817,79 @@ class LoginController extends GetxController {
// 使用登陆请求服务器
Future<void> requestLogin(Map params) async {
Logger().d("请求后台数据login=$params");
var response = await Api.login({
"mail": params["mail"],
"phone": params["phone"],
"stype": params["stype"],
"vcode": params["vcode"],
"ttoken": params["ttoken"],
"phonemodel": _appVersion + _countryCode + _userChannel.toString(),
"phonemac": params["phonemac"],
"name": params["name"],
"avatar": params["avatar"],
});
if (response != null) {
Logger().d("请求后台数据login回复数据=$response");
// final instance = await GetStorage();
await _storage.write('loginInfo', jsonEncode(response));
// 2. 将响应数据转换为User对象
try {
Logger().d('响应数据: ${response.toString()}'); // 调试输出原始数据
if (response['token'].toString() != '') {
// 新增:检查 user 字段是否存在且类型正确
// if (response.containsKey('data') && response['data'] is Map) {
final user = User.fromJson(response);
Logger().d('用户初始化成功: ${response['token'].toString()}');
await _storage.write("logintoken", response['token'].toString());
// 存储到 SharedPreferences (仅存储用户数据)
//final prefs = await GetStorage();
await _storage.write('user_info', user.toJson());
//获取app的配置信息
await getAppConfig();
Get.offAllNamed(Routes.pairing);
// } else {
// throw FormatException('API 响应缺少 user 字段');
// }
} else {
throw Exception('接口返回错误: ${response['msg']}');
try {
Logger().d("请求后台数据login=$params");
// 在Api.login调用时添加超时
var response = await Api.login({
"mail": params["mail"],
"phone": params["phone"],
"stype": params["stype"],
"vcode": params["vcode"],
"ttoken": params["ttoken"],
"phonemodel": _appVersion + _countryCode + _userChannel.toString(),
"phonemac": params["phonemac"],
"name": params["name"],
"avatar": params["avatar"],
}).timeout(
const Duration(seconds: 30), // 30秒超时
onTimeout: () {
throw TimeoutException('登录请求超时', const Duration(seconds: 30));
},
);
if (response != null) {
Logger().d("请求后台数据login回复数据=$response");
await _storage.write('loginInfo', jsonEncode(response));
try {
Logger().d('响应数据: ${response.toString()}');
if (response['token'].toString() != '') {
final user = User.fromJson(response);
Logger().d('用户初始化成功: ${response['token'].toString()}');
await _storage.write("logintoken", response['token'].toString());
await _storage.write('user_info', user.toJson());
// 获取app的配置信息
await getAppConfig();
Get.offAllNamed(Routes.pairing);
} else {
throw Exception('接口返回错误: ${response['msg']}');
}
} catch (e, stack) {
Logger().d('用户数据处理失败: $e');
Logger().d('堆栈跟踪: $stack');
Get.snackbar(
'error'.tr,
'userDataParseFailed'.tr, // 用户数据解析失败
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
}
} catch (e, stack) {
// 新增:打印堆栈跟踪
Logger().d('用户数据处理失败: $e');
Logger().d('堆栈跟踪: $stack');
Get.snackbar('错误', '用户数据解析失败,请检查网络');
} else {
// API返回null的情况
Get.snackbar(
'error'.tr,
'networkError'.tr, // 网络错误,请重试
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
}
// Get.offAll(() => IndexWidget());
} catch (e) {
// 网络请求异常
Logger().e('登录请求失败: $e');
Get.snackbar(
'error'.tr,
'loginFailed'.tr, // 登录失败
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
} finally {
// 确保无论成功还是失败都重置加载状态
_isLoading.value = false;
}
}

17
lib/modules/login/views/login_view.dart

@ -13,12 +13,17 @@ class LoginView extends GetView<LoginController> {
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
body: SafeArea(
child: Obx(() => controller.viewState == LoginViewState.welcome
? _buildWelcomeView(isDarkMode)
: _buildVerificationView(context, isDarkMode)),
return GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
},
child: Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
body: SafeArea(
child: Obx(() => controller.viewState == LoginViewState.welcome
? _buildWelcomeView(isDarkMode)
: _buildVerificationView(context, isDarkMode)),
),
),
);
}

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

@ -1,15 +1,14 @@
import 'dart:async';
import 'dart:io';
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
import '../../../data/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../../data/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart';
import '../../../data/models/user_Info.dart';
import '../../../routes/app_routes.dart';
import '../../../core/themes/theme_service.dart';
import '../../../data/services/network/api.dart';
import '../../../data/services/version_update_service.dart';
import '../../../modules/common/widgets/version_update_dialog.dart';
@ -24,191 +23,174 @@ class SplashController extends GetxController {
final _loadingProgress = 0.0.obs;
double get loadingProgress => _loadingProgress.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; //是否同意隐私政策
// 版本号
//final String version = 'V1.0.0';
// 主题服务
final ThemeService _themeService = ThemeService();
// 会话级别的版本检查标记(静态变量,整个应用会话期间保持)
static bool _hasCheckedVersionInSession = false;
// 版本检查相关常量
static const String _lastVersionCheckKey = 'last_version_check_timestamp';
static const int _versionCheckIntervalHours = 24; // 24小时间隔
// 使用一个反应式变量来决定是否可以导航,替代所有布尔标记
final _canNavigate = false.obs;
@override
void onInit() {
super.onInit();
// 首先检查隐私政策状态
final hasAcceptedPrivacy = _storage.read('hasAcceptedPrivacy') ?? false;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!hasAcceptedPrivacy) {
// 优先显示隐私政策弹窗,暂停其他初始化
_showPrivacyDialog();
} else {
// 已同意隐私政策,正常启动流程
_startNormalFlow();
}
});
// 注意:追踪权限请求将在用户同意隐私政策后执行
}
/// 异步请求追踪权限
Future<void> _requestTrackingPermissionAsync() async {
try {
await AppTrackingTransparency.requestTrackingAuthorization();
Logger.d(_tag, '用户追踪权限请求完成');
} catch (e) {
Logger.e(_tag, '用户追踪权限请求失败: $e');
} finally {
_isTrackingPermissionRequested.value = true;
// 监听 _canNavigate 状态,一旦为 true,立即且仅执行一次导航
once(_canNavigate, (_) => _navigateToTargetPage());
// 启动整个初始化流程
_initializeApp();
}
/// 启动并协调所有初始化任务
Future<void> _initializeApp() async {
// 步骤 1: 检查并等待隐私政策同意
final hasAccepted = await _checkAndRequestPrivacy();
if (!hasAccepted) {
// 如果用户拒绝隐私政策,应用会退出,后续代码不执行
return;
}
}
void _startNormalFlow() {
_startLoadingAnimation();
_startParallelTasks();
}
// 步骤 2: 并行执行所有启动任务,并等待它们全部完成
// Future.wait 会等待数组中所有的 Future 都完成后才继续
await Future.wait([
_startLoadingAnimation(), // 任务1: 最小动画时长
_performParallelTasks(), // 任务2: 其他所有后台任务
]);
/// 启动并行任务:在1.5秒动画期间同时执行多个任务
void _startParallelTasks() {
// 并行执行所有任务
_requestTrackingPermissionAsync();
_performVersionCheckAsync();
_validateTokenAsync();
// 步骤 3: 所有任务(动画和后台)都已完成,触发导航
_canNavigate.value = true;
}
/// 异步执行版本检查
Future<void> _performVersionCheckAsync() async {
try {
Logger.d(_tag, '开始执行版本检查');
await _checkVersionUpdateIfNeeded();
} catch (e) {
Logger.e(_tag, '版本检查失败: $e');
/// 检查隐私政策,如果未同意,则显示弹窗并等待用户操作
/// 返回用户是否同意
Future<bool> _checkAndRequestPrivacy() async {
if (_storage.read<bool>('hasAcceptedPrivacy') ?? false) {
return true;
}
//finally {
// _isVersionCheckCompleted.value = true;
// }
}
void _onPrivacyAccepted() {
_storage.write('hasAcceptedPrivacy', true);
Get.back(); // 关闭弹窗
_startNormalFlow(); // 开始正常流程(已包含版本检查)
}
void _onPrivacyRejected() {
exit(0); // 退出应用
}
final completer = Completer<bool>();
WidgetsBinding.instance.addPostFrameCallback((_) {
PrivacyPolicyDialog.show(
onAccept: () {
_storage.write('hasAcceptedPrivacy', true);
Get.back(); // 关闭弹窗
if (!completer.isCompleted) completer.complete(true);
},
onReject: () {
if (!completer.isCompleted) completer.complete(false);
exit(0); // 退出应用
},
canDismiss: false,
);
});
// 显示隐私政策弹窗
void _showPrivacyDialog() {
PrivacyPolicyDialog.show(
onAccept: () {
_onPrivacyAccepted();
_hasAcceptedPrivacy.value = true;
},
onReject: _onPrivacyRejected,
canDismiss: false, // 禁止通过返回键关闭弹窗
);
return completer.future;
}
// 进度条动画(持续1.5秒)
void _startLoadingAnimation() {
/// 动画任务:返回一个在动画结束时完成的 Future
Future<void> _startLoadingAnimation() {
const totalDuration = 1500; // 动画总时长1.5秒
const steps = totalDuration ~/ 30; // 30ms间隔,更平滑的动画
const steps = totalDuration ~/ 30;
const stepValue = 1.0 / steps;
final completer = Completer<void>();
Timer.periodic(const Duration(milliseconds: 30), (timer) {
if (_loadingProgress.value < 1.0) {
_loadingProgress.value += stepValue;
} else {
_loadingProgress.value = 1.0;
timer.cancel();
_isAnimationCompleted.value = true;
_checkNavigateCondition();
if (!completer.isCompleted) completer.complete();
}
});
return completer.future;
}
/// 将所有并行的后台任务打包成一个 Future
Future<void> _performParallelTasks() async {
// 同样使用 Future.wait,确保此函数在所有子任务完成后才结束
await Future.wait([
_requestTrackingPermissionAsync(),
_validateTokenAsync(),
_performVersionCheckAsync(),
]);
Logger.d(_tag, '所有后台任务已完成');
}
/// 异步请求追踪权限
Future<void> _requestTrackingPermissionAsync() async {
try {
await AppTrackingTransparency.requestTrackingAuthorization();
Logger.d(_tag, '用户追踪权限请求完成');
} catch (e) {
Logger.e(_tag, '用户追踪权限请求失败: $e');
// 即使失败,也视为完成,不阻塞主流程
}
}
/// 异步验证Token和加载应用配置
Future<void> _validateTokenAsync() async {
try {
bool isLoggedIn = false;
final token = _storage.read('logintoken');
Logger.d(_tag, 'Token验证开始: $token');
if (token != null && token.isNotEmpty) {
isLoggedIn = await getLoginToken();
final isLoggedIn = await getLoginToken();
if (isLoggedIn) {
// 只有登录成功,才获取App配置
await getAppConfig();
_isAppConfigLoaded.value = true;
} else {
await User.clearUserSession(); //
}
}
_isLoggedIn.value = isLoggedIn;
Logger.d(_tag, 'Token验证完成,登录状态: $isLoggedIn');
} catch (e) {
Logger.e(_tag, 'Token验证失败: $e');
_isLoggedIn.value = false;
} finally {
_isTokenValidated.value = true;
Logger.e(_tag, 'Token验证或配置加载失败: $e');
await User.clearUserSession();
}
}
// 检查跳转条件:所有任务都完成后才跳转
void _checkNavigateCondition() {
if (_isAnimationCompleted.value &&
_isTrackingPermissionRequested.value &&
_isVersionCheckCompleted.value &&
_isTokenValidated.value) {
_navigateToTargetPage();
/// 异步执行版本检查
Future<void> _performVersionCheckAsync() async {
try {
final versionService = VersionUpdateService.to;
final updateInfo = await versionService.checkForUpdate();
if (updateInfo.hasUpdate) {
Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}');
final completer = Completer<void>();
// 在主线程中显示更新对话框
WidgetsBinding.instance.addPostFrameCallback((_) {
// 使用 whenComplete 确保无论弹窗如何关闭,Future 都会完成
VersionUpdateDialog.show(updateInfo).whenComplete(() {
if (!completer.isCompleted) completer.complete();
});
});
// 等待弹窗交互结束
await completer.future;
} else {
Logger.i(_tag, '当前已是最新版本');
}
} catch (e) {
Logger.e(_tag, '版本检查失败: $e');
// 即使检查失败,也视为任务完成
}
}
// 根据登录状态跳转到目标页面
/// 根据最终状态跳转到目标页面
void _navigateToTargetPage() {
if (_isLoggedIn.value && _isAppConfigLoaded.value) {
// 导航时直接检查最终的数据状态,而不是依赖中间变量
final token = _storage.read('logintoken');
final isAppConfigInitialized = AppConfig.isInitialized();
// 检查登录状态和配置初始化状态
if (token != null && token.isNotEmpty && isAppConfigInitialized) {
Get.offAllNamed(Routes.pairing);
} else {
Get.offAllNamed(Routes.login);
}
}
/// 异步获取登录Token
Future<bool> getLoginToken() async {
try {
Logger.d(_tag, 'getLoginToken:进来了');
//if (User.getname() != "") {
final response = await Api.tokenlogin();
Logger.d(_tag, 'getLoginToken: $response');
if (response == null) return false;
if (response['user'] != '') {
Logger.d(_tag, 'getLoginToken:进来了');
// 根据API设计调整
if (response?['user'] != null && response['user'] != '') {
final user = User.fromJson(response);
// 存储用户信息(示例)
await _storage.write('user_info', user.toJson());
return true;
}
//return true;
//}
return false;
} catch (e) {
Logger.e(_tag, 'Token获取失败: $e');
@ -218,140 +200,14 @@ class SplashController extends GetxController {
Future<bool> getAppConfig() async {
try {
Logger.d(_tag, 'getAppConfig:进来了');
//if (User.getname() != "") {
final response = await Api.getappconfig();
Logger.d(_tag, 'getAppConfig: $response');
if (response == null) return false;
// 根据API设计调整
final resp = UserGetAppConfigResp.fromJson(response);
Logger.d(_tag, 'getAppConfig:进来了');
// 存储用户信息(示例)
// _storage.write('ENV', resp.env);
// _storage.write('AGENTS', resp.agents);
// _storage.write('MCPS', resp.mcps);
AppConfig.initialize(resp);
return true;
} catch (e) {
Logger.e(_tag, 'getAppConfig 取失败: $e');
Logger.e(_tag, 'getAppConfig 获取失败: $e');
return false;
}
}
/// 混合检查机制:判断是否需要进行版本检查
/// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时
Future<void> _checkVersionUpdateIfNeeded() async {
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');
await _performVersionCheck();
} else {
Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔');
}
} catch (e) {
Logger.e(_tag, '版本检查判断失败: $e');
}
}
/// 执行版本检查(在后台进行,不阻塞启动流程)
Future<void> _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).then((_) {
// 无论用户选择更新还是稍后提醒,都标记版本检查完成
_isVersionCheckCompleted.value = true;
_checkNavigateCondition();
});
});
} else {
_isVersionCheckCompleted.value = true;
Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}');
}
// 更新检查记录
_updateVersionCheckRecord();
}).catchError((e) {
Logger.e(_tag, '版本检查失败: $e');
// 即使检查失败,也标记版本检查完成,让流程继续
_isVersionCheckCompleted.value = true;
// 更新检查记录,避免频繁重试
_updateVersionCheckRecord();
});
} catch (e) {
Logger.e(_tag, '版本检查失败: $e');
// 即使检查失败,也标记版本检查完成,让流程继续
_isVersionCheckCompleted.value = true;
// 更新检查记录,避免频繁重试
_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');
}
}
}

13
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -318,6 +318,8 @@ object AgentService : CoroutineScope {
_isTtsSpeaking.set(true)
restartIdleCheck()
sendEvent("tts_started", mapOf("status" to "started"))
// 停止气泡音,避免与TTS同时播放
audioPlayer?.stopAudio()
}
TtsEventType.SYNTHESIS_COMPLETED -> {
@ -330,11 +332,15 @@ object AgentService : CoroutineScope {
_isTtsSpeaking.set(false)
restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled"))
// TTS取消时停止气泡音
audioPlayer?.stopAudio()
}
TtsEventType.PLAYBACK_STARTED -> { //开始AI播报内容
restartIdleCheck()
sendEvent("playback_started", mapOf("status" to "playback_started"))
// 确保气泡音已停止,避免与TTS播放重叠
audioPlayer?.stopAudio()
// 发送AI说话开始广播
val intent =
Intent("com.yunqiinnovation.music_service.AI_SAY_START").apply {
@ -357,6 +363,8 @@ object AgentService : CoroutineScope {
TtsEventType.ERROR -> {
_isTtsSpeaking.set(false)
restartIdleCheck()
// TTS错误时停止气泡音
audioPlayer?.stopAudio()
val params = event.params
val code = params["errorCode"] as? String ?: "UNKNOWN_ERROR"
val message = params["errorMessage"] as? String ?: "未知错误"
@ -665,6 +673,8 @@ object AgentService : CoroutineScope {
// 并行执行停止操作,加快响应速度
stopTts()
stopAiStream()
// 停止气泡音,避免在打断后继续播放
audioPlayer?.stopAudio()
// 记录打断耗时
@ -891,7 +901,8 @@ object AgentService : CoroutineScope {
responseBuilder.append(token)
if (speakResponse && reply && broadcast) {
ttsService?.speakStream(token)
// audioPlayer?.stopAudio()
// 在开始流式TTS时立即停止气泡音
audioPlayer?.stopAudio()
}
if (reply && token.isNotEmpty()) {
// 发送流式回复token

55
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -996,7 +996,8 @@ class ChatApiStreamCallback: StreamCallback {
if speakResponse && reply && broadcast{
try agentService.azureTtsHelper?.speakStream(token)
// try agentService.audioPlayer?.stopAwaitSound()
// 在开始流式TTS时立即停止气泡音
// try agentService.audioPlayer?.stopAwaitSound()
}
if (reply && token != "") {
try agentService.sendEvent(name: "assistant_token", data: ["token": token])
@ -1219,8 +1220,12 @@ class AudioPlayer {
private var player: AVAudioPlayer?
private var callingPlayer: AVAudioPlayer?
private var awaitPlayer: AVAudioPlayer? // 新增:等待音效播放器
private var awaitTimer: DispatchSourceTimer? // 新增:等待音效超时定时器
private let logger = OSLog(subsystem: "com.yunqiinnovation.agent_service", category: "AudioPlayer")
private let audioQueue = DispatchQueue(label: "agent.audio.player", qos: .userInitiated)
// 等待音效超时时间(秒)- 改为 var 以支持动态修改
private var awaitTimeoutSeconds: TimeInterval = 10.0
private var resourcesBundle: Bundle {
#if SWIFT_PACKAGE
@ -1246,7 +1251,14 @@ class AudioPlayer {
}
func playAwaitSound() {
// 先停止之前的等待音效和定时器
stopAwaitSound()
// 播放等待音效
playLoopSound(named: "await", fileType: "mp3", player: &awaitPlayer)
// 启动超时定时器
startAwaitTimeout()
}
func playStopSound() {
@ -1263,8 +1275,14 @@ class AudioPlayer {
}
func stopAwaitSound() {
// 停止音频播放
awaitPlayer?.stop()
awaitPlayer = nil
// 停止超时定时器
stopAwaitTimeout()
os_log("停止等待音效播放", log: logger, type: .debug)
}
private func playSound(named: String, fileType: String = "mp3") {
@ -1337,6 +1355,41 @@ class AudioPlayer {
os_log("播放循环音频失败: %{public}@", log: logger, type: .error, error.localizedDescription)
}
}
private func startAwaitTimeout() {
// 停止之前的定时器
stopAwaitTimeout()
// 创建新的定时器
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility))
awaitTimer = timer
timer.schedule(deadline: .now() + awaitTimeoutSeconds)
timer.setEventHandler { [weak self] in
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
os_log("等待音效超时,自动停止播放 - 超时时间: %.0f秒",
log: self.logger, type: .info, self.awaitTimeoutSeconds)
self.stopAwaitSound()
}
}
timer.resume()
os_log("启动等待音效超时定时器,超时时间: %.0f秒",
log: logger, type: .debug, awaitTimeoutSeconds)
}
private func stopAwaitTimeout() {
awaitTimer?.cancel()
awaitTimer = nil
}
func setAwaitTimeout(_ seconds: TimeInterval) {
awaitTimeoutSeconds = max(2.0, seconds) // 最小5秒
os_log("设置等待音效超时时间: %.0f秒", log: logger, type: .info, awaitTimeoutSeconds)
}
}
extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {

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

@ -100,14 +100,9 @@ public class AzureAsrHelper: NSObject {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region)
// 针对翻译场景的性能优化
optimizeForTranslation()
// optimizeForTranslation()
if isAutoDetectLanguage {
// 针对翻译场景优化的语言检测配置
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 {
@ -150,55 +145,7 @@ 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
]
}
/**
* 开始连续语音识别
*
@ -249,28 +196,7 @@ 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 ""
// }
// }
/**
* 停止连续语音识别
@ -388,11 +314,20 @@ public class AzureAsrHelper: NSObject {
// 创建识别器
if isAutoDetectLanguage {
// 使用正确的初始化方法
let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages)
print("autoDetectConfig: \(autoDetectConfig)")
guard let autoDetectSourceLanguageConfig = try? SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) else {
print("创建自动语言检测配置失败,使用默认语言配置")
// 如果创建失败,使用默认的单语言配置
recognizer = try SPXSpeechRecognizer(
speechConfiguration: speechConfig!,
audioConfiguration: audioConfig!
)
return true
}
print("autoDetectConfig: \(autoDetectSourceLanguageConfig)")
recognizer = try SPXSpeechRecognizer(
speechConfiguration: speechConfig!,
autoDetectSourceLanguageConfiguration: autoDetectConfig,
autoDetectSourceLanguageConfiguration: autoDetectSourceLanguageConfig,
audioConfiguration: audioConfig!
)
} else {
@ -484,60 +419,7 @@ public class AzureAsrHelper: NSObject {
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)
}
/**
* 动态切换语言(翻译场景中的语言切换)
@ -588,13 +470,16 @@ public class AzureAsrHelper: NSObject {
let result = event.result
guard !(result.text?.isEmpty ?? true) else { return }
// let detectedLanguage = self.getDetectedLanguage(from: result)
// 检测文本语言,避免重复调用
let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN"
// 使用现有的 getDetectedLanguage 方法替代 result.language
let detectedLanguage = self.getDetectedLanguage(from: result)
let text = result.text
print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)")
// // 检测文本语言,避免重复调用
// let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN"
// 输出检测到的语言信息
print("检测到的语言: \(detectedLanguage)")
// // 输出检测到的语言信息
// print("检测到的语言: \(detectedLanguage)")
DispatchQueue.main.async {
callback.onRecognizing(result.text ?? "", detectedLanguage)
@ -608,11 +493,16 @@ public class AzureAsrHelper: NSObject {
guard result.reason == .recognizedSpeech,
!(result.text?.isEmpty ?? true) else { return }
// 使用现有的 getDetectedLanguage 方法替代 result.language
let detectedLanguage = self.getDetectedLanguage(from: result)
let text = result.text
print("识别完成事件=检测到语言: \(detectedLanguage), 识别中: \(text)")
// 检测文本语言,避免重复调用
let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN"
// let detectedLanguage = self.detectTextLanguage(result.text ?? "") ?? "zh-CN"
// 输出检测到的语言信息
print("检测到的语言: \(detectedLanguage)")
// // 输出检测到的语言信息
// print("检测到的语言: \(detectedLanguage)")
// 在主线程回调结果
DispatchQueue.main.async {
@ -1175,3 +1065,54 @@ private func runMicrophoneCapture() {
func onError(_ error: String)
}
}
// //ios 语言检测
// /**
// * 检测输入文本的语言,从支持的语言列表中选择最匹配的
// *
// * @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
// ]
// }

59
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift

@ -26,22 +26,32 @@ class SwiftOpusAudioProcessor: NSObject {
cleanup()
}
/// 线程安全的解码器初始化
private func initializeOpusDecoder() -> Bool {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
if decoder != nil {
return true
}
var error: Int32 = 0
decoder = opus_decoder_create(sampleRate, channels, &error)
let newDecoder = opus_decoder_create(sampleRate, channels, &error)
guard error == OPUS_OK, decoder != nil else {
guard error == OPUS_OK, newDecoder != nil else {
print("Opus解码器创建失败,错误代码: \(error)")
return false
}
decoder = newDecoder
return true
}
/// 线程安全的解码器销毁
private func destroyOpusDecoder() {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
if let decoder = decoder {
opus_decoder_destroy(decoder)
self.decoder = nil
@ -112,23 +122,44 @@ class SwiftOpusAudioProcessor: NSObject {
}
}
// 简化的 Opus 解码方法,重点是容错处理
/// 改进的 Opus 解码方法,增强容错和线程安全
private func decodeOpusPacket(_ packet: Data) {
guard let decoder = decoder else { return }
// 添加线程安全锁
objc_sync_enter(self)
defer { objc_sync_exit(self) }
// 验证解码器状态
guard let decoder = decoder, isDecodingActive else {
print("解码器未初始化或已停止")
return
}
let maxSamples = Int(maxFrameSize)
var pcmBuffer = [opus_int16](repeating: 0, count: maxSamples * Int(channels))
let decodedSamples = packet.withUnsafeBytes { packetPtr in
let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress
return opus_decode(
decoder,
uint8Ptr,
opus_int32(packet.count),
&pcmBuffer,
Int32(maxSamples),
0
)
// 确保指针有效
guard let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress else {
print("无法获取数据包指针")
return opus_int32(-1)
}
// 添加异常捕获机制
var result: opus_int32 = -1
// 使用 autoreleasepool 确保内存管理
autoreleasepool {
result = opus_decode(
decoder,
uint8Ptr,
opus_int32(packet.count),
&pcmBuffer,
Int32(maxSamples),
0
)
}
return result
}
// 增强的错误处理:检查各种错误情况
@ -237,4 +268,4 @@ class SwiftOpusAudioProcessor: NSObject {
func getCurrentConfig() -> (sampleRate: Int32, channels: Int32) {
return (sampleRate, channels)
}
}
}

Loading…
Cancel
Save