Browse Source

修改声音复刻

weicu
tanlongsheng 10 months ago
parent
commit
ba3184deb3
  1. 8
      lib/data/services/speech_impl/azure_tts_service.dart
  2. 5
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  3. 5
      lib/data/services/speech_impl/volcano_tts_service.dart
  4. 3
      lib/data/services/tts_service.dart
  5. 20
      lib/modules/translation/middleware/controllers/translation_middleware_controller.dart
  6. 52
      lib/modules/voice_replication/views/recorder_bottom_sheet.dart
  7. 5
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  8. 47
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

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

@ -182,6 +182,14 @@ class AzureTtsService extends GetxService implements TtsService {
} }
} }
@override
Future<bool> setVoiceFlocking(String speakerProfileId) async {
final result = await _channel.invokeMethod('setVoiceFlocking', {
'speakerProfileId': speakerProfileId,
});
return result;
}
@override @override
Future<bool> startspeak(String sessionid) async { Future<bool> startspeak(String sessionid) async {
if (!_isInitialized) await initialize(); if (!_isInitialized) await initialize();

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

@ -94,6 +94,11 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
} }
} }
@override
Future<bool> setVoiceFlocking(String speakerProfileId) async {
return true;
}
// 语音合成 // 语音合成
Future<void> _speechSynthesis(String text) async { Future<void> _speechSynthesis(String text) async {
await _tryConnecting(); await _tryConnecting();

5
lib/data/services/speech_impl/volcano_tts_service.dart

@ -185,6 +185,11 @@ class VolcanoTtsService extends GetxService implements TtsService {
} }
} }
@override
Future<bool> setVoiceFlocking(String speakerProfileId) async {
return true;
}
/// 设置是否启用音频数据回调 /// 设置是否启用音频数据回调
Future<bool> setEnableAudioCallback(bool enable) async { Future<bool> setEnableAudioCallback(bool enable) async {
if (!await _ensureInitialized()) return false; if (!await _ensureInitialized()) return false;

3
lib/data/services/tts_service.dart

@ -19,6 +19,9 @@ abstract class TtsService {
/// 设置语音 /// 设置语音
Future<bool> setVoice(String voiceName); Future<bool> setVoice(String voiceName);
/// 设置语音复刻
Future<bool> setVoiceFlocking(String speakerProfileId);
///开始新的播放会话 ///开始新的播放会话
Future<bool> startspeak(String sessionid); Future<bool> startspeak(String sessionid);

20
lib/modules/translation/middleware/controllers/translation_middleware_controller.dart

@ -75,16 +75,16 @@ class TranslationMiddlewareController extends GetxController {
iconColor: Colors.indigo[500]!, // 主靛 iconColor: Colors.indigo[500]!, // 主靛
targetRoute: Routes.textTranslation, targetRoute: Routes.textTranslation,
), ),
// TranslationOption( TranslationOption(
// translationId: 'voice_replication', translationId: 'voice_replication',
// title: '声音复刻', title: '声音复刻',
// icon: Icons.volume_up, icon: Icons.volume_up,
// startColor: Colors.pink[400]!, // 粉色 startColor: Colors.pink[400]!, // 粉色
// endColor: Colors.pink[600]!, // 深粉 endColor: Colors.pink[600]!, // 深粉
// iconBgColor: Colors.white, // 白色 iconBgColor: Colors.white, // 白色
// iconColor: Colors.pink[500]!, // 主粉 iconColor: Colors.pink[500]!, // 主粉
// targetRoute: Routes.voiceReplication, targetRoute: Routes.voiceReplication,
// ), ),
]; ];
/// 导航到指定的翻译功能 /// 导航到指定的翻译功能

52
lib/modules/voice_replication/views/recorder_bottom_sheet.dart

@ -118,11 +118,53 @@ class _RecorderBottomSheetState extends State<RecorderBottomSheet> {
final streamed = await request.send(); final streamed = await request.send();
final response = await http.Response.fromStream(streamed); final response = await http.Response.fromStream(streamed);
if (streamed.statusCode < 200 || streamed.statusCode >= 300) { if (streamed.statusCode >= 200 && streamed.statusCode < 300) {
final bodyText = utf8.decode(response.bodyBytes, allowMalformed: true);
if (bodyText.isNotEmpty) {
final decoded = jsonDecode(bodyText);
String status = decoded['status'] ?? 'NotStarted';
final operationLocation = response.headers['operation-location'];
int retryCount = 0;
while (status != 'Succeeded' && status != 'Failed' && retryCount < 30) {
await Future.delayed(const Duration(seconds: 1));
if (operationLocation != null && operationLocation.isNotEmpty) {
status = await _operations(operationLocation);
} else {
final consentUrl =
'https://$_resourceRegion.api.cognitive.microsoft.com/customvoice/consents/$_jessicaConsentId?api-version=2024-02-01-preview';
status = await _operations(consentUrl);
}
retryCount++;
}
if (status != 'Succeeded') {
throw Exception('Consent validation failed or timed out: $status');
}
}
} else {
throw Exception('Failed to create consent: ${response.statusCode}'); throw Exception('Failed to create consent: ${response.statusCode}');
} }
} }
Future<String> _operations(String operationUrl) async {
String status = 'NotStarted';
final url = Uri.parse(operationUrl);
final response = await http.get(url, headers: {
'Ocp-Apim-Subscription-Key': _resourceKey,
});
if (response.statusCode == 200) {
final bodyText = utf8.decode(response.bodyBytes, allowMalformed: true);
if (bodyText.isNotEmpty) {
final decoded = jsonDecode(bodyText);
status = decoded['status'] ?? status;
}
}
return status;
}
Future<void> _personalvoices() async { Future<void> _personalvoices() async {
final url = Uri.parse( final url = Uri.parse(
'https://$_resourceRegion.api.cognitive.microsoft.com/customvoice/personalvoices/$_jessicaPersonalVoiceId?api-version=2024-02-01-preview'); 'https://$_resourceRegion.api.cognitive.microsoft.com/customvoice/personalvoices/$_jessicaPersonalVoiceId?api-version=2024-02-01-preview');
@ -133,8 +175,6 @@ class _RecorderBottomSheetState extends State<RecorderBottomSheet> {
request.fields['consentId'] = _jessicaConsentId; request.fields['consentId'] = _jessicaConsentId;
request.files.add(await http.MultipartFile.fromPath('audiodata', _path)); request.files.add(await http.MultipartFile.fromPath('audiodata', _path));
request.files.add(await http.MultipartFile.fromPath('audiodata',
'/data/user/0/com.saitong.voitrans/app_flutter/VoiceReplication/recording_1764763812656.wav'));
final streamed = await request.send(); final streamed = await request.send();
final response = await http.Response.fromStream(streamed); final response = await http.Response.fromStream(streamed);
@ -142,17 +182,11 @@ class _RecorderBottomSheetState extends State<RecorderBottomSheet> {
final bodyText = utf8.decode(response.bodyBytes, allowMalformed: true); final bodyText = utf8.decode(response.bodyBytes, allowMalformed: true);
if (bodyText.isNotEmpty) { if (bodyText.isNotEmpty) {
final decoded = jsonDecode(bodyText); final decoded = jsonDecode(bodyText);
print('------2-------$decoded');
_controller.speakerProfileId.value = decoded['speakerProfileId']; _controller.speakerProfileId.value = decoded['speakerProfileId'];
_storage.write('open_voice_replication', true); _storage.write('open_voice_replication', true);
_storage.write('speakerProfileId', decoded['speakerProfileId']); _storage.write('speakerProfileId', decoded['speakerProfileId']);
} }
} else { } else {
final bodyText = utf8.decode(response.bodyBytes, allowMalformed: true);
if (bodyText.isNotEmpty) {
final decoded = jsonDecode(bodyText);
print('------2-------$decoded');
}
throw Exception( throw Exception(
'Failed to create personal voice: ${response.statusCode}'); 'Failed to create personal voice: ${response.statusCode}');
} }

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

@ -699,6 +699,11 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
result.success(azureTtsHelper.setVoice(voiceName)) result.success(azureTtsHelper.setVoice(voiceName))
} }
"setVoiceFlocking" -> {
val speakerProfileId = call.argument<String>("speakerProfileId") ?: ""
result.success(azureTtsHelper.setVoiceFlocking(speakerProfileId))
}
"speakText" -> { "speakText" -> {
val text = call.argument<String>("text") ?: return result.error( val text = call.argument<String>("text") ?: return result.error(
"INVALID_ARGUMENTS", "INVALID_ARGUMENTS",

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

@ -51,6 +51,9 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
private val streamBuffer = StringBuilder() private val streamBuffer = StringBuilder()
private var lastSpeakTime = 0L private var lastSpeakTime = 0L
private var enableVoiceFlocking = false
private var speakerProfileId: String = ""
/** /**
* 初始化TTS引擎 * 初始化TTS引擎
* *
@ -354,6 +357,19 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
} }
} }
/**
* 开启声音复刻
*/
fun setVoiceFlocking(id: String): Boolean {
if(id.isNotEmpty()) {
enableVoiceFlocking = true
} else {
enableVoiceFlocking = false
}
speakerProfileId = id
return true
}
/** /**
* 重新创建合成器 * 重新创建合成器
*/ */
@ -502,16 +518,27 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
.replace(Regex("[😀-🟿]+"), "") // 简化表情符号移除 .replace(Regex("[😀-🟿]+"), "") // 简化表情符号移除
.trim() .trim()
// 优化:简化SSML结构,减少嵌套层级 if(enableVoiceFlocking) {
return """ return """
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="zh-CN"> <speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xml:lang="zh-CN">
<voice name="$currentVoice"> <voice name="DragonLatestNeural">
<prosody rate="$currentRate" pitch="$currentPitch" volume="$currentVolume"> <mstts:ttsembedding speakerProfileId="$speakerProfileId">
$processedText $processedText
</prosody> </mstts:ttsembedding>
</voice> </voice>
</speak> </speak>
""".trimIndent() """.trimIndent()
} else {
return """
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="zh-CN">
<voice name="$currentVoice">
<prosody rate="$currentRate" pitch="$currentPitch" volume="$currentVolume">
$processedText
</prosody>
</voice>
</speak>
""".trimIndent()
}
} }
/** /**

Loading…
Cancel
Save