Browse Source

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

newdev_shunjiawei
lxm 1 year ago
parent
commit
f9a72d6fc2
  1. 28
      lib/data/models/user_Info.dart
  2. 9
      lib/data/services/network/api.dart
  3. 14
      lib/data/services/speech_impl/azure_tts_service.dart
  4. 8
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  5. 39
      lib/data/services/tts_service.dart
  6. 129
      lib/modules/pairing/controllers/pairing_controller.dart
  7. 51
      lib/modules/pairing/views/pairing_view.dart
  8. 73
      lib/modules/pairing/views/pairing_view_new.dart
  9. 3
      lib/modules/settings/views/settings_view.dart
  10. 6
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  11. 6
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

28
lib/data/models/user_Info.dart

@ -14,14 +14,15 @@ class User {
String name;
String uid;
String avatar;
String deviceid;
String language;
String token;
String mail;
String phone;
String phonemac;
String password;
int membershipLevel = 0; //添加会员等级字段,0表示普通用户
int viplv = 0; //添加会员等级字段,0表示普通用户
String earphonemac;
String earphoneauthcode;
// final Int64 lastbettime;
// final Int64 createtime;
// final String wxopenid;
@ -33,14 +34,15 @@ class User {
required this.name,
required this.uid,
required this.avatar,
required this.deviceid,
required this.language,
required this.token,
required this.mail,
required this.phone,
required this.phonemac,
required this.password,
this.membershipLevel = 0, // 添加默认值
required this.viplv, // 添加默认值
required this.earphonemac,
required this.earphoneauthcode,
// required this.lastbettime,
// required this.createtime,
// required this.wxopenid,
@ -60,14 +62,16 @@ class User {
name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值
uid: (userData?['uid'] as String?) ?? '',
avatar: (userData?['avatar'] as String?) ?? '',
deviceid: (userData?['deviceid'] as String?) ?? '',
language: (userData?['language'] as String?) ?? 'en',
mail: (userData?['mail'] as String?) ?? '',
phone: (userData?['phone'] as String?) ?? '',
phonemac: (userData?['phonemac'] as String?) ?? '',
password: (userData?['password'] as String?) ?? '',
membershipLevel: (userData?['membershipLevel'] as int?) ?? 0, // 提供默认值
viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值
earphonemac: (userData?['earphonemac'] as String?) ?? '',
earphoneauthcode: (userData?['earphoneauthcode'] as String?) ?? '',
// lastbettime: _parseTimestamp(userData['lastbettime']),
// createtime: _parseTimestamp(userData['createtime']),
// wxopenid: (userData?['wxopenid'] as String?) ?? '',
@ -85,13 +89,16 @@ class User {
name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值
uid: (userData?['uid'] as String?) ?? '',
avatar: (userData?['avatar'] as String?) ?? '',
deviceid: (userData?['deviceid'] as String?) ?? '',
language: (userData?['language'] as String?) ?? 'en',
mail: (userData?['mail'] as String?) ?? '',
phone: (userData?['phone'] as String?) ?? '',
phonemac: (userData?['phonemac'] as String?) ?? '',
password: (userData?['password'] as String?) ?? '',
viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值
earphonemac: (userData?['earphonemac'] as String?) ?? '',
earphoneauthcode: (userData?['earphoneauthcode'] as String?) ?? '',
// lastbettime: _parseTimestamp(userData['lastbettime']),
// createtime: _parseTimestamp(userData['createtime']),
// wxopenid: (userData?['wxopenid'] as String?) ?? '',
@ -106,14 +113,17 @@ class User {
'name': name,
'uid': uid,
'avatar': avatar,
'deviceid': deviceid,
'language': language,
'token': token,
'mail': mail,
'phone': phone,
'phonemac': phonemac,
'password': password,
'membershipLevel': membershipLevel,
'viplv': viplv,
'earphonemac': earphonemac,
'earphoneauthcode': earphoneauthcode,
// 'lastbettime': lastbettime,
// 'createtime': createtime,
// 'wxopenid': wxopenid,

9
lib/data/services/network/api.dart

@ -126,4 +126,13 @@ class Api {
params: params,
);
}
//绑定设备
static bindearphone([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_bindearphone',
params: params,
);
}
}

14
lib/data/services/speech_impl/azure_tts_service.dart

@ -413,4 +413,16 @@ class AzureTtsService extends GetxService implements TtsService {
Logger.error('释放资源失败: ${e.toString()}');
}
}
}
@override
Future<bool> setAudioOutputDevice() async {
try {
// 开始合成,传递参数
final result = await _channel.invokeMethod('setAudioOutputDevice');
return result;
} catch (e) {
Logger.error('音频输出设备类型失败: ${e.toString()}');
return false;
}
}
}

