Browse Source

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

newdev_shunjiawei
tanlongsheng 12 months ago
parent
commit
1c04d078ee
  1. 162
      lib/data/services/meeting/meeting_task_service.dart
  2. 36
      lib/data/services/version_update_service.dart
  3. 1
      lib/modules/agent/controllers/agent_controller.dart
  4. 200
      lib/modules/agent/views/agent_view.dart
  5. 1
      lib/modules/agent/views/music_card.dart
  6. 127
      lib/modules/login/constants/country_codes.dart
  7. 154
      lib/modules/login/controllers/login_controller.dart
  8. 1
      lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart
  9. 26
      lib/modules/meeting/views/meeting_details_view.dart
  10. 1
      lib/modules/meeting/views/meeting_home_view.dart
  11. 1
      lib/modules/meeting/views/meeting_view.dart
  12. 22
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  13. 75
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  14. 76
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  15. 740
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift

162
lib/data/services/meeting/meeting_task_service.dart

@ -10,29 +10,35 @@ import '../db/sqflite_api.dart';
import '../deapsound_ai_service.dart';
import 'meeting_upload_service.dart';
///会议任务服务类,负责管理会议音频的转写任务处理流程,包括任务提交、状态轮询、结果处理和AI分析等功能。
class MeetingTaskService extends GetxService {
final GetStorage _storage = GetStorage();
MeetingUploadService get _uploadService => Get.find<MeetingUploadService>();
List _taskList = [];
RxList taskFinishList = [].obs;
List _taskList = []; //待处理的会议任务列表
RxList taskFinishList = [].obs; //已处理的会议任务列表
bool _isTask = false;
bool _isTask = false; //是否有正在处理的任务
String _userId = '';
//字节跳动(火山引擎)语音服务配置
final String _appid = '2316081933';
final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En';
final String _cluster = 'volc_auc_tele_flash';
final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc';
final String _cluster = 'volc_auc_video_flash';
final String _cluster1 = 'volc.bigasr.auc';
final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc'; //
final String _serviceUrl1 =
'https://openspeech.bytedance.com/api/v3/auc/bigmodel'; //大模型录音文件识别api地址
Map<String, String> _headers = {};
Map<String, String> _headers1 = {};
int meetingId = 0;
RxInt tasktype = 1.obs;
List textList = [];
String summary = '';
String overview = '';
RxInt tasktype = 1.obs; //任务类型,1-转写中,2-总结中,3-完成
List textList = []; //转写结果列表
String summary = ''; //AI总结内容
String overview = ''; //AI概览内容
@override
void onInit() {
@ -41,6 +47,14 @@ class MeetingTaskService extends GetxService {
'Authorization': 'Bearer; $_token',
'Content-Type': 'application/json',
};
// _headers1 = {
// 'X-Api-App-Key': _appid,
// 'X-Api-Access-Key': _token,
// 'X-Api-Resource-Id': _cluster1,
// 'X-Api-Request-Id': DateTime.now().millisecondsSinceEpoch.toString(),
// 'X-Api-Sequence': '-1',
// };
_getAgent();
_getTaskList();
}
@ -56,6 +70,14 @@ class MeetingTaskService extends GetxService {
_executeTask();
}
///添加会议任务
///[id] 会议ID
///[audiourl] 音频URL
///[taskid] 任务ID
///[isSpeaker] 是否为发言人
///[language] 语言
///[prompt] 提示词
///[outline] 大纲
Future<void> addTask(int id, String audiourl, String taskid, bool isSpeaker,
String language, String prompt, String outline) async {
if (audiourl.isEmpty) {
@ -81,6 +103,14 @@ class MeetingTaskService extends GetxService {
}
}
///添加会议任务列表
///[id] 会议ID
///[audiourl] 音频URL
///[taskId] 任务ID
///[isSpeaker] 是否为发言人
///[language] 语言
///[prompt] 提示词
///[outline] 大纲
Future<void> addTaskList(int id, String audiourl, String taskId,
bool isSpeaker, String language, String prompt, String outline) async {
if (taskId.isEmpty) {
@ -103,14 +133,15 @@ class MeetingTaskService extends GetxService {
_executeTask();
}
///执行会议任务
void _executeTask() async {
if (_isTask) return;
_isTask = true;
while (_taskList.isNotEmpty) {
await Future.delayed(const Duration(seconds: 2));
for (var element in List.from(_taskList)) {
await _fileRecognize(element);
Logger.info("lxm--轮询---会议任务列表$_taskList");
for (var task in List.from(_taskList)) {
await _fileRecognize(task);
}
}
_isTask = false;
@ -133,9 +164,46 @@ class MeetingTaskService extends GetxService {
}
}
//
Future<String> _submitTask(
String audiourl, bool isSpeaker, String language) async {
String format = _getFileType(audiourl);
final taskId = DateTime.now().millisecondsSinceEpoch.toString();
_headers1 = {
'X-Api-App-Key': _appid,
'X-Api-Access-Key': _token,
'X-Api-Resource-Id': _cluster1,
'X-Api-Request-Id': taskId,
'X-Api-Sequence': '-1',
};
print("lxm--大模型asr请求头$_headers1");
final requestBody1 = jsonEncode({
"user": {
"uid": _userId,
},
"audio": {
"url": audiourl,
"language": language,
"format": format,
},
"request": {
"model_name": "bigmodel", // 模型名称,目前只有bigmodel
"model_version": "400", // 模型版本,400模型性能略有提升,且ITN有较大优化
//"enable_itn": true, // 启用文本规范化,默认为true
"enable_punc": true, // 启用标点,默认为false
//"enable_ddc": false, // 启用语义顺滑,默认为false
"enable_speaker_info": isSpeaker, // 启用说话人聚类分离,10人以内效果较好
//"enable_channel_split": false, // 启用双声道识别,默认为false
//"show_utterances": true, // 输出语音停顿、分句、分词信息
//"show_speech_rate": false, // 分句信息携带语速
//"show_volume": false, // 分句信息携带音量
//"enable_lid": false, // 启用语种识别
}
});
print("lxm--大模型asr请求体$requestBody1");
final requestBody = jsonEncode({
"app": {
@ -158,18 +226,23 @@ class MeetingTaskService extends GetxService {
try {
final response = await http.post(
Uri.parse('$_serviceUrl/submit'),
headers: _headers,
body: requestBody,
Uri.parse('$_serviceUrl1/submit'),
headers: _headers1,
body: requestBody1,
);
print("lxm--大模型asr响应体${response.body}");
print("lxm--大模型asr上传响应头${response.headers['x-tt-logid']}");
print("lxm--大模型asr上传服务端返回的状态码${response.headers['x-api-status-code']}");
print("lxm--大模型asr上传服务端返回的消息${response.headers['x-api-message']}");
if (response.statusCode != 200) {
throw Exception('Submit failed with status: ${response.statusCode}');
}
final respDic = jsonDecode(response.body);
final taskId = respDic['resp']['id'] as String;
Logger.debug('taskId: $taskId');
//final respDic = jsonDecode(response.body);
//final taskId = respDic['resp']['id'] as String;
//Logger.debug('taskId: $taskId');
return taskId;
} catch (e) {
Logger.error('Submit error: ${e.toString()}');
@ -182,6 +255,8 @@ class MeetingTaskService extends GetxService {
return array[array.length - 1];
}
///轮询语音转文字任务状态
///[taskId] 任务ID
Future<Map<String, dynamic>> _queryTask(String taskId) async {
final queryBody = jsonEncode({
"appid": _appid,
@ -192,11 +267,18 @@ class MeetingTaskService extends GetxService {
try {
final response = await http.post(
Uri.parse('$_serviceUrl/query'),
headers: _headers,
body: queryBody,
Uri.parse('$_serviceUrl1/query'),
headers: _headers1,
body: jsonEncode({}),
);
// print("lxm---大模型asr响应体${response.body}");
// print("lxm---大模型asr响应头${response.headers}");
// print("lxm---大模型asr响应状态码${response.statusCode}");
// print("lxm---大模型asr响应体logid${response.headers['x-tt-logid']}");
// print("lxm---大模型asr服务端返回的状态码${response.headers['x-api-status-code']}");
// print("lxm---大模型asr服务端返回的消息${response.headers['x-api-message']}");
if (response.statusCode != 200) {
throw Exception('Query failed with status: ${response.statusCode}');
}
@ -209,20 +291,24 @@ class MeetingTaskService extends GetxService {
}
}
///轮询语音转文字任务状态,若任务完成则处理转写结果
///[item] 包含任务ID、会议ID、AI总结提示词、AI概览提示词的Map
Future<void> _fileRecognize(Map item) async {
try {
var respDic = await _queryTask(item['taskid']);
var code = respDic['resp']['code'] as int;
//var code = respDic['resp']['code'] as int;
print("lxm---大模型asr响应体结果$respDic['result']['text']");
if (code == 1000) {
if (respDic['result']['text'] != null) {
await _addTextList(
item['id'],
respDic['resp']['utterances'],
item['prompt'],
item['outline'],
item['id'], //会议ID
respDic['result']['utterances'], //语音转文字结果
item['prompt'], //AI总结提示词
item['outline'], //AI概览提示词
);
return;
} else if (code < 2000) {
} else {
Logger.debug("FileRecognize failed");
return;
}
@ -231,11 +317,18 @@ class MeetingTaskService extends GetxService {
}
}
///处理语音识别完成后的转写结果,包括数据库存储、状态更新和触发AI分析
///[id] 会议ID
///[utterances] 语音转文字结果
///[prompt] AI总结提示词
///[outline] AI概览提示词
Future<void> _addTextList(
int id, List utterances, String prompt, String outline) async {
String text = '';
String personnel = '';
List<Map<String, Object?>> list = [];
// 处理转写结果,将每个句子的开始时间、结束时间、发言人和内容存储到列表中
for (var element in utterances) {
String speaker = '';
if (element['additions'].isNotEmpty) {
@ -254,8 +347,10 @@ class MeetingTaskService extends GetxService {
});
text += '${element['text']}\n';
}
// 存储处理后的转写结果到数据库
var result = await SqfliteApi.insertMeetingSpeaker(list);
if (result != null) {
// 更新会议状态为“总结中”
await SqfliteApi.editMeeting(
id,
{'tasktype': 2, 'personnel': personnel},
@ -282,9 +377,16 @@ class MeetingTaskService extends GetxService {
_storage.write("meeting_task_list", _taskList);
_storage.write("meeting_task_finish_list", taskFinishList);
}
// 触发AI分析,分别请求总结和概览
aiServiceMessage(id, text, prompt, outline);
}
///触发AI分析,分别请求总结和概览
///[id] 会议ID
///[text] 语音转文字结果
///[prompt] AI总结提示词
///[outline] AI概览提示词
void aiServiceMessage(int id, String text, String prompt, String outline) {
int awaitQuantity = 0;
String summaryText = '';
@ -321,6 +423,10 @@ class MeetingTaskService extends GetxService {
});
}
///处理AI分析完成后的结果,包括数据库存储和状态更新
///[id] 会议ID
///[summaryText] 总结结果
///[overviewText] 概览结果
void _aiServiceValue(int id, String summaryText, String overviewText) {
SqfliteApi.editMeeting(id, {
'tasktype': 3,

36
lib/data/services/version_update_service.dart

@ -211,6 +211,7 @@ class VersionUpdateService extends GetxService {
/// 获取当前平台信息
Future<PlatformInfo> getCurrentPlatformInfo() async {
if (Platform.isIOS) {
await _getIosDeviceBrand();
return const PlatformInfo(
platform: 'ios',
storeType: AppStoreType.appStore,
@ -310,6 +311,41 @@ class VersionUpdateService extends GetxService {
}
}
/// 获取iOS设备品牌信息
Future<void> _getIosDeviceBrand() async {
try {
final deviceInfo = DeviceInfoPlugin();
final iosInfo = await deviceInfo.iosInfo;
// 获取设备型号信息
final model =
iosInfo.model.toLowerCase(); // 例如: "iPhone", "iPad", "iPod touch"
//print('lxm---model: $model');
final utsname = iosInfo.utsname; // 包含更详细的设备信息
//print('lxm---utsname: $utsname');
final machineId = utsname.machine.toLowerCase(); // 例如: "iPhone14,2"
//print('lxm---machineId: $machineId');
// 根据设备型号确定品牌
if (model.contains('iphone') || machineId.contains('iphone')) {
_deviceBrand = 'iphone';
} else if (model.contains('ipad') || machineId.contains('ipad')) {
_deviceBrand = 'ipad';
} else if (model.contains('ipod') || machineId.contains('ipod')) {
_deviceBrand = 'ipod';
} else if (model.contains('mac') || machineId.contains('mac')) {
_deviceBrand = 'mac';
} else {
_deviceBrand = 'ios'; // 默认值
}
_logger.d('iOS设备品牌: $_deviceBrand (model: $model, machine: $machineId)');
} catch (e) {
_logger.e('获取iOS设备品牌失败: $e');
_deviceBrand = 'ios'; // 默认值
}
}
/// 判断是否需要强制更新
/// 当主版本号(第一位数字)不同时返回true
bool _shouldForceUpdate(String current, String latest) {

1
lib/modules/agent/controllers/agent_controller.dart

@ -1002,6 +1002,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
if (function_name.contains("music")) {
Future.delayed(Duration(seconds: 1), () {
_qqmusicManager.syncCurrentPlayInfo(isOpenPlay: false);
musicStatus();
});
// }
}

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

@ -25,6 +25,9 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
late MessageActionController messageActionController;
double _keyboardHeight = 0;
// 👇 新增:用于控制 TextField 焦点
final FocusNode _textFocusNode = FocusNode();
@override
void initState() {
super.initState();
@ -35,6 +38,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
@override
void dispose() {
_textFocusNode.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@ -286,65 +290,102 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
children: [
// 聊天内容
Expanded(
child: Obx(() {
return 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();
}
child: GestureDetector(
onTap: () {
// 点击聊天区域时,收起键盘并切换工具显示状态
FocusScope.of(context).unfocus();
if (controller.isPlusOptionsExpanded.value) {
controller.collapsePlusOptions();
}
},
child: Obx(() {
return 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 index == 0
? IntroStepBuilder(
group: 'ai',
order: 4,
getOverlayPosition:
IntroOverlayBuilder.overlayPosition,
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);
}
},
onTap: () async {
params.onFinish();
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) =>
Padding(
key: key,
final message = controller
.messages[controller.messages.length - 1 - index];
return index == 0
? IntroStepBuilder(
group: 'ai',
order: 4,
getOverlayPosition:
IntroOverlayBuilder.overlayPosition,
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);
}
},
onTap: () async {
params.onFinish();
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) =>
Padding(
key: key,
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onLongPressStart:
(LongPressStartDetails details) {
messageActionController
.onLongPressMessage(context,
isDarkMode, details, message);
},
child: MessageBubble(
sessionid: message.sessionid,
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
functionCalls: message.functionCalls,
isToolCallsExpanded:
message.isToolCallsExpanded,
),
),
),
)
: Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onLongPressStart:
@ -367,35 +408,12 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
message.isToolCallsExpanded,
),
),
),
)
: Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onLongPressStart:
(LongPressStartDetails details) {
messageActionController.onLongPressMessage(
context, isDarkMode, details, message);
},
child: MessageBubble(
sessionid: message.sessionid,
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
functionCalls: message.functionCalls,
isToolCallsExpanded:
message.isToolCallsExpanded,
),
),
);
},
),
);
}),
);
},
),
);
}),
),
),
// 输入区域
@ -553,6 +571,13 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
return GestureDetector(
onTap: () {
controller.toggleInputMode();
// 切换模式时,确保焦点正确
if (!isTextMode) {
// 切换到语音模式时,请求焦点
Future.delayed(Duration.zero, () {
_textFocusNode.requestFocus();
});
}
},
child: Container(
width: 44.w,
@ -652,6 +677,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
controller.isImageInputActive.value;
return TextField(
focusNode: _textFocusNode, // 绑定焦点节点
controller: controller.textController,
maxLines: isImageActive
? 3

1
lib/modules/agent/views/music_card.dart

@ -17,7 +17,6 @@ Widget buildMusicItem(
'coverUrl': image,
};
controller.musicStatus(); //刷新状态
return Container(
padding: EdgeInsets.all(14.w),
decoration: BoxDecoration(

127
lib/modules/login/constants/country_codes.dart

@ -0,0 +1,127 @@
// Country dialing codes list extracted for maintainability and reuse.
final List<Map<String, String>> kCountryCodes = [
{'name': '邮箱', 'code': '', 'flag': '✉️'}, // 保持首位
{'name': '中国', 'code': '+86', 'flag': '🇨🇳'}, // 保持第二位
// 按国家代码数字升序排列(去除+号后排序)
{'name': '美国', 'code': '+1', 'flag': '🇺🇸'},
{'name': '加拿大', 'code': '+1', 'flag': '🇨🇦'},
{'name': '俄罗斯', 'code': '+7', 'flag': '🇷🇺'},
{'name': '英国', 'code': '+44', 'flag': '🇬🇧'},
{'name': '德国', 'code': '+49', 'flag': '🇩🇪'},
{'name': '意大利', 'code': '+39', 'flag': '🇮🇹'},
{'name': '法国', 'code': '+33', 'flag': '🇫🇷'},
{'name': '西班牙', 'code': '+34', 'flag': '🇪🇸'},
{'name': '南非', 'code': '+27', 'flag': '🇿🇦'},
{'name': '沙特阿拉伯', 'code': '+966', 'flag': '🇸🇦'},
{'name': '土耳其', 'code': '+90', 'flag': '🇹🇷'},
{'name': '日本', 'code': '+81', 'flag': '🇯🇵'},
{'name': '韩国', 'code': '+82', 'flag': '🇰🇷'},
{'name': '新加坡', 'code': '+65', 'flag': '🇸🇬'},
{'name': '马来西亚', 'code': '+60', 'flag': '🇲🇾'},
{'name': '泰国', 'code': '+66', 'flag': '🇹🇭'},
{'name': '越南', 'code': '+84', 'flag': '🇻🇳'},
{'name': '印度', 'code': '+91', 'flag': '🇮🇳'},
{'name': '巴基斯坦', 'code': '+92', 'flag': '🇵🇰'},
{'name': '菲律宾', 'code': '+63', 'flag': '🇵🇭'},
{'name': '印度尼西亚', 'code': '+62', 'flag': '🇮🇩'},
{'name': '阿联酋', 'code': '+971', 'flag': '🇦🇪'},
{'name': '墨西哥', 'code': '+52', 'flag': '🇲🇽'},
{'name': '巴西', 'code': '+55', 'flag': '🇧🇷'},
{'name': '阿根廷', 'code': '+54', 'flag': '🇦🇷'},
{'name': '澳大利亚', 'code': '+61', 'flag': '🇦🇺'},
{'name': '新西兰', 'code': '+64', 'flag': '🇳🇿'},
// 中国地区(保持最后)
{'name': '中国香港', 'code': '+852', 'flag': '🇭🇰'},
{'name': '中国澳门', 'code': '+853', 'flag': '🇲🇴'},
{'name': '中国台湾', 'code': '+886', 'flag': '🇹🇼'},
// 添加的其他主要国家
{'name': '埃及', 'code': '+20', 'flag': '🇪🇬'},
{'name': '希腊', 'code': '+30', 'flag': '🇬🇷'},
{'name': '荷兰', 'code': '+31', 'flag': '🇳🇱'},
{'name': '比利时', 'code': '+32', 'flag': '🇧🇪'},
{'name': '匈牙利', 'code': '+36', 'flag': '🇭🇺'},
{'name': '罗马尼亚', 'code': '+40', 'flag': '🇷🇴'},
{'name': '瑞士', 'code': '+41', 'flag': '🇨🇭'},
{'name': '奥地利', 'code': '+43', 'flag': '🇦🇹'},
{'name': '丹麦', 'code': '+45', 'flag': '🇩🇰'},
{'name': '瑞典', 'code': '+46', 'flag': '🇸🇪'},
{'name': '挪威', 'code': '+47', 'flag': '🇳🇴'},
{'name': '波兰', 'code': '+48', 'flag': '🇵🇱'},
{'name': '秘鲁', 'code': '+51', 'flag': '🇵🇪'},
{'name': '智利', 'code': '+56', 'flag': '🇨🇱'},
{'name': '哥伦比亚', 'code': '+57', 'flag': '🇨🇴'},
{'name': '委内瑞拉', 'code': '+58', 'flag': '🇻🇪'},
{'name': '葡萄牙', 'code': '+351', 'flag': '🇵🇹'},
{'name': '卢森堡', 'code': '+352', 'flag': '🇱🇺'},
{'name': '爱尔兰', 'code': '+353', 'flag': '🇮🇪'},
{'name': '冰岛', 'code': '+354', 'flag': '🇮🇸'},
{'name': '阿尔巴尼亚', 'code': '+355', 'flag': '🇦🇱'},
{'name': '马耳他', 'code': '+356', 'flag': '🇲🇹'},
{'name': '塞浦路斯', 'code': '+357', 'flag': '🇨🇾'},
{'name': '芬兰', 'code': '+358', 'flag': '🇫🇮'},
{'name': '保加利亚', 'code': '+359', 'flag': '🇧🇬'},
{'name': '立陶宛', 'code': '+370', 'flag': '🇱🇹'},
{'name': '拉脱维亚', 'code': '+371', 'flag': '🇱🇻'},
{'name': '爱沙尼亚', 'code': '+372', 'flag': '🇪🇪'},
{'name': '乌克兰', 'code': '+380', 'flag': '🇺🇦'},
{'name': '塞尔维亚', 'code': '+381', 'flag': '🇷🇸'},
{'name': '克罗地亚', 'code': '+385', 'flag': '🇭🇷'},
{'name': '斯洛文尼亚', 'code': '+386', 'flag': '🇸🇮'},
{'name': '捷克', 'code': '+420', 'flag': '🇨🇿'},
{'name': '斯洛伐克', 'code': '+421', 'flag': '🇸🇰'},
{'name': '危地马拉', 'code': '+502', 'flag': '🇬🇹'},
{'name': '萨尔瓦多', 'code': '+503', 'flag': '🇸🇻'},
{'name': '洪都拉斯', 'code': '+504', 'flag': '🇭🇳'},
{'name': '尼加拉瓜', 'code': '+505', 'flag': '🇳🇮'},
{'name': '哥斯达黎加', 'code': '+506', 'flag': '🇨🇷'},
{'name': '巴拿马', 'code': '+507', 'flag': '🇵🇦'},
{'name': '海地', 'code': '+509', 'flag': '🇭🇹'},
{'name': '玻利维亚', 'code': '+591', 'flag': '🇧🇴'},
{'name': '厄瓜多尔', 'code': '+593', 'flag': '🇪🇨'},
{'name': '巴拉圭', 'code': '+595', 'flag': '🇵🇾'},
{'name': '乌拉圭', 'code': '+598', 'flag': '🇺🇾'},
{'name': '摩洛哥', 'code': '+212', 'flag': '🇲🇦'},
{'name': '阿尔及利亚', 'code': '+213', 'flag': '🇩🇿'},
{'name': '突尼斯', 'code': '+216', 'flag': '🇹🇳'},
{'name': '尼日利亚', 'code': '+234', 'flag': '🇳🇬'},
{'name': '加纳', 'code': '+233', 'flag': '🇬🇭'},
{'name': '埃塞俄比亚', 'code': '+251', 'flag': '🇪🇹'},
{'name': '肯尼亚', 'code': '+254', 'flag': '🇰🇪'},
{'name': '坦桑尼亚', 'code': '+255', 'flag': '🇹🇿'},
{'name': '乌干达', 'code': '+256', 'flag': '🇺🇬'},
{'name': '孟加拉国', 'code': '+880', 'flag': '🇧🇩'},
{'name': '以色列', 'code': '+972', 'flag': '🇮🇱'},
{'name': '黎巴嫩', 'code': '+961', 'flag': '🇱🇧'},
{'name': '约旦', 'code': '+962', 'flag': '🇯🇴'},
{'name': '伊拉克', 'code': '+964', 'flag': '🇮🇶'},
{'name': '科威特', 'code': '+965', 'flag': '🇰🇼'},
{'name': '巴林', 'code': '+973', 'flag': '🇧🇭'},
{'name': '卡塔尔', 'code': '+974', 'flag': '🇶🇦'},
{'name': '阿曼', 'code': '+968', 'flag': '🇴🇲'},
{'name': '阿富汗', 'code': '+93', 'flag': '🇦🇫'},
{'name': '斯里兰卡', 'code': '+94', 'flag': '🇱🇰'},
{'name': '缅甸', 'code': '+95', 'flag': '🇲🇲'},
{'name': '尼泊尔', 'code': '+977', 'flag': '🇳🇵'},
{'name': '蒙古', 'code': '+976', 'flag': '🇲🇳'},
{'name': '柬埔寨', 'code': '+855', 'flag': '🇰🇭'},
{'name': '老挝', 'code': '+856', 'flag': '🇱🇦'},
{'name': '文莱', 'code': '+673', 'flag': '🇧🇳'},
{'name': '东帝汶', 'code': '+670', 'flag': '🇹🇱'},
{'name': '斐济', 'code': '+679', 'flag': '🇫🇯'},
{'name': '所罗门群岛', 'code': '+677', 'flag': '🇸🇧'},
{'name': '刚果民主共和国', 'code': '+243', 'flag': '🇨🇩'},
{'name': '安哥拉', 'code': '+244', 'flag': '🇦🇴'},
{'name': '苏丹', 'code': '+249', 'flag': '🇸🇩'},
{'name': '马尔代夫', 'code': '+960', 'flag': '🇲🇻'},
{'name': '不丹', 'code': '+975', 'flag': '🇧🇹'},
{'name': '摩纳哥', 'code': '+377', 'flag': '🇲🇨'},
{'name': '圣马力诺', 'code': '+378', 'flag': '🇸🇲'},
{'name': '牙买加', 'code': '+1-876', 'flag': '🇯🇲'},
{'name': '巴哈马', 'code': '+1-242', 'flag': '🇧🇸'},
{'name': '巴布亚新几内亚', 'code': '+675', 'flag': '🇵🇬'},
{'name': '萨摩亚', 'code': '+685', 'flag': '🇼🇸'},
];

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

@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart';
import 'package:logger/web.dart';
import 'package:get_storage/get_storage.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:crypto/crypto.dart'; // 用于生成设备指纹
import 'dart:convert';
import '../../../data/services/db/sqflite_api.dart';
import 'facebookLogin.dart';
@ -21,6 +22,7 @@ import '../../../../data/models/user_Info.dart';
import '../../../data/models/channel_app_model.dart';
import '../../../data/services/version_update_service.dart';
import '../../../modules/common/widgets/privacy_policy_dialog.dart';
import '../constants/country_codes.dart';
enum LoginViewState {
welcome, // 欢迎页面
@ -42,7 +44,7 @@ class LoginController extends GetxController {
UserChannel _userChannel = UserChannel.OFFICIAL; // 默认渠道
final isChinaMainland = false.obs; //是否中国大陆地区
final _hasAcceptedPrivacy = false.obs;
final _hasAcceptedPrivacy = false.obs; //是否同意隐私政策
bool get hasAcceptedPrivacy => _hasAcceptedPrivacy.value;
set hasAcceptedPrivacy(bool value) => _hasAcceptedPrivacy.value = value;
@ -53,131 +55,7 @@ class LoginController extends GetxController {
String get selectedCountryCode => _selectedCountryCode.value;
String get selectedCountryFlag => _selectedCountryFlag.value;
// 预设国家编码列表
final List<Map<String, String>> countryCodes = [
{'name': '邮箱', 'code': '', 'flag': '✉️'}, // 保持首位
{'name': '中国', 'code': '+86', 'flag': '🇨🇳'}, // 保持第二位
// 按国家代码数字升序排列(去除+号后排序)
{'name': '美国', 'code': '+1', 'flag': '🇺🇸'},
{'name': '加拿大', 'code': '+1', 'flag': '🇨🇦'},
{'name': '俄罗斯', 'code': '+7', 'flag': '🇷🇺'},
{'name': '英国', 'code': '+44', 'flag': '🇬🇧'},
{'name': '德国', 'code': '+49', 'flag': '🇩🇪'},
{'name': '意大利', 'code': '+39', 'flag': '🇮🇹'},
{'name': '法国', 'code': '+33', 'flag': '🇫🇷'},
{'name': '西班牙', 'code': '+34', 'flag': '🇪🇸'},
{'name': '南非', 'code': '+27', 'flag': '🇿🇦'},
{'name': '沙特阿拉伯', 'code': '+966', 'flag': '🇸🇦'},
{'name': '土耳其', 'code': '+90', 'flag': '🇹🇷'},
{'name': '日本', 'code': '+81', 'flag': '🇯🇵'},
{'name': '韩国', 'code': '+82', 'flag': '🇰🇷'},
{'name': '新加坡', 'code': '+65', 'flag': '🇸🇬'},
{'name': '马来西亚', 'code': '+60', 'flag': '🇲🇾'},
{'name': '泰国', 'code': '+66', 'flag': '🇹🇭'},
{'name': '越南', 'code': '+84', 'flag': '🇻🇳'},
{'name': '印度', 'code': '+91', 'flag': '🇮🇳'},
{'name': '巴基斯坦', 'code': '+92', 'flag': '🇵🇰'},
{'name': '菲律宾', 'code': '+63', 'flag': '🇵🇭'},
{'name': '印度尼西亚', 'code': '+62', 'flag': '🇮🇩'},
{'name': '阿联酋', 'code': '+971', 'flag': '🇦🇪'},
{'name': '墨西哥', 'code': '+52', 'flag': '🇲🇽'},
{'name': '巴西', 'code': '+55', 'flag': '🇧🇷'},
{'name': '阿根廷', 'code': '+54', 'flag': '🇦🇷'},
{'name': '澳大利亚', 'code': '+61', 'flag': '🇦🇺'},
{'name': '新西兰', 'code': '+64', 'flag': '🇳🇿'},
// 中国地区(保持最后)
{'name': '中国香港', 'code': '+852', 'flag': '🇭🇰'},
{'name': '中国澳门', 'code': '+853', 'flag': '🇲🇴'},
{'name': '中国台湾', 'code': '+886', 'flag': '🇹🇼'},
// 添加的其他主要国家
{'name': '埃及', 'code': '+20', 'flag': '🇪🇬'},
{'name': '希腊', 'code': '+30', 'flag': '🇬🇷'},
{'name': '荷兰', 'code': '+31', 'flag': '🇳🇱'},
{'name': '比利时', 'code': '+32', 'flag': '🇧🇪'},
{'name': '匈牙利', 'code': '+36', 'flag': '🇭🇺'},
{'name': '罗马尼亚', 'code': '+40', 'flag': '🇷🇴'},
{'name': '瑞士', 'code': '+41', 'flag': '🇨🇭'},
{'name': '奥地利', 'code': '+43', 'flag': '🇦🇹'},
{'name': '丹麦', 'code': '+45', 'flag': '🇩🇰'},
{'name': '瑞典', 'code': '+46', 'flag': '🇸🇪'},
{'name': '挪威', 'code': '+47', 'flag': '🇳🇴'},
{'name': '波兰', 'code': '+48', 'flag': '🇵🇱'},
{'name': '秘鲁', 'code': '+51', 'flag': '🇵🇪'},
{'name': '智利', 'code': '+56', 'flag': '🇨🇱'},
{'name': '哥伦比亚', 'code': '+57', 'flag': '🇨🇴'},
{'name': '委内瑞拉', 'code': '+58', 'flag': '🇻🇪'},
{'name': '葡萄牙', 'code': '+351', 'flag': '🇵🇹'},
{'name': '卢森堡', 'code': '+352', 'flag': '🇱🇺'},
{'name': '爱尔兰', 'code': '+353', 'flag': '🇮🇪'},
{'name': '冰岛', 'code': '+354', 'flag': '🇮🇸'},
{'name': '阿尔巴尼亚', 'code': '+355', 'flag': '🇦🇱'},
{'name': '马耳他', 'code': '+356', 'flag': '🇲🇹'},
{'name': '塞浦路斯', 'code': '+357', 'flag': '🇨🇾'},
{'name': '芬兰', 'code': '+358', 'flag': '🇫🇮'},
{'name': '保加利亚', 'code': '+359', 'flag': '🇧🇬'},
{'name': '立陶宛', 'code': '+370', 'flag': '🇱🇹'},
{'name': '拉脱维亚', 'code': '+371', 'flag': '🇱🇻'},
{'name': '爱沙尼亚', 'code': '+372', 'flag': '🇪🇪'},
{'name': '乌克兰', 'code': '+380', 'flag': '🇺🇦'},
{'name': '塞尔维亚', 'code': '+381', 'flag': '🇷🇸'},
{'name': '克罗地亚', 'code': '+385', 'flag': '🇭🇷'},
{'name': '斯洛文尼亚', 'code': '+386', 'flag': '🇸🇮'},
{'name': '捷克', 'code': '+420', 'flag': '🇨🇿'},
{'name': '斯洛伐克', 'code': '+421', 'flag': '🇸🇰'},
{'name': '危地马拉', 'code': '+502', 'flag': '🇬🇹'},
{'name': '萨尔瓦多', 'code': '+503', 'flag': '🇸🇻'},
{'name': '洪都拉斯', 'code': '+504', 'flag': '🇭🇳'},
{'name': '尼加拉瓜', 'code': '+505', 'flag': '🇳🇮'},
{'name': '哥斯达黎加', 'code': '+506', 'flag': '🇨🇷'},
{'name': '巴拿马', 'code': '+507', 'flag': '🇵🇦'},
{'name': '海地', 'code': '+509', 'flag': '🇭🇹'},
{'name': '玻利维亚', 'code': '+591', 'flag': '🇧🇴'},
{'name': '厄瓜多尔', 'code': '+593', 'flag': '🇪🇨'},
{'name': '巴拉圭', 'code': '+595', 'flag': '🇵🇾'},
{'name': '乌拉圭', 'code': '+598', 'flag': '🇺🇾'},
{'name': '摩洛哥', 'code': '+212', 'flag': '🇲🇦'},
{'name': '阿尔及利亚', 'code': '+213', 'flag': '🇩🇿'},
{'name': '突尼斯', 'code': '+216', 'flag': '🇹🇳'},
{'name': '尼日利亚', 'code': '+234', 'flag': '🇳🇬'},
{'name': '加纳', 'code': '+233', 'flag': '🇬🇭'},
{'name': '埃塞俄比亚', 'code': '+251', 'flag': '🇪🇹'},
{'name': '肯尼亚', 'code': '+254', 'flag': '🇰🇪'},
{'name': '坦桑尼亚', 'code': '+255', 'flag': '🇹🇿'},
{'name': '乌干达', 'code': '+256', 'flag': '🇺🇬'},
{'name': '孟加拉国', 'code': '+880', 'flag': '🇧🇩'},
{'name': '以色列', 'code': '+972', 'flag': '🇮🇱'},
{'name': '黎巴嫩', 'code': '+961', 'flag': '🇱🇧'},
{'name': '约旦', 'code': '+962', 'flag': '🇯🇴'},
{'name': '伊拉克', 'code': '+964', 'flag': '🇮🇶'},
{'name': '科威特', 'code': '+965', 'flag': '🇰🇼'},
{'name': '巴林', 'code': '+973', 'flag': '🇧🇭'},
{'name': '卡塔尔', 'code': '+974', 'flag': '🇶🇦'},
{'name': '阿曼', 'code': '+968', 'flag': '🇴🇲'},
{'name': '阿富汗', 'code': '+93', 'flag': '🇦🇫'},
{'name': '斯里兰卡', 'code': '+94', 'flag': '🇱🇰'},
{'name': '缅甸', 'code': '+95', 'flag': '🇲🇲'},
{'name': '尼泊尔', 'code': '+977', 'flag': '🇳🇵'},
{'name': '蒙古', 'code': '+976', 'flag': '🇲🇳'},
{'name': '柬埔寨', 'code': '+855', 'flag': '🇰🇭'},
{'name': '老挝', 'code': '+856', 'flag': '🇱🇦'},
{'name': '文莱', 'code': '+673', 'flag': '🇧🇳'},
{'name': '东帝汶', 'code': '+670', 'flag': '🇹🇱'},
{'name': '斐济', 'code': '+679', 'flag': '🇫🇯'},
{'name': '所罗门群岛', 'code': '+677', 'flag': '🇸🇧'},
{'name': '刚果民主共和国', 'code': '+243', 'flag': '🇨🇩'},
{'name': '安哥拉', 'code': '+244', 'flag': '🇦🇴'},
{'name': '苏丹', 'code': '+249', 'flag': '🇸🇩'},
{'name': '马尔代夫', 'code': '+960', 'flag': '🇲🇻'},
{'name': '不丹', 'code': '+975', 'flag': '🇧🇹'},
{'name': '摩纳哥', 'code': '+377', 'flag': '🇲🇨'},
{'name': '圣马力诺', 'code': '+378', 'flag': '🇸🇲'},
{'name': '牙买加', 'code': '+1-876', 'flag': '🇯🇲'},
{'name': '巴哈马', 'code': '+1-242', 'flag': '🇧🇸'},
{'name': '巴布亚新几内亚', 'code': '+675', 'flag': '🇵🇬'},
{'name': '萨摩亚', 'code': '+685', 'flag': '🇼🇸'},
];
final List<Map<String, String>> countryCodes = kCountryCodes;
// 初始化默认选择的国家代码
void _initDefaultCountryCode() {
@ -236,7 +114,7 @@ class LoginController extends GetxController {
_verificationCodeController = TextEditingController();
isChinaMainland.value =
Get.deviceLocale?.countryCode?.toUpperCase() == 'CN';
Get.deviceLocale?.countryCode?.toUpperCase() == 'CN'; //是否中国大陆地区
// 初始化默认国家代码
_initDefaultCountryCode();
@ -304,9 +182,27 @@ class LoginController extends GetxController {
_deviceId = androidInfo.id;
} else if (GetPlatform.isIOS) {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
_deviceId = iosInfo.identifierForVendor ?? 'unknown';
//print('lxm---iosInfo: $iosInfo.toString()');
final utsname = iosInfo.utsname; // 包含更详细的设备信息
//print('lxm---utsname: $utsname');
final machineId = utsname.machine.toLowerCase(); // 例如: "iPhone14,2"
Logger().d('lxm---machineId: $machineId');
final systemVersion = utsname.release; // 例如: "16.4.1"
Logger().d('lxm---systemVersion: $systemVersion');
try {
// 组合 machine 和 systemVersion 生成指纹
final fingerprint = '$machineId|$systemVersion';
final hashedFingerprint =
md5.convert(utf8.encode(fingerprint)).toString();
Logger()
.d('生成设备指纹: $hashedFingerprint (基于 $machineId 和 $systemVersion)');
_deviceId = hashedFingerprint;
} catch (e) {
Logger().e('生成设备指纹失败: $e');
_deviceId = iosInfo.identifierForVendor ?? 'unknown';
}
}
Logger().d('deviceId: $_deviceId');
} catch (e) {
Logger().e('获取设备信息失败: $e');
_deviceId = 'unknown';

1
lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart

@ -5,6 +5,7 @@ import 'package:get/get.dart';
import '../../../../routes/app_routes.dart';
import '../../controllers/meeting_home_controller.dart';
///会议底部导航栏抽屉,提供会议列表、会议详情、会议设置
class NavigationBarBottomSheet extends GetView<MeetingHomeController> {
const NavigationBarBottomSheet({super.key});

26
lib/modules/meeting/views/meeting_details_view.dart

@ -17,6 +17,7 @@ import 'bottomSheet/share_bottom_sheet.dart';
import 'tabs/speech_tab.dart';
import 'tabs/summary_tab.dart';
///会议音频详情页面,提供会议音频播放、会议音频分享、会议音频更多操作
class MeetingDetailsView extends GetView<MeetingDetailsController> {
const MeetingDetailsView({super.key});
@ -40,10 +41,9 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Obx(
() => AppBar(
backgroundColor:
isDarkMode
? (controller.isTop.value ? Colors.black : Colors.grey[900])
: (controller.isTop.value ? Colors.grey[50] : Colors.grey[200]),
backgroundColor: isDarkMode
? (controller.isTop.value ? Colors.black : Colors.grey[900])
: (controller.isTop.value ? Colors.grey[50] : Colors.grey[200]),
surfaceTintColor: Colors.transparent,
elevation: 0.5,
leading: controller.isSpeakerText.value
@ -202,7 +202,8 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
Get.bottomSheet(
const ShareBottomSheet(),
isScrollControlled: true,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
backgroundColor:
isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
@ -234,7 +235,8 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
Get.bottomSheet(
const MoreBottomSheet(),
isScrollControlled: true,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
backgroundColor:
isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
@ -301,10 +303,14 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
child: Obx(
() => Row(
children: [
_iconItem(Icons.summarize, 'overview'.tr, 0), // 对应中文:概览
_iconItem(Icons.message, 'transcription'.tr, 1), // 对应中文:转写
_iconItem(Icons.article, 'summary'.tr, 2), // 对应中文:总结
_iconItem(Icons.lan, 'mindMap'.tr, 3), // 对应中文:思维导图
_iconItem(
Icons.summarize, 'overview'.tr, 0), // 对应中文:概览
_iconItem(Icons.message, 'transcription'.tr,
1), // 对应中文:转写
_iconItem(
Icons.article, 'summary'.tr, 2), // 对应中文:总结
_iconItem(
Icons.lan, 'mindMap'.tr, 3), // 对应中文:思维导图
],
),
),

1
lib/modules/meeting/views/meeting_home_view.dart

@ -11,6 +11,7 @@ import '../model/meeting_model.dart';
import '../utils/meeting_ui_utils.dart';
import 'bottomSheet/edit_title_bottom_sheet.dart';
///会议主页面,提供会议列表、会议详情、会议设置
class MeetingHomeView extends StatefulWidget {
const MeetingHomeView({super.key});

1
lib/modules/meeting/views/meeting_view.dart

@ -9,6 +9,7 @@ import '../utils/meeting_ui_utils.dart';
import 'bottomSheet/filtrate_drawer.dart';
import 'bottomSheet/navigation_bar_bottom_sheet.dart';
///meeting模块主容器页面,提供底部导航栏、浮动按钮、侧板抽屉
class MeetingView extends GetView<MeetingController> {
const MeetingView({super.key});

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

@ -480,13 +480,14 @@ object AgentService : CoroutineScope {
"phone_call" -> {
// 通话模式:禁用空闲检测,保持持续激活
Log.d(TAG, "通话模式:禁用空闲检测")
startIdleCheck(mode,maxIdleSeconds)
return
}
else -> {
// 未知模式:使用默认的空闲检测
Log.w(TAG, "未知识别模式: $mode,使用默认空闲检测")
startIdleCheck(maxIdleSeconds)
startIdleCheck(mode,maxIdleSeconds)
}
}
}
@ -494,7 +495,7 @@ object AgentService : CoroutineScope {
/**
* 启动空闲检测
*/
private fun startIdleCheck(idleSeconds: Int = maxIdleSeconds) {
private fun startIdleCheck(mode: String,idleSeconds: Int = maxIdleSeconds) {
stopIdleCheck() // 先停止现有的检查
if (!isRecognitionActive) return
@ -503,8 +504,8 @@ object AgentService : CoroutineScope {
idleCheckJob = launch {
try {
delay(idleSeconds * 1000L)
if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming) {
Log.d(TAG, "空闲检测触发自动停止,isActive: $isActive, isRecognitionActive: $isRecognitionActive, isTtsSpeaking: $isTtsSpeaking, isAiStreaming: $isAiStreaming, mode: $mode")
if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming && mode != "phone_call") {
Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode")
BleService.closeCodec()
stopRecognition()
@ -517,6 +518,10 @@ object AgentService : CoroutineScope {
)
)
}
if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming && mode == "phone_call") {
Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode")
startRecognition(false, mode)
}
} catch (e: CancellationException) {
// 协程被取消,正常行为
} catch (e: Exception) {
@ -531,6 +536,7 @@ object AgentService : CoroutineScope {
private fun stopIdleCheck() {
idleCheckJob?.cancel()
idleCheckJob = null
Log.d(TAG, "空闲检测已停止")
}
/**
@ -643,11 +649,9 @@ object AgentService : CoroutineScope {
override fun onRecognizing(sessionid:String, recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()&&isKeepResult==true) {
// 检测到语音,更新状态
_hasSpeechDetected.set(true)
Log.d(TAG, "ai识别中重置定时器")
restartIdleCheck()
_hasSpeechDetected.set(true)
Log.d(TAG, "ai识别中重置定时器")
restartIdleCheck()
sendEvent(

75
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -18,6 +18,7 @@ import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.yunqiinnovation.ble_service.BleService
import com.deep_voice.speech.tts.AudioOutputDevice
import com.deep_voice.speech.tts.AudioDataListener
import kotlinx.coroutines.*
import com.yunqiinnovation.azure_speech.tools.RecordFile
/** AzureSpeechPlugin */
@ -105,6 +106,70 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
}
}
}
/**
* 从WAV格式音频数据中提取纯PCM数据
*
* @param wavData WAV格式的音频数据
* @return 纯PCM音频数据
*/
private fun extractPcmFromWav(wavData: ByteArray): ByteArray {
try {
// WAV文件头最小长度为44字节
if (wavData.size < 44) {
FileLogger.e(tag, "WAV数据长度不足,无法解析文件头")
return ByteArray(0)
}
// 检查RIFF标识符
val riffHeader = String(wavData, 0, 4, Charsets.US_ASCII)
if (riffHeader != "RIFF") {
FileLogger.e(tag, "不是有效的RIFF WAV文件,标识符: $riffHeader")
return ByteArray(0)
}
// 检查WAVE标识符
val waveHeader = String(wavData, 8, 4, Charsets.US_ASCII)
if (waveHeader != "WAVE") {
FileLogger.e(tag, "不是有效的WAVE文件,标识符: $waveHeader")
return ByteArray(0)
}
// 查找data chunk
var offset = 12 // 跳过RIFF头部
while (offset < wavData.size - 8) {
val chunkId = String(wavData, offset, 4, Charsets.US_ASCII)
val chunkSize = (wavData[offset + 4].toInt() and 0xFF) or
((wavData[offset + 5].toInt() and 0xFF) shl 8) or
((wavData[offset + 6].toInt() and 0xFF) shl 16) or
((wavData[offset + 7].toInt() and 0xFF) shl 24)
if (chunkId == "data") {
// 找到data chunk,提取PCM数据
val dataOffset = offset + 8
val dataSize = minOf(chunkSize, wavData.size - dataOffset)
if (dataSize > 0) {
val pcmData = ByteArray(dataSize)
System.arraycopy(wavData, dataOffset, pcmData, 0, dataSize)
FileLogger.d(tag, "成功提取PCM数据: ${pcmData.size}字节")
return pcmData
}
break
}
// 移动到下一个chunk
offset += 8 + chunkSize
}
FileLogger.e(tag, "未找到data chunk")
return ByteArray(0)
} catch (e: Exception) {
FileLogger.e(tag, "解析WAV数据异常: ${e.message}")
return ByteArray(0)
}
}
// 音频数据 事件发送方法
private fun sendAudioDataEvent(event: Map<String, Any>) {
if (audioDataEventSink == null) {
@ -937,7 +1002,15 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
}
override fun onSynthesisAudioGenerated(text: String, audioData: ByteArray) {
FileLogger.d(tag, "语音合成音频生成,文本=${text},音频数据大小=${audioData.size}")
BleService.writeExternalAudioData(audioData)
// Azure TTS返回的是RIFF WAV格式,需要去除文件头
val pcmData = extractPcmFromWav(audioData)
if (pcmData.isNotEmpty()) {
FileLogger.d(tag, "提取PCM数据,大小=${pcmData.size}字节")
BleService.writeExternalAudioData(pcmData)
}
else {
FileLogger.e(tag, "提取PCM数据失败,音频数据可能不是WAV格式")
}
}
override fun onSynthesisFailed(text: String, error: String) {
// sendAstEvent(

76
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift

@ -18,7 +18,7 @@ import ble_service
// 日志标签
private let tag = "AzureSpeechPlugin"
// 日志对象
private let log = OSLog(subsystem: "com.azure.speech", category: "AzureSpeechPlugin")
internal let log = OSLog(subsystem: "com.azure.speech", category: "AzureSpeechPlugin")
// MARK: - 防抖相关(iOS 原生层)
// 函数级注释:用于限制 startContinuousRecognition/stopContinuousRecognition 在极短时间内的重复触发
@ -264,6 +264,65 @@ private func sendAstEvent(_ event: [String: Any]) {
sendAudioDataEvent(eventMap)
}
/**
* 从WAV格式音频数据中提取纯PCM数据
* @param wavData WAV格式的音频数据
* @return 纯PCM音频数据,如果提取失败则返回空Data
*/
internal func extractPcmFromWav(_ wavData: Data) -> Data {
// WAV文件头最小长度为44字节
guard wavData.count >= 44 else {
os_log("WAV数据长度不足,无法解析文件头", log: log, type: .error)
return Data()
}
// 检查RIFF标识符
let riffHeader = String(data: wavData.subdata(in: 0..<4), encoding: .ascii) ?? ""
guard riffHeader == "RIFF" else {
os_log("不是有效的RIFF WAV文件,标识符: %@", log: log, type: .error, riffHeader)
return Data()
}
// 检查WAVE标识符
let waveHeader = String(data: wavData.subdata(in: 8..<12), encoding: .ascii) ?? ""
guard waveHeader == "WAVE" else {
os_log("不是有效的WAVE文件,标识符: %@", log: log, type: .error, waveHeader)
return Data()
}
// 查找data chunk
var offset = 12 // 跳过RIFF头部
while offset < wavData.count - 8 {
let chunkIdData = wavData.subdata(in: offset..<(offset + 4))
let chunkId = String(data: chunkIdData, encoding: .ascii) ?? ""
// 读取chunk大小(小端序)
let chunkSizeData = wavData.subdata(in: (offset + 4)..<(offset + 8))
let chunkSize = chunkSizeData.withUnsafeBytes { bytes in
return bytes.load(as: UInt32.self).littleEndian
}
if chunkId == "data" {
// 找到data chunk,提取PCM数据
let dataOffset = offset + 8
let dataSize = min(Int(chunkSize), wavData.count - dataOffset)
if dataSize > 0 {
let pcmData = wavData.subdata(in: dataOffset..<(dataOffset + dataSize))
os_log("成功提取PCM数据: %d字节", log: log, type: .info, pcmData.count)
return pcmData
}
break
}
// 移动到下一个chunk
offset += 8 + Int(chunkSize)
}
os_log("未找到data chunk", log: log, type: .error)
return Data()
}
// MARK: - ASR 方法处理
private func handleAsrMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
@ -950,9 +1009,18 @@ private class AstEventCallback: IntegratedSpeechTranslationService.ServiceEventC
* @param audioData 音频数据
*/
public func onSynthesisAudioGenerated(text: String, audioData: Data) {
print("语音合成音频数据生成回调", text, audioData.count)
// 调用BLE服务写入音频数据
BleService.shared.writeExternalAudioData(data: audioData)
guard let plugin = plugin else { return }
os_log("语音合成音频生成,文本=%@,音频数据大小=%d", log: plugin.log, type: .info, text, audioData.count)
// Azure TTS返回的是RIFF WAV格式,需要去除文件头提取PCM数据
let pcmData = plugin.extractPcmFromWav(audioData)
if !pcmData.isEmpty {
os_log("提取PCM数据,大小=%d字节", log: plugin.log, type: .info, pcmData.count)
BleService.shared.writeExternalAudioData(data: pcmData)
} else {
os_log("提取PCM数据失败,音频数据可能不是WAV格式", log: plugin.log, type: .error)
}
// plugin?.sendAsrEvent([
// "type": "synthesisAudioGenerated",

740
local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift

File diff suppressed because it is too large
Loading…
Cancel
Save