Browse Source

修改选择音色

weicu
tanlongsheng 2 years ago
parent
commit
0dfc80721b
  1. 55
      lib/data/services/speech_factory.dart
  2. 393
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  3. 3
      lib/modules/home/controllers/home_controller.dart
  4. 281
      lib/modules/timbre/controllers/timbre_controller.dart
  5. 66
      lib/modules/timbre/views/timbre_view.dart

55
lib/data/services/speech_factory.dart

@ -1,5 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'asr_service.dart'; import 'asr_service.dart';
import 'speech_impl/voice_clone_tts_service.dart';
import 'tts_service.dart'; import 'tts_service.dart';
import 'speech_impl/azure_asr_service.dart'; import 'speech_impl/azure_asr_service.dart';
import 'speech_impl/azure_tts_service.dart'; import 'speech_impl/azure_tts_service.dart';
@ -11,60 +13,70 @@ import '../../core/utils/logger.dart';
enum SpeechServiceType { enum SpeechServiceType {
/// Azure 语音服务 /// Azure 语音服务
azure, azure,
/// Flutter 本地语音服务 /// Flutter 本地语音服务
flutter, flutter,
} }
/// 语音服务工厂类 /// 语音服务工厂类
/// ///
/// 用于根据配置信息获取不同的TTS和ASR实现 /// 用于根据配置信息获取不同的TTS和ASR实现
class SpeechFactory extends GetxService { class SpeechFactory extends GetxService {
static SpeechFactory get to => Get.find<SpeechFactory>(); static SpeechFactory get to => Get.find<SpeechFactory>();
/// 当前使用的语音服务类型 /// 当前使用的语音服务类型
SpeechServiceType _currentServiceType = SpeechServiceType.azure; SpeechServiceType _currentServiceType = SpeechServiceType.azure;
/// 获取当前使用的语音服务类型 /// 获取当前使用的语音服务类型
SpeechServiceType get currentServiceType => _currentServiceType; SpeechServiceType get currentServiceType => _currentServiceType;
/// 设置语音服务类型 /// 设置语音服务类型
void setServiceType(SpeechServiceType type) { void setServiceType(SpeechServiceType type) {
if (_currentServiceType != type) { if (_currentServiceType != type) {
_currentServiceType = type; _currentServiceType = type;
Logger.info('语音服务类型已切换为: ${type.toString().split('.').last}'); Logger.info('语音服务类型已切换为: ${type.toString().split('.').last}');
// 重新注册服务 // 重新注册服务
_registerServices(); _registerServices();
} }
} }
/// 注册语音服务 /// 注册语音服务
void _registerServices() { void _registerServices() {
// 注销现有服务 // 注销现有服务
if (Get.isRegistered<AsrService>()) { if (Get.isRegistered<AsrService>()) {
Get.delete<AsrService>(force: true); Get.delete<AsrService>(force: true);
} }
if (Get.isRegistered<TtsService>()) { if (Get.isRegistered<TtsService>()) {
Get.delete<TtsService>(force: true); Get.delete<TtsService>(force: true);
} }
final GetStorage storage = GetStorage();
String timbre = storage.read('timbre') ?? '1';
// 根据类型注册新服务 // 根据类型注册新服务
switch (_currentServiceType) { switch (_currentServiceType) {
case SpeechServiceType.azure: case SpeechServiceType.azure:
Get.lazyPut<AsrService>(() => AzureAsrService()); Get.lazyPut<AsrService>(() => AzureAsrService());
Get.lazyPut<TtsService>(() => AzureTtsService()); if (timbre == '1') {
Get.lazyPut<TtsService>(() => AzureTtsService());
} else {
Get.lazyPut<TtsService>(() => VoiceCloneTtsService());
}
break; break;
case SpeechServiceType.flutter: case SpeechServiceType.flutter:
Get.lazyPut<AsrService>(() => FlutterAsrService()); Get.lazyPut<AsrService>(() => FlutterAsrService());
Get.lazyPut<TtsService>(() => FlutterTtsService()); if (timbre == '1') {
Get.lazyPut<TtsService>(() => FlutterTtsService());
} else {
Get.lazyPut<TtsService>(() => VoiceCloneTtsService());
}
break; break;
} }
} }
/// 初始化语音服务 /// 初始化语音服务
Future<void> initialize({SpeechServiceType? initialType}) async { Future<void> initialize({SpeechServiceType? initialType}) async {
try { try {
@ -72,24 +84,25 @@ class SpeechFactory extends GetxService {
if (initialType != null) { if (initialType != null) {
_currentServiceType = initialType; _currentServiceType = initialType;
} }
// 注册服务 // 注册服务
_registerServices(); _registerServices();
// // 初始化服务 // // 初始化服务
// final asrService = Get.find<AsrService>(); // final asrService = Get.find<AsrService>();
// final ttsService = Get.find<TtsService>(); // final ttsService = Get.find<TtsService>();
// await asrService.initialize(); // await asrService.initialize();
// await ttsService.initialize(); // await ttsService.initialize();
Logger.info('语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}'); Logger.info(
'语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}');
} catch (e) { } catch (e) {
Logger.error('语音服务初始化失败: $e'); Logger.error('语音服务初始化失败: $e');
rethrow; rethrow;
} }
} }
/// 获取ASR服务 /// 获取ASR服务
AsrService getAsrService() { AsrService getAsrService() {
if (!Get.isRegistered<AsrService>()) { if (!Get.isRegistered<AsrService>()) {
@ -97,7 +110,7 @@ class SpeechFactory extends GetxService {
} }
return Get.find<AsrService>(); return Get.find<AsrService>();
} }
/// 获取TTS服务 /// 获取TTS服务
TtsService getTtsService() { TtsService getTtsService() {
if (!Get.isRegistered<TtsService>()) { if (!Get.isRegistered<TtsService>()) {
@ -105,4 +118,4 @@ class SpeechFactory extends GetxService {
} }
return Get.find<TtsService>(); return Get.find<TtsService>();
} }
} }

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

@ -0,0 +1,393 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:just_audio/just_audio.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../../../core/utils/logger.dart';
import '../tts_service.dart';
import 'volcano_tts_api_service.dart';
/// 声音复刻服务,负责基本的文本转语音功能
///
/// 使用讯飞声音复刻实现TtsService接口
class VoiceCloneTtsService extends GetxService implements TtsService {
final String hostUrl = "wss://cn-huabei-1.xf-yun.com/v1/private/voice_clone";
final String appId = '43d87d22';
final String apiKey = '2bd01ce06ac35957fc0c082f9b6284ec';
final String apiSecret = 'ODU3OTUyYjhjMjY5NDhkMmMxYWY2Zjlm';
WebSocketChannel? _channel;
final AudioPlayer _player = AudioPlayer();
late ConcatenatingAudioSource _playlist;
// 当前语音
final RxString _currentVoice = ''.obs;
@override
String get currentVoice => _currentVoice.value;
// 播放状态
final RxBool _isSpeaking = false.obs;
@override
bool get isSpeaking => _isSpeaking.value;
// 事件流控制器
final StreamController<TtsEvent> _eventController =
StreamController<TtsEvent>.broadcast();
@override
Stream<TtsEvent> get onEvent => _eventController.stream;
bool _isInitialized = false;
String _resId = '';
// 语音合成队列
final List _textQueue = [];
bool _isProcessingQueue = false;
BytesBuilder _audioBytes = BytesBuilder();
Completer<void> _completer = Completer();
final isEnabled = true.obs;
// 流式文本缓冲区
String _streamBuffer = '';
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
if (_isInitialized) return true;
try {
final GetStorage storage = GetStorage();
String timbre = storage.read('timbre');
_resId = timbre;
_playlist = ConcatenatingAudioSource(
useLazyPreparation: true,
children: [],
);
return true;
} catch (e) {
Logger.error('TTS初始化失败: $e');
return false;
}
}
@override
Future<bool> setVoice(String voiceName) async {
if (!_isInitialized) await initialize();
try {
Logger.info('设置TTS语音: $voiceName');
return true;
} catch (e) {
Logger.error('设置TTS语音失败: $e');
return false;
}
}
// 语音合成
Future<void> _speechSynthesis(String text) async {
await _tryConnecting();
_channel?.sink.add(
json.encode({
"header": {"app_id": appId, "status": 2, "res_id": _resId},
"parameter": {
"tts": {
"vcn": "x5_clone",
"audio": {"encoding": "lame", "sample_rate": 16000}
}
},
"payload": {
"text": {
"status": 2,
"seq": 0,
"text": base64Encode(utf8.encode(text))
}
},
}),
);
}
Future<void> _tryConnecting() async {
try {
final url = _geturl();
_channel = WebSocketChannel.connect(Uri.parse(url));
_channel?.stream.timeout(const Duration(seconds: 10)); //设置超时时间
await _channel?.ready;
_channel?.stream.listen(
(message) {
_handle(message);
},
onError: (error) {
Logger.error('连接失败: $error');
},
onDone: () {
Logger.error('WebSocket 连接已关闭!');
},
cancelOnError: true,
);
} catch (e) {
Logger.error('初始化异常: $e');
}
}
String _geturl() {
// 解析 URL
final uri = Uri.parse(hostUrl);
// 获取当前时间并格式化为 RFC1123 格式
final date = HttpDate.format(DateTime.now().toUtc());
// 参与签名的字段
final signString = [
"host: ${uri.host}",
"date: $date",
"GET ${uri.path} HTTP/1.1",
];
// 拼接签名字符串
final sgin = signString.join("\n");
// 使用 HMAC-SHA256 签名
final key = utf8.encode(apiSecret);
final bytes = utf8.encode(sgin);
final hmacSha256 = Hmac(sha256, key);
final digest = hmacSha256.convert(bytes);
final sha = base64Encode(digest.bytes);
// 构建请求参数
final authUrl =
'api_key="$apiKey", algorithm="hmac-sha256", headers="host date request-line", signature="$sha"';
// 将请求参数使用 base64 编码
final authorization = base64Encode(utf8.encode(authUrl));
// 构建查询参数
final queryParams = {
"host": uri.host,
"date": date,
"authorization": authorization,
};
// 将查询参数编码并拼接到 URL 后面
final callurl =
Uri.parse(hostUrl).replace(queryParameters: queryParams).toString();
return callurl;
}
void _handle(data) async {
final result = jsonDecode(data);
int code = result['header']['code'];
if (code == 0) {
int status = result['header']['status'];
if (status == 1 || status == 2) {
Uint8List audio = base64Decode(result['payload']['audio']['audio']);
_audioBytes.add(audio);
}
if (status == 2) {
final newSource = BytesAudioSource(_audioBytes.toBytes());
await _playlist.add(newSource);
if (!_isSpeaking.value) {
_audioPlay();
}
_completer.complete();
_completer = Completer();
}
}
}
void _audioPlay() async {
_isSpeaking.value = true;
// 发送开始事件
_eventController.add(TtsEvent(type: TtsEventType.started));
await _player.setAudioSource(_playlist);
await _player.play();
}
@override
Future<bool> speakOnce(String text) async {
if (!isEnabled.value || text.isEmpty) return false;
try {
_audioBytes.clear();
await _speechSynthesis(text);
await _completer.future;
return true;
} catch (e) {
_isSpeaking.value = false;
Logger.error('语音合成失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('语音合成失败: $e'));
return false;
}
}
@override
Future<bool> speakStream(String text) async {
if (!isEnabled.value || text.isEmpty) return false;
try {
// 添加文本到缓冲区
_streamBuffer += text;
// 如果缓冲区为空,直接返回
if (_streamBuffer.isEmpty) return true;
// 使用正则表达式匹配句子,包括结束符号
// 匹配任意字符,直到遇到句子结束符号
final sentenceRegex = RegExp(
r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])');
bool hasProcessed = false;
// 查找所有完整句子
final matches = sentenceRegex.allMatches(_streamBuffer);
final List<String> sentences = [];
int lastMatchEnd = 0;
for (final match in matches) {
// 提取完整句子(包含结束符号)
final sentence = match.group(1)?.trim();
if (sentence != null && sentence.isNotEmpty) {
sentences.add(sentence);
lastMatchEnd = match.end;
}
}
// 处理找到的句子
for (final sentence in sentences) {
_textQueue.add(sentence);
hasProcessed = true;
}
// 更新缓冲区,只保留未完成的部分
if (lastMatchEnd > 0) {
_streamBuffer = _streamBuffer.substring(lastMatchEnd);
}
// 如果处理了文本并且队列未在处理中,开始处理队列
if (hasProcessed && !_isProcessingQueue) {
_processQueue();
}
return true;
} catch (e) {
Logger.error('处理流式文本失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('处理流式文本失败: $e'));
return false;
}
}
@override
Future<bool> flushStream() async {
if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true;
try {
// 处理缓冲区中可能的完整句子
await speakStream('');
// 如果缓冲区仍有剩余文本,将其添加到播放队列
if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) {
_textQueue.add(_streamBuffer);
// 清空缓冲区
_streamBuffer = '';
// 如果队列未在处理中,开始处理队列
if (!_isProcessingQueue) {
_processQueue();
}
}
Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条');
return true;
} catch (e) {
Logger.error('刷新TTS流失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('刷新TTS流失败: $e'));
return false;
}
}
/// 处理语音合成队列
Future<void> _processQueue() async {
if (_textQueue.isEmpty || _isProcessingQueue) return;
_isProcessingQueue = true;
try {
while (_textQueue.isNotEmpty) {
if (!isEnabled.value) {
_textQueue.clear();
break;
}
final item = _textQueue.removeAt(0);
// 使用speakOnce播放当前项
await speakOnce(item);
}
} catch (e) {
Logger.error('处理语音队列出错: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('处理语音队列出错: $e'));
} finally {
_isProcessingQueue = false;
}
}
@override
Future<bool> stop() async {
try {
_textQueue.clear();
_player.stop();
_playlist.clear();
_audioBytes = BytesBuilder();
if (_isSpeaking.value) {
_isSpeaking.value = false;
// 发送取消事件
_eventController.add(TtsEvent(type: TtsEventType.canceled));
}
return true;
} catch (e) {
Logger.error('停止语音合成出错: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('停止语音合成出错: $e'));
return false;
}
}
@override
Future<void> dispose() async {
if (!_isInitialized) return;
try {
await stop();
// 关闭事件流控制器
await _eventController.close();
_isInitialized = false;
_player.dispose();
} catch (e) {
Logger.error('释放资源失败: $e');
}
}
}

3
lib/modules/home/controllers/home_controller.dart

@ -113,7 +113,6 @@ class HomeController extends GetxController {
// 设置蓝牙监听器 // 设置蓝牙监听器
void _setupBluetoothListeners() { void _setupBluetoothListeners() {
// 监听设备连接 // 监听设备连接
_bluetoothService.onDeviceConnected((device) { _bluetoothService.onDeviceConnected((device) {
Logger.info('监听到蓝牙设备连接: ${device.name}'); Logger.info('监听到蓝牙设备连接: ${device.name}');
@ -188,7 +187,7 @@ class HomeController extends GetxController {
// 打开AI伴侣功能 // 打开AI伴侣功能
void openAIGirlfriend() { void openAIGirlfriend() {
exploreController.onAIFeatureTap(2);//进入AI女友页面 exploreController.onAIFeatureTap(2); //进入AI女友页面
} }
// 展示AI功能 // 展示AI功能

281
lib/modules/timbre/controllers/timbre_controller.dart

@ -1,14 +1,287 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart';
class TimbreController extends GetxController { class TimbreController extends GetxController {
RxList dataList = [ final GetStorage _storage = GetStorage();
{'type': '1', 'name': '默认音色', 'description': '微软云端音色服务'}, final _record = AudioRecorder();
{'type': '2', 'name': '声音复刻', 'description': '复刻本人音色服务'}
].obs; final String _appId = "43d87d22";
final String _appKey = "2bd01ce06ac35957fc0c082f9b6284ec";
RxList dataList = [].obs;
RxString type = '1'.obs; RxString type = '1'.obs;
RxString text = ''.obs;
String _token = '';
int _textId = 0;
int _segId = 0;
String _taskId = '67e0f5e6025cf871b1de31b2';
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_getDataList();
}
@override
void onClose() {
_record.dispose();
super.onClose();
}
void _getDataList() {
List? timbreList = _storage.read('timbreList');
String? timbre = _storage.read('timbre');
if (timbreList != null) {
dataList.value = timbreList;
} else {
dataList.add({
'type': '1',
'name': '默认音色',
'description': '微软云端音色服务',
'isUsable': true
});
_storage.write('timbreList', [
{
'type': '1',
'name': '默认音色',
'description': '微软云端音色服务',
'isUsable': true
}
]);
}
if (timbre != null) {
type.value = timbre;
}
}
// 录制声音
void startRecorder() async {
if (_token.isEmpty) {
await _getToken();
}
await _getTraintext();
await _addTask();
final directory = await getApplicationDocumentsDirectory();
final path =
'${directory.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav';
await _record.start(
const RecordConfig(
encoder: AudioEncoder.wav,
sampleRate: 24000,
numChannels: 1,
),
path: path,
);
}
// 获取token
Future<void> _getToken() async {
int timeStamp = DateTime.now().millisecondsSinceEpoch;
String data =
'{"base":{"appid":"$_appId","version":"v1","timestamp":"$timeStamp"},"model":"remote"}';
String keySignInput = _appKey + timeStamp.toString();
List<int> keySignBytes = utf8.encode(keySignInput);
Digest keySignDigest = md5.convert(keySignBytes);
String keySign = keySignDigest.toString();
String signInput = keySign + data;
List<int> signBytes = utf8.encode(signInput);
Digest signDigest = md5.convert(signBytes);
String sign = signDigest.toString();
final response = await http.post(
Uri.parse('http://avatar-hci.xfyousheng.com/aiauth/v1/token'),
headers: {
'Content-Type': 'application/json',
'Authorization': sign,
},
body: data,
);
if (response.statusCode == 200) {
final body = json.decode(response.body);
if (body['retcode'] == '000000') {
_token = body['accesstoken'];
}
}
}
// 获取训练文本
Future<void> _getTraintext() async {
String data = '{"textId":"5001"}';
Map sign = _sign(data);
final response = await http.post(
Uri.parse('http://opentrain.xfyousheng.com/voice_train/task/traintext'),
headers: {
'Content-Type': 'application/json',
'X-Sign': sign['sign'],
'X-Token': _token,
'X-AppId': _appId,
'X-Time': sign['timeStamp'],
},
body: data,
);
if (response.statusCode == 200) {
final body = json.decode(response.body);
if (body['code'] == 0) {
_textId = body['data']['textId'];
Map textSegs = body['data']['textSegs'].first;
_segId = textSegs['segId'];
text.value = _decodeCorruptedText(textSegs['segText']);
}
}
}
// 创建训练任务
Future<void> _addTask() async {
String data = '{"resourceType":"12"}';
Map sign = _sign(data);
final response = await http.post(
Uri.parse('http://opentrain.xfyousheng.com/voice_train/task/add'),
headers: {
'Content-Type': 'application/json',
'X-Sign': sign['sign'],
'X-Token': _token,
'X-AppId': _appId,
'X-Time': sign['timeStamp'],
},
body: data,
);
if (response.statusCode == 200) {
final body = json.decode(response.body);
if (body['code'] == 0) {
_taskId = body['data'];
}
}
}
Map _sign(String data) {
int timeStamp = DateTime.now().millisecondsSinceEpoch;
String keySignInput = _appKey + timeStamp.toString();
List<int> keySignBytes = utf8.encode(keySignInput);
Digest keySignDigest = md5.convert(keySignBytes);
String keySign = keySignDigest.toString();
String signInput = keySign + data;
List<int> signBytes = utf8.encode(signInput);
Digest signDigest = md5.convert(signBytes);
String sign = signDigest.toString();
return {'sign': sign, 'timeStamp': timeStamp.toString()};
}
String _decodeCorruptedText(String corruptedText) {
List<int> latin1Bytes =
corruptedText.codeUnits.map((ch) => ch & 0xFF).toList();
return utf8.decode(latin1Bytes);
}
// 完成录制
void completeRecorder() async {
final audioPath = await _record.stop();
String data =
'{"taskId":"$_taskId","textId":"$_textId","textSegId":"$_segId"}';
Map sign = _sign(data);
final uri = Uri.parse(
'http://opentrain.xfyousheng.com/voice_train/task/submitWithAudio');
final request = http.MultipartRequest('POST', uri);
request.files.add(await http.MultipartFile.fromPath(
'file',
audioPath!,
filename: path.basename(audioPath),
));
request.fields.addAll({
'taskId': _taskId,
'textId': '$_textId',
'textSegId': '$_segId',
});
request.headers.addAll({
"X-Sign": sign['sign'],
"X-Token": _token,
"X-AppId": _appId,
"X-Time": sign['timeStamp'],
});
final response = await request.send();
if (response.statusCode == 200) {
final responseBody = await response.stream.bytesToString();
final body = json.decode(responseBody);
if (body['code'] == 0) {
text.value = '';
dataList.add({
'type': _taskId,
'name': '声音复刻$_taskId',
'description': '复刻本人音色服务',
'isUsable': false
});
await _getTaskResult(_taskId);
}
}
}
// 查询训练任务状态
Future<void> _getTaskResult(String id) async {
String data = '{"taskId":"$id"}';
Map sign = _sign(data);
final response = await http.post(
Uri.parse('http://opentrain.xfyousheng.com/voice_train/task/result'),
headers: {
'Content-Type': 'application/json',
'X-Sign': sign['sign'],
'X-Token': _token,
'X-AppId': _appId,
'X-Time': sign['timeStamp'],
},
body: data,
);
if (response.statusCode == 200) {
final body = json.decode(response.body);
if (body['code'] == 0) {
if (body['data']['trainStatus'] == -1) {
await _getTaskResult(id);
}
if (body['data']['trainStatus'] == 1) {
Map? dataMap =
dataList.firstWhereOrNull((item) => item['type'] == id);
if (dataMap != null) {
dataMap['type'] = body['data']['assetId'];
dataMap['name'] = '声音复刻${body['data']['assetId']}';
dataMap['isUsable'] = true;
dataList.refresh();
_storage.write('timbreList', dataList);
}
}
if (body['data']['trainStatus'] == 0) {
Get.snackbar(
'error'.tr, // 错误
_decodeCorruptedText(body['data']['failedDesc']),
snackPosition: SnackPosition.TOP,
);
}
}
}
}
// 取消
void cancel() {
text.value = '';
_record.stop();
Get.back();
}
// 选择音色
void selectTone(Map data) {
if (!data['isUsable']) return;
type.value = data['type'];
_storage.write('timbre', data['type']);
} }
} }

66
lib/modules/timbre/views/timbre_view.dart

@ -60,7 +60,69 @@ class TimbreView extends GetView<TimbreController> {
), ),
), ),
GestureDetector( GestureDetector(
onTap: () {}, onTap: () {
controller.startRecorder();
Get.dialog(
barrierDismissible: false,
AlertDialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
title: Text(
'请朗读',
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
),
),
content: Obx(
() => controller.text.value.isNotEmpty
? Text(
controller.text.value,
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
)
: Container(
width: 40.w,
height: 40.w,
alignment: Alignment.center,
child: CircularProgressIndicator(
strokeWidth: 2.w,
valueColor: const AlwaysStoppedAnimation<Color>(
Colors.blue),
),
),
),
actions: [
TextButton(
onPressed: () {
controller.cancel();
},
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
),
Obx(() => controller.text.value.isNotEmpty
? TextButton(
onPressed: () {
Get.back();
controller.completeRecorder();
},
child: Text(
'完成录制',
style: TextStyle(
color: isDarkMode
? Colors.red[300]
: Colors.red[600],
),
),
)
: const SizedBox()),
],
),
);
},
child: Container( child: Container(
width: 130.w, width: 130.w,
height: 48.w, height: 48.w,
@ -89,7 +151,7 @@ class TimbreView extends GetView<TimbreController> {
Widget _item(bool isDarkMode, Map data) { Widget _item(bool isDarkMode, Map data) {
return InkWell( return InkWell(
onTap: () { onTap: () {
controller.type.value = data['type']; controller.selectTone(data);
}, },
child: Obx( child: Obx(
() => Container( () => Container(

Loading…
Cancel
Save