diff --git a/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt b/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt
index 8d4f460e9..b6e861db2 100644
--- a/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt
+++ b/android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt
@@ -19,6 +19,7 @@ class FloatingWindowService : Service() {
private var windowManager: WindowManager? = null
private var floatingView: View? = null
private var isMinimized = true
+ private lateinit var layoutParams: WindowManager.LayoutParams
private lateinit var sourceTextView: TextView
private lateinit var translatedTextView: TextView
@@ -73,7 +74,7 @@ class FloatingWindowService : Service() {
setupClickListeners()
// 设置窗口参数
- val layoutParams = WindowManager.LayoutParams().apply {
+ layoutParams = WindowManager.LayoutParams().apply {
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -255,26 +256,12 @@ class FloatingWindowService : Service() {
}
private fun setupClickListeners() {
- // 最小化状态点击展开
- minimizedView.setOnClickListener {
- isMinimized = false
- updateViewState(false)
- notifyStateChange("expanded")
- }
-
- // 展开状态点击最小化
- expandedView.setOnClickListener {
- isMinimized = true
- updateViewState(true)
- notifyStateChange("minimized")
- }
-
// 长按关闭
floatingView!!.setOnLongClickListener {
closeFloatingWindow()
true
}
-
+
// 拖拽功能
setupDragListener()
}
@@ -284,25 +271,40 @@ class FloatingWindowService : Service() {
var initialY = 0
var initialTouchX = 0f
var initialTouchY = 0f
-
+ var dragging = false
+ val threshold = (4 * resources.displayMetrics.density).toInt()
floatingView!!.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
- val layoutParams = floatingView!!.layoutParams as WindowManager.LayoutParams
initialX = layoutParams.x
initialY = layoutParams.y
initialTouchX = event.rawX
initialTouchY = event.rawY
+ dragging = false
true
}
MotionEvent.ACTION_MOVE -> {
- val layoutParams = floatingView!!.layoutParams as WindowManager.LayoutParams
- layoutParams.x = initialX + (event.rawX - initialTouchX).toInt()
- layoutParams.y = initialY + (event.rawY - initialTouchY).toInt()
+ val dx = (event.rawX - initialTouchX).toInt()
+ val dy = (event.rawY - initialTouchY).toInt()
+ if (Math.abs(dx) > threshold || Math.abs(dy) > threshold) dragging = true
+ val dm = resources.displayMetrics
+ val maxX = dm.widthPixels - (floatingView?.width ?: 0)
+ val maxY = dm.heightPixels - (floatingView?.height ?: 0)
+ layoutParams.x = (initialX + dx).coerceIn(0, maxX)
+ layoutParams.y = (initialY + dy).coerceIn(0, maxY)
windowManager?.updateViewLayout(floatingView, layoutParams)
true
}
- else -> false
+ MotionEvent.ACTION_UP -> {
+ if (!dragging) {
+ isMinimized = !isMinimized
+ updateViewState(isMinimized)
+ notifyStateChange(if (isMinimized) "minimized" else "expanded")
+ }
+ dragging = false
+ true
+ }
+ else -> true
}
}
}
@@ -347,11 +349,10 @@ class FloatingWindowService : Service() {
val x = intent.getIntExtra(EXTRA_X, 0)
val y = intent.getIntExtra(EXTRA_Y, 0)
- floatingView?.let { view ->
- val layoutParams = view.layoutParams as WindowManager.LayoutParams
+ floatingView?.let {
layoutParams.x = x
layoutParams.y = y
- windowManager?.updateViewLayout(view, layoutParams)
+ windowManager?.updateViewLayout(it, layoutParams)
}
}
diff --git a/assets/images/DefaultPicture.jpg b/assets/images/DefaultPicture.jpg
new file mode 100644
index 000000000..ef039c301
Binary files /dev/null and b/assets/images/DefaultPicture.jpg differ
diff --git a/assets/images/defaultPicture.png b/assets/images/defaultPicture.png
new file mode 100644
index 000000000..57953cc0b
Binary files /dev/null and b/assets/images/defaultPicture.png differ
diff --git a/assets/images/headphone_new3.png b/assets/images/headphone_new3.png
deleted file mode 100644
index 6906348ce..000000000
Binary files a/assets/images/headphone_new3.png and /dev/null differ
diff --git a/assets/images/headphone_new4.png b/assets/images/headphone_new4.png
deleted file mode 100644
index 7161868f8..000000000
Binary files a/assets/images/headphone_new4.png and /dev/null differ
diff --git a/ios/Runner/RunnerProfile.entitlements b/ios/Runner/RunnerProfile.entitlements
new file mode 100644
index 000000000..0c67376eb
--- /dev/null
+++ b/ios/Runner/RunnerProfile.entitlements
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart
index ae0534031..e25234505 100644
--- a/lib/core/bindings/initial_binding.dart
+++ b/lib/core/bindings/initial_binding.dart
@@ -1,5 +1,6 @@
-import 'package:voitrans/data/services/pay_service.dart';
-import 'package:voitrans/data/services/qq_music.dart';
+import '../../data/services/meeting/summary_task_service.dart';
+import '/data/services/pay_service.dart';
+import '/data/services/qq_music.dart';
import '../../../data/services/user_portrait.dart';
@@ -70,6 +71,9 @@ class InitialBinding extends Bindings {
// 会议助手任务服务
Get.lazyPut(() => MeetingTaskService(), fenix: true);
+ // 会议助手总结任务服务
+ Get.lazyPut(() => SummaryTaskService(), fenix: true);
+
// 会议助手音频上传
Get.lazyPut(() => MeetingUploadService(),
fenix: true);
diff --git a/lib/core/widgets/common_bottom_nav.dart b/lib/core/widgets/common_bottom_nav.dart
index 04c9d5b31..c1946f17f 100644
--- a/lib/core/widgets/common_bottom_nav.dart
+++ b/lib/core/widgets/common_bottom_nav.dart
@@ -17,8 +17,8 @@ class CommonBottomNav extends StatelessWidget {
return BottomNavigationBar(
currentIndex: currentIndex,
onTap: (index) {
- // Navigate to the appropriate page based on index
- if(index ==currentIndex) return;//如果是当前页面,不跳转
+ // 点击当前项不执行跳转
+ if (index == currentIndex) return; //如果是当前页面,不跳转
switch (index) {
case 0:
Get.offAllNamed(Routes.home);
diff --git a/lib/data/models/appconfig_model.dart b/lib/data/models/appconfig_model.dart
index 65ea7838e..e656a62c9 100644
--- a/lib/data/models/appconfig_model.dart
+++ b/lib/data/models/appconfig_model.dart
@@ -79,6 +79,7 @@ class DBProduct {
final String? productimage;
final String scanuuid;
final String? version;
+ bool? isforceupdate;
final bool? iscontinuouschat;
final String? updatedescription;
final String? updatepackageaddress;
@@ -91,6 +92,7 @@ class DBProduct {
required this.productimage,
required this.scanuuid,
required this.version,
+ required this.isforceupdate, //该产品固件版本是否强制更新
required this.iscontinuouschat,
required this.updatedescription,
required this.updatepackageaddress,
diff --git a/lib/data/models/appconfig_model.g.dart b/lib/data/models/appconfig_model.g.dart
index f99ba35c6..fc97723ed 100644
--- a/lib/data/models/appconfig_model.g.dart
+++ b/lib/data/models/appconfig_model.g.dart
@@ -79,6 +79,7 @@ DBProduct _$DBProductFromJson(Map json) => DBProduct(
productimage: json['productimage'] as String?,
scanuuid: json['scanuuid'] as String,
version: json['version'] as String?,
+ isforceupdate: json['isforceupdate'] as bool?,
iscontinuouschat: json['iscontinuouschat'] as bool?,
updatedescription: json['updatedescription'] as String?,
updatepackageaddress: json['updatepackageaddress'] as String?,
@@ -91,6 +92,7 @@ Map _$DBProductToJson(DBProduct instance) => {
'productimage': instance.productimage,
'scanuuid': instance.scanuuid,
'version': instance.version,
+ 'isforceupdate': instance.isforceupdate,
'iscontinuouschat': instance.iscontinuouschat,
'devicename': instance.devicename,
'updatedescription': instance.updatedescription,
diff --git a/lib/data/services/account_service.dart b/lib/data/services/account_service.dart
index 419749ee4..c2c9748bd 100644
--- a/lib/data/services/account_service.dart
+++ b/lib/data/services/account_service.dart
@@ -6,6 +6,7 @@ import '../../modules/login/controllers/googleLogin.dart';
import '../../modules/login/controllers/facebookLogin.dart';
import '../services/network/api.dart';
import 'db/database_helper.dart';
+import 'meeting/meeting_task_service.dart';
/// 账号管理服务
///
@@ -79,6 +80,10 @@ class AccountService extends GetxService {
/// 返回操作结果,包含成功状态和错误信息
Future signOut() async {
try {
+ // 清除会议记录任务列表
+ final taskService = Get.find();
+ taskService.cancelTask();
+
// 上传会议记录操作记录
await Synchrodata.uploadOperationRecords();
diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart
index 5b9f3754c..09eb7b842 100644
--- a/lib/data/services/ble_manager.dart
+++ b/lib/data/services/ble_manager.dart
@@ -8,8 +8,8 @@ import 'package:get_storage/get_storage.dart';
import 'package:get/get.dart';
import 'package:ble_service/ble_service.dart';
import 'package:agent_service/agent_service.dart';
-import '../../data/models/appconfig_model.dart';
-import '../../data/models/user_Info.dart' as userInfo;
+import '/data/models/appconfig_model.dart';
+
import '../../data/models/appconfig.dart';
import '../../core/utils/logger.dart';
import 'bluetooth_manager.dart';
@@ -27,6 +27,67 @@ class BleCommands {
static const int CONTROL_CODEC = 0x05;
}
+/// 判断编解码控制指令是否成功
+///
+/// 根据 rawData 第二字节是否为非 0 来判断:
+/// - 成功:第二字节不为 0
+/// - 失败或无法解析:返回 false
+bool _isCodecControlSuccess(Map data) {
+ final dynamic raw = data['rawData'];
+ if (raw == null) return false;
+
+ int? secondByte;
+
+ if (raw is List) {
+ if (raw.length >= 2) {
+ final s = raw[1];
+ if (s is int) {
+ secondByte = s;
+ } else if (s is num) {
+ secondByte = s.toInt();
+ } else if (s is String) {
+ final t = s.trim();
+ if (t.startsWith('0x') || t.startsWith('0X')) {
+ secondByte = int.tryParse(t.substring(2), radix: 16);
+ } else {
+ secondByte = int.tryParse(t);
+ }
+ }
+ }
+ } else if (raw is String) {
+ final normalized = raw.trim().replaceAll('[', '').replaceAll(']', '');
+ final parts =
+ normalized.split(RegExp(r'[,\s]+')).where((e) => e.isNotEmpty).toList();
+ if (parts.length >= 2) {
+ final t = parts[1].trim();
+ if (t.startsWith('0x') || t.startsWith('0X')) {
+ secondByte = int.tryParse(t.substring(2), radix: 16);
+ } else {
+ secondByte = int.tryParse(t);
+ }
+ }
+ } else if (raw is Map) {
+ final dynamic s = raw['second'] ?? raw['status2'] ?? raw['result'];
+ if (s is int) {
+ secondByte = s;
+ } else if (s is num) {
+ secondByte = s.toInt();
+ } else if (s is String) {
+ final t = s.trim();
+ if (t.startsWith('0x') || t.startsWith('0X')) {
+ secondByte = int.tryParse(t.substring(2), radix: 16);
+ } else {
+ secondByte = int.tryParse(t);
+ }
+ }
+ } else {
+ // 无法解析的类型
+ return false;
+ }
+
+ return secondByte != null && secondByte != 0;
+}
+
/// 蓝牙设备信息缓存模型
class DeviceInfo {
final String address;
@@ -135,6 +196,11 @@ class BleManager extends GetxService {
// 设备信息缓存
final deviceInfo = Rx(null); // 使用 Rx 包装
+ //添加编码器状态跟踪
+ final _isCodecActive = false.obs;
+ bool get isCodecActive => _isCodecActive.value;
+ RxBool get isCodecActiveRx => _isCodecActive;
+
// 订阅
StreamSubscription? _statusSubscription;
StreamSubscription? _dataSubscription;
@@ -306,7 +372,18 @@ class BleManager extends GetxService {
Logger.info('lxm--处理编解码控制命令');
// 处理通话状态
print(" CONTROL_CODEC: $commandId,$data");
- //_isCall.value = data['callStatus'] as bool? ?? false;
+
+ final bool codecSuccess = _isCodecControlSuccess(data);
+ _isCodecActive.value = codecSuccess; // 更新编解码器状态
+
+ if (codecSuccess) {
+ print("lxm--编解码控制成功 (rawData 第二字节非 0)");
+ Logger.info('lxm--编解码控制成功 (rawData 第二字节非 0)');
+ } else {
+ print("lxm--编解码控制失败 (rawData 为 [162, 0] 或无法解析)");
+ Logger.warning('lxm--编解码控制失败 (rawData 为 [162, 0] 或无法解析)');
+ }
+
break;
default:
Logger.warning('lxm--未处理的命令ID: $commandId,数据: $data');
@@ -1091,6 +1168,11 @@ class BleManager extends GetxService {
List? uuids,
}) async {
try {
+ // 在注册新设备前先清除旧设备信息
+ if (macAddress != null && macAddress != currentDeviceAddress) {
+ clearDeviceInfo(); // 清除上一个设备的信息
+ }
+
// 保存设备地址信息
if (macAddress != null) {
currentDeviceAddress = macAddress;
diff --git a/lib/data/services/deapsound_ai_service.dart b/lib/data/services/deapsound_ai_service.dart
index da7181f4f..311af1184 100644
--- a/lib/data/services/deapsound_ai_service.dart
+++ b/lib/data/services/deapsound_ai_service.dart
@@ -78,6 +78,10 @@ class DeapsoundAIService implements AiService {
..body = jsonEncode(requestBody);
final response = await client.send(request);
+ if (response.statusCode != 200) {
+ throw Exception('Unknown error');
+ }
+
await for (final chunk in response.stream
.transform(utf8.decoder)
.transform(const LineSplitter())) {
diff --git a/lib/data/services/meeting/meeting_task_service.dart b/lib/data/services/meeting/meeting_task_service.dart
index fc6f6b8fc..b17d6adf0 100644
--- a/lib/data/services/meeting/meeting_task_service.dart
+++ b/lib/data/services/meeting/meeting_task_service.dart
@@ -8,16 +8,16 @@ import 'package:http/http.dart' as http;
import '../../../core/utils/logger.dart';
import '../../../core/utils/synchrodata.dart';
import '../db/sqflite_api.dart';
-import '../deapsound_ai_service.dart';
import 'meeting_upload_service.dart';
-import '../usage_stats_service.dart';
-import '../../models/meeting_usage_stats.dart';
+import '../../../data/models/appconfig.dart';
+import 'summary_task_service.dart';
///会议任务服务类,负责管理会议音频的转写任务处理流程,包括任务提交、状态轮询、结果处理和AI分析等功能。
class MeetingTaskService extends GetxService {
final GetStorage _storage = GetStorage();
MeetingUploadService get _uploadService => Get.find();
+ SummaryTaskService get _summaryTaskService => Get.find();
List _taskList = []; //待处理的会议任务列表
RxList taskFinishList = [].obs; //已处理的会议任务列表
@@ -26,42 +26,84 @@ class MeetingTaskService extends GetxService {
String _userId = '';
+ // final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc'; //
+ // final String _cluster = 'volc_auc_video_flash';
//字节跳动(火山引擎)语音服务配置
- final String _appid = '2316081933';
- final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En';
- 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地址
+ // final String _appid = '2316081933';
+ // final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En';
+ // final String _cluster1 = 'volc.bigasr.auc';
+
+ // final String _serviceUrl1 =
+ // 'https://openspeech.bytedance.com/api/v3/auc/bigmodel'; //大模型录音文件识别api地址
+ late final String _appid;
+ late final String _token;
+ late final String _cluster1;
+ late final String _serviceUrl1;
+ late final String _modelName;
+ late final String _modelVersion;
Map _headers = {};
Map _headers1 = {};
int meetingId = 0;
RxInt tasktype = 1.obs; //任务类型,1-转写中,2-总结中,3-完成
List textList = []; //转写结果列表
- String summary = ''; //AI总结内容
- String overview = ''; //AI概览内容
@override
void onInit() {
super.onInit();
- _headers = {
- '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': '',
- 'X-Api-Sequence': '-1',
- };
+
+ _initialize();
_getAgent();
_getTaskList();
}
+ void cancelTask() {
+ _isTask = false;
+ _taskList.clear();
+ _storage.remove("meeting_task_list");
+ _storage.remove("meeting_task_finish_list");
+ _uploadService.cancelUpload();
+ _summaryTaskService.cancelTask();
+ }
+
+ Future _initialize() async {
+ try {
+ _appid = AppConfig.env('VOLC_OPENSPEECH_APP_ID') ?? '';
+ _token = AppConfig.env('VOLC_OPENSPEECH_ACCESS_TOKEN') ?? '';
+ _cluster1 = AppConfig.env('VOLC_OPENSPEECH_CLUSTER_BIGMODEL') ?? '';
+ _serviceUrl1 = AppConfig.env('VOLC_OPENSPEECH_BASE_URL_BIGMODEL') ?? '';
+ _modelName = AppConfig.env('VOLC_OPENSPEECH_MODEL_NAME') ?? 'bigmodel';
+ _modelVersion = AppConfig.env('VOLC_OPENSPEECH_MODEL_VERSION') ?? '400';
+
+ if (_appid.isEmpty ||
+ _token.isEmpty ||
+ _cluster1.isEmpty ||
+ _serviceUrl1.isEmpty ||
+ _modelName.isEmpty ||
+ _modelVersion.isEmpty) {
+ Logger.error('初始化会议任务服务失败:配置参数为空');
+ return false;
+ }
+
+ _headers = {
+ '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': '',
+ 'X-Api-Sequence': '-1',
+ };
+ } catch (e) {
+ Logger.error('初始化会议任务服务失败:$e');
+ return false;
+ }
+ return true;
+ }
+
void _getAgent() async {
Map userInfo = _storage.read("user_info");
_userId = userInfo['user']['uid'];
@@ -188,6 +230,7 @@ class MeetingTaskService extends GetxService {
if (isDeleteTask) {
_taskList.removeWhere((element) => element['id'] == id);
_storage.write("meeting_task_list", _taskList);
+ _summaryTaskService.removeTask(id);
}
}
@@ -210,8 +253,8 @@ class MeetingTaskService extends GetxService {
"format": format,
},
"request": {
- "model_name": "bigmodel", // 模型名称,目前只有bigmodel
- "model_version": "400", // 模型版本,400模型性能略有提升,且ITN有较大优化
+ "model_name": _modelName, // 模型名称,目前只有bigmodel
+ "model_version": _modelVersion, // 模型版本,400模型性能略有提升,且ITN有较大优化
//"enable_itn": true, // 启用文本规范化,默认为true
"enable_punc": true, // 启用标点,默认为false
//"enable_ddc": false, // 启用语义顺滑,默认为false
@@ -226,24 +269,24 @@ class MeetingTaskService extends GetxService {
Logger.info("lxm--大模型asr请求体$requestBody1");
- final requestBody = jsonEncode({
- "app": {
- "appid": _appid,
- "token": _token,
- "cluster": _cluster,
- },
- "user": {
- "uid": _userId,
- },
- "audio": {
- "format": format,
- "url": audiourl,
- },
- "additions": {
- "language": language,
- "with_speaker_info": isSpeaker ? "True" : "False",
- }
- });
+ // final requestBody = jsonEncode({
+ // "app": {
+ // "appid": _appid,
+ // "token": _token,
+ // "cluster": _cluster,
+ // },
+ // "user": {
+ // "uid": _userId,
+ // },
+ // "audio": {
+ // "format": format,
+ // "url": audiourl,
+ // },
+ // "additions": {
+ // "language": language,
+ // "with_speaker_info": isSpeaker ? "True" : "False",
+ // }
+ // });
try {
final response = await http.post(
@@ -280,12 +323,12 @@ class MeetingTaskService extends GetxService {
///轮询语音转文字任务状态
///[taskId] 任务ID
Future