5 changed files with 769 additions and 29 deletions
@ -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'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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']); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue