You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
291 lines
8.1 KiB
291 lines
8.1 KiB
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:crypto/crypto.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 {
|
|
final GetStorage _storage = GetStorage();
|
|
final _record = AudioRecorder();
|
|
|
|
final String _appId = "43d87d22";
|
|
final String _appKey = "2bd01ce06ac35957fc0c082f9b6284ec";
|
|
|
|
RxList dataList = [].obs;
|
|
RxString type = '1'.obs;
|
|
|
|
RxString text = ''.obs;
|
|
|
|
String _token = '';
|
|
|
|
int _textId = 0;
|
|
int _segId = 0;
|
|
String _taskId = '';
|
|
|
|
@override
|
|
void 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': 'defaultTimbre',
|
|
'description': 'microsoftCloudTimbreService',
|
|
'isUsable': true
|
|
});
|
|
_storage.write('timbreList', [
|
|
{
|
|
'type': '1',
|
|
'name': 'defaultTimbre',
|
|
'description': 'microsoftCloudTimbreService',
|
|
'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 recordDir = Directory('${directory.path}/RecordSound');
|
|
if (!await recordDir.exists()) {
|
|
await recordDir.create(recursive: true);
|
|
}
|
|
final path =
|
|
'${directory.path}/RecordSound/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","engineVersion":"omni_v1"}';
|
|
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': '声音复刻${dataList.length}',
|
|
'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['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'] || type.value == data['type']) return;
|
|
type.value = data['type'];
|
|
_storage.write('timbre', data['type']);
|
|
}
|
|
}
|
|
|