8
lib/data/services/speech_impl/voice_clone_tts_service.dart

@ -390,4 +390,10 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
Logger.error('释放资源失败: ${e.toString()}');
}
}
}
@override
Future<bool> setAudioOutputDevice() {
// TODO: implement setAudioOutputDevice
throw UnimplementedError();
}
}

39
lib/data/services/tts_service.dart

@ -2,52 +2,51 @@ import 'dart:async';
/// 语音合成服务接口
abstract class TtsService {
/// 当前选择的语音
String get currentVoice;
/// 当前是否正在播放
bool get isSpeaking;
/// TTS事件流,用于监听语音合成事件
Stream<TtsEvent> get onEvent;
/// 初始化语音合成服务
Future<bool> initialize({
List<String>? supportedLanguages,
});
/// 设置语音
Future<bool> setVoice(String voiceName);
/// 单次播放文本,完成后返回
Future<bool> speakOnce(String text);
/// 将文本加入播放队列,支持连续播放多段文本
Future<bool> speakStream(String text);
/// 播放队列中剩余文本
Future<bool> flushStream();
/// 停止播放
Future<bool> stop();
/// 释放资源
Future<void> dispose();
Future<bool> setAudioOutputDevice();
}
/// TTS事件类型
enum TtsEventType {
/// 开始播放
started,
/// 播放完成
completed,
/// 播放取消
canceled,
/// 播放错误
error,
}
@ -56,15 +55,15 @@ enum TtsEventType {
class TtsEvent {
/// 事件类型
final TtsEventType type;
/// 错误信息(仅在 error 类型中有效)
final String error;
TtsEvent({
required this.type,
this.error = '',
});
/// 创建错误事件的快捷构造函数
factory TtsEvent.error(String errorMessage) {
return TtsEvent(
@ -72,12 +71,12 @@ class TtsEvent {
error: errorMessage,
);
}
/// 检查是否为错误
bool get isError => type == TtsEventType.error;
@override
String toString() {
return 'TtsEvent{type: $type, error: $error}';
}
}
}

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

@ -1,4 +1,7 @@
import 'package:app_settings/app_settings.dart';
import 'package:deep_voice/data/models/user_Info.dart';
import 'package:deep_voice/data/services/network/api.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:async';
import 'package:permission_handler/permission_handler.dart';
@ -15,9 +18,10 @@ enum PairingState {
checking, // 检查中(蓝牙、权限等)
waitingClassic, // 等待经典蓝牙连接
verifying, // 验证设备
manualbinding, //手动绑定设备
associating, // 正在进行BLE设备关联
waitingBle, // 等待BLE连接
completed, // 配对完成
waitingBle, //等待BLE连接
completed, //配对完成
error, // 错误状态
}
@ -31,6 +35,7 @@ enum ErrorType {
deviceBound, // 设备已被绑定
associationFailed, // 关联失败
bleTimeout, // BLE连接超时
authnFailed, // 授权失败
networkError, // 网络错误
}
@ -53,6 +58,7 @@ class PairingController extends GetxController {
final isLoading = false.obs;
final showConnectButton = false.obs;
final showRetryButton = false.obs;
final showAutoCodeInput = false.obs;
final showSkipButton = false.obs;
final connectButtonText = '连接设备'.obs;
final retryButtonText = '重试'.obs;
@ -68,6 +74,7 @@ class PairingController extends GetxController {
// ========== 当前设备信息 ==========
BluetoothDevice? _currentClassicDevice;
String? _verifiedDeviceAddress;
TextEditingController codeInputController = TextEditingController();
// Public getter for view access
BluetoothDevice? get currentDevice => _currentClassicDevice;
@ -236,6 +243,7 @@ class PairingController extends GetxController {
showConnectButton.value = false;
showRetryButton.value = false;
showSkipButton.value = false;
showAutoCodeInput.value = false;
switch (state) {
case PairingState.idle:
@ -276,7 +284,12 @@ class PairingController extends GetxController {
_startBleConnection();
});
break;
case PairingState.manualbinding:
statusText.value = '需要手动版定授权码...';
isLoading.value = false;
showAutoCodeInput.value = true;
showSkipButton.value = true;
break;
case PairingState.completed:
statusText.value = '配对成功!';
isLoading.value = false;
@ -306,6 +319,10 @@ class PairingController extends GetxController {
showRetryButton.value = true;
retryButtonText.value = '重新关联';
break;
case ErrorType.authnFailed:
showRetryButton.value = true;
retryButtonText.value = '重新授权';
break;
default:
showRetryButton.value = true;
retryButtonText.value = '重试';
@ -393,44 +410,48 @@ class PairingController extends GetxController {
Future<void> _verifyDevice(BluetoothDevice device) async {
try {
Logger.i(_tag, '开始验证设备: ${device.name} (${device.address})');
// TODO: 实现API验证逻辑
// 暂时模拟验证通过
await Future.delayed(const Duration(seconds: 2));
// 检查设备名称是否包含 DeepSound 或 DeapSound
final deviceName = device.name ?? '';
if (deviceName.contains('DeepSound') ||
deviceName.contains('DeapSound')) {
Logger.i(_tag, '设备验证通过');
_verifiedDeviceAddress = device.address;
// 检查是否已有BLE关联
final pairedMacAddress = await _bleManager.getPairedMacAddress();
Logger.i(_tag, '已关联的MAC地址: $pairedMacAddress');
if (pairedMacAddress != null &&
pairedMacAddress.toUpperCase() == device.address.toUpperCase()) {
// 已有关联,检查BLE连接状态
Logger.i(_tag, '设备已关联,检查BLE连接状态');
final currentBleState = await _bleManager.getConnectionState();
Logger.i(_tag, '验证时BLE状态: $currentBleState');
if (currentBleState == 2) {
// BleConnectionState.connected
Logger.i(_tag, 'BLE已连接,直接完成配对');
_transition(PairingState.completed);
// await Future.delayed(const Duration(seconds: 2));;
if (await authbindingDevice()) {
// 检查设备名称是否包含 DeepSound 或 DeapSound
final deviceName = device.name ?? '';
if (deviceName.contains('DeepSound') ||
deviceName.contains('DeapSound')) {
Logger.i(_tag, '设备验证通过');
_verifiedDeviceAddress = device.address;
// 检查是否已有BLE关联
final pairedMacAddress = await _bleManager.getPairedMacAddress();
Logger.i(_tag, '已关联的MAC地址: $pairedMacAddress');
if (pairedMacAddress != null &&
pairedMacAddress.toUpperCase() == device.address.toUpperCase()) {
// 已有关联,检查BLE连接状态
Logger.i(_tag, '设备已关联,检查BLE连接状态');
final currentBleState = await _bleManager.getConnectionState();
Logger.i(_tag, '验证时BLE状态: $currentBleState');
if (currentBleState == 2) {
// BleConnectionState.connected
Logger.i(_tag, 'BLE已连接,直接完成配对');
_transition(PairingState.completed);
} else {
Logger.i(_tag, 'BLE未连接,等待BLE连接');
_transition(PairingState.waitingBle);
}
} else {
Logger.i(_tag, 'BLE未连接,等待BLE连接');
_transition(PairingState.waitingBle);
// 没有关联或关联的是其他设备,需要进行关联
Logger.i(_tag, '需要进行BLE设备关联');
_transition(PairingState.associating);
}
} else {
// 没有关联或关联的是其他设备,需要进行关联
Logger.i(_tag, '需要进行BLE设备关联');
_transition(PairingState.associating);
// _setError(ErrorType.deviceNotMatch, '请连接 DeepSound 设备');
_transition(PairingState.completed);
}
} else {
_setError(ErrorType.deviceNotMatch, '请连接 DeepSound 设备');
//验证失败 进入手动授权
_transition(PairingState.manualbinding);
}
} catch (e) {
Logger.e(_tag, '设备验证异常: $e');
@ -504,6 +525,46 @@ class PairingController extends GetxController {
}
}
//自动绑定设备
Future<bool> authbindingDevice() async {
if (User.instance.earphonemac == currentDevice?.address) {
// 已经绑定
return true;
}
// 调用BLE管理器的自动绑定方法
final response = await Api.bindearphone({
"earphonemac": currentDevice?.address,
"earphoneauthcode": "",
});
Logger.i(_tag, '手动授权结果 ${response}');
if (response != null) {
Logger.i(_tag, '设备自动绑定成功');
User.instance.earphonemac = currentDevice?.address ?? '';
return true;
} else {
Logger.e(_tag, '手动绑定失败!');
return false;
}
}
/// 用户点击授权码输入框
void onAutoCodeInputTap() async {
// 调用BLE管理器的自动绑定方法
final response = await Api.bindearphone({
"earphonemac": currentDevice?.address,
"earphoneauthcode": codeInputController.text.trim(),
});
Logger.i(_tag, '手动授权结果 ${response}');
if (response != null) {
_transition(PairingState.verifying);
_verifyDevice(_currentClassicDevice!);
} else {
Logger.e(_tag, '手动绑定失败!');
_setError(ErrorType.authnFailed, '设备授权失败,请检查授权码');
}
}
/// 导航到主页
void _navigateToHome() {
Future.delayed(const Duration(seconds: 1), () async {

51
lib/modules/pairing/views/pairing_view.dart

@ -336,6 +336,8 @@ class PairingView extends GetView<PairingController> {
return '扫描超时,请确保设备在附近';
case ErrorType.networkError:
return '网络错误,请检查网络连接';
case ErrorType.authnFailed:
return '授权失败,请检查授权码是否正确';
default:
return '配对失败,请重试';
}
@ -433,7 +435,7 @@ class PairingView extends GetView<PairingController> {
final showConnectButton = controller.showConnectButton.value;
final showRetryButton = controller.showRetryButton.value;
final showSkipButton = controller.showSkipButton.value;
final showAutoCodeInput = controller.showAutoCodeInput.value;
return Container(
width: double.infinity,
child: Column(
@ -490,6 +492,51 @@ class PairingView extends GetView<PairingController> {
),
),
),
if (showAutoCodeInput)
Column(
children: [
// 输入框
SizedBox(
width: double.infinity,
height: 48.h,
child: TextField(
controller: controller
.codeInputController, // 你可以在 Controller 中声明这个
decoration: InputDecoration(
hintText: '请输入代码',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
),
contentPadding: EdgeInsets.symmetric(horizontal: 16.w),
),
),
),
SizedBox(height: 12.h),
SizedBox(
width: double.infinity,
height: 48.h,
child: ElevatedButton(
onPressed: controller.onAutoCodeInputTap,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(Get.context!).primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
elevation: 0,
),
child: Text(
'授权绑定',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
],
),
// 自动输入代码按钮
// 跳过按钮
if (showSkipButton) ...[
@ -510,11 +557,9 @@ class PairingView extends GetView<PairingController> {
),
),
],
],
),
);
});
}
}

73
lib/modules/pairing/views/pairing_view_new.dart

@ -21,17 +21,17 @@ class PairingView extends GetView<PairingController> {
children: [
// 顶部标题
_buildHeader(isDarkMode),
// 中间内容
Expanded(
child: Center(
child: Obx(() => _buildContent(isDarkMode)),
),
),
// 底部按钮
Obx(() => _buildButtons(isDarkMode)),
SizedBox(height: 20.h),
],
),
@ -69,15 +69,15 @@ class PairingView extends GetView<PairingController> {
Widget _buildContent(bool isDarkMode) {
final state = controller.currentState;
final isLoading = controller.isLoading.value;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 状态图标
_buildStateIcon(state, isLoading, isDarkMode),
SizedBox(height: 40.h),
// 状态文本
Text(
controller.statusText.value,
@ -87,7 +87,7 @@ class PairingView extends GetView<PairingController> {
),
textAlign: TextAlign.center,
),
// 错误详情
if (state == PairingState.error) ...[
SizedBox(height: 16.h),
@ -132,48 +132,51 @@ class PairingView extends GetView<PairingController> {
),
);
}
IconData iconData;
Color iconColor;
switch (state) {
case PairingState.idle:
case PairingState.checking:
iconData = Icons.bluetooth_searching;
iconColor = Colors.blue;
break;
case PairingState.waitingClassic:
iconData = Icons.bluetooth;
iconColor = Colors.grey;
break;
case PairingState.verifying:
iconData = Icons.verified_user;
iconColor = Colors.orange;
break;
case PairingState.associating:
iconData = Icons.link;
iconColor = Colors.blue;
break;
case PairingState.waitingBle:
iconData = Icons.sync;
iconColor = Colors.blue;
break;
case PairingState.manualbinding:
iconData = Icons.blind;
iconColor = Colors.blue;
break;
case PairingState.completed:
iconData = Icons.check_circle;
iconColor = Colors.green;
break;
case PairingState.error:
iconData = Icons.error;
iconColor = Colors.red;
break;
}
return Container(
width: 100.w,
height: 100.w,
@ -193,8 +196,9 @@ class PairingView extends GetView<PairingController> {
Widget _buildButtons(bool isDarkMode) {
final showConnectButton = controller.showConnectButton.value;
final showRetryButton = controller.showRetryButton.value;
final showAutoCodeInput = controller.showAutoCodeInput.value;
final showSkipButton = controller.showSkipButton.value;
return Column(
children: [
// 主要操作按钮
@ -204,16 +208,40 @@ class PairingView extends GetView<PairingController> {
controller.onConnectTap,
isDarkMode,
),
if (showRetryButton)
_buildPrimaryButton(
'重试',
controller.onRetryTap,
isDarkMode,
),
SizedBox(height: 12.h),
if (showAutoCodeInput)
// 自动输入代码按钮
SizedBox(
width: double.infinity,
height: 48.h,
child: ElevatedButton(
onPressed: controller.onAutoCodeInputTap,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(Get.context!).primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
elevation: 0,
),
child: Text(
'自动输入代码',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
// 跳过按钮
if (showSkipButton)
TextButton(
@ -231,7 +259,8 @@ class PairingView extends GetView<PairingController> {
}
/// 构建主要按钮
Widget _buildPrimaryButton(String text, VoidCallback onPressed, bool isDarkMode) {
Widget _buildPrimaryButton(
String text, VoidCallback onPressed, bool isDarkMode) {
return SizedBox(
width: double.infinity,
height: 48.h,
@ -255,4 +284,4 @@ class PairingView extends GetView<PairingController> {
),
);
}
}
}

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

@ -200,8 +200,7 @@ class SettingsView extends GetView<SettingsController> {
2: 'proUser', //'专业用户'
};
final membershipString = membershipMap[
controller
.userInfo.value.membershipLevel] ??
controller.userInfo.value.viplv] ??
'Unknown';
return Text(
membershipString,

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

@ -15,7 +15,7 @@ import com.deep_voice.speech.tts.TtsEvent
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
/** AzureSpeechPlugin */
class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
private val tag = "AzureSpeechPlugin"
@ -501,6 +501,10 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
azureTtsHelper.release()
result.success(true)
}
"setAudioOutputDevice" -> {
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER)
result.success(true)
}
else -> {
result.notImplemented()

6
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

@ -223,7 +223,11 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
}
AudioOutputDevice.SPEAKER -> {
// 强制使用扬声器
manager.mode = AudioManager.MODE_NORMAL
/* MODE_NORMAL 默认媒体模式 ❌ 不能强制扬声器(会被耳机覆盖)
MODE_IN_COMMUNICATION VoIP 通信模式 ✅ 可以强制使用扬声器或听筒
MODE_IN_CALL 电话模式(仅限系统) ✅ 更强但风险大,不推荐使用
*/
manager.mode = AudioManager.MODE_IN_COMMUNICATION
manager.isSpeakerphoneOn = true
}
AudioOutputDevice.HEADPHONES -> {

Loading…
Cancel
Save