diff --git a/lib/data/services/speech_impl/azure_tts_service.dart b/lib/data/services/speech_impl/azure_tts_service.dart index 369e0ca31..cebcb9a10 100644 --- a/lib/data/services/speech_impl/azure_tts_service.dart +++ b/lib/data/services/speech_impl/azure_tts_service.dart @@ -182,6 +182,14 @@ class AzureTtsService extends GetxService implements TtsService { } } + @override + Future setVoiceFlocking(String speakerProfileId) async { + final result = await _channel.invokeMethod('setVoiceFlocking', { + 'speakerProfileId': speakerProfileId, + }); + return result; + } + @override Future startspeak(String sessionid) async { if (!_isInitialized) await initialize(); diff --git a/lib/data/services/speech_impl/voice_clone_tts_service.dart b/lib/data/services/speech_impl/voice_clone_tts_service.dart index a779270c4..15ea6803b 100644 --- a/lib/data/services/speech_impl/voice_clone_tts_service.dart +++ b/lib/data/services/speech_impl/voice_clone_tts_service.dart @@ -94,6 +94,11 @@ class VoiceCloneTtsService extends GetxService implements TtsService { } } + @override + Future setVoiceFlocking(String speakerProfileId) async { + return true; + } + // 语音合成 Future _speechSynthesis(String text) async { await _tryConnecting(); diff --git a/lib/data/services/speech_impl/volcano_tts_service.dart b/lib/data/services/speech_impl/volcano_tts_service.dart index b2bdcc2a1..2bb04ade0 100644 --- a/lib/data/services/speech_impl/volcano_tts_service.dart +++ b/lib/data/services/speech_impl/volcano_tts_service.dart @@ -185,6 +185,11 @@ class VolcanoTtsService extends GetxService implements TtsService { } } + @override + Future setVoiceFlocking(String speakerProfileId) async { + return true; + } + /// 设置是否启用音频数据回调 Future setEnableAudioCallback(bool enable) async { if (!await _ensureInitialized()) return false; diff --git a/lib/data/services/tts_service.dart b/lib/data/services/tts_service.dart index f36827699..b4bf72625 100644 --- a/lib/data/services/tts_service.dart +++ b/lib/data/services/tts_service.dart @@ -19,6 +19,9 @@ abstract class TtsService { /// 设置语音 Future setVoice(String voiceName); + /// 设置语音复刻 + Future setVoiceFlocking(String speakerProfileId); + ///开始新的播放会话 Future startspeak(String sessionid); diff --git a/lib/modules/translation/middleware/controllers/translation_middleware_controller.dart b/lib/modules/translation/middleware/controllers/translation_middleware_controller.dart index ff336f4b7..ad9473fb9 100644 --- a/lib/modules/translation/middleware/controllers/translation_middleware_controller.dart +++ b/lib/modules/translation/middleware/controllers/translation_middleware_controller.dart @@ -75,16 +75,16 @@ class TranslationMiddlewareController extends GetxController { iconColor: Colors.indigo[500]!, // 主靛 targetRoute: Routes.textTranslation, ), - // TranslationOption( - // translationId: 'voice_replication', - // title: '声音复刻', - // icon: Icons.volume_up, - // startColor: Colors.pink[400]!, // 粉色 - // endColor: Colors.pink[600]!, // 深粉 - // iconBgColor: Colors.white, // 白色 - // iconColor: Colors.pink[500]!, // 主粉 - // targetRoute: Routes.voiceReplication, - // ), + TranslationOption( + translationId: 'voice_replication', + title: '声音复刻', + icon: Icons.volume_up, + startColor: Colors.pink[400]!, // 粉色 + endColor: Colors.pink[600]!, // 深粉 + iconBgColor: Colors.white, // 白色 + iconColor: Colors.pink[500]!, // 主粉 + targetRoute: Routes.voiceReplication, + ), ]; /// 导航到指定的翻译功能 diff --git a/lib/modules/voice_replication/views/recorder_bottom_sheet.dart b/lib/modules/voice_replication/views/recorder_bottom_sheet.dart index 9bbbbc8a4..ea8475343 100644 --- a/lib/modules/voice_replication/views/recorder_bottom_sheet.dart +++ b/lib/modules/voice_replication/views/recorder_bottom_sheet.dart @@ -118,11 +118,53 @@ class _RecorderBottomSheetState extends State { final streamed = await request.send(); 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}'); } } + Future _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 _personalvoices() async { final url = Uri.parse( 'https://$_resourceRegion.api.cognitive.microsoft.com/customvoice/personalvoices/$_jessicaPersonalVoiceId?api-version=2024-02-01-preview'); @@ -133,8 +175,6 @@ class _RecorderBottomSheetState extends State { request.fields['consentId'] = _jessicaConsentId; 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 response = await http.Response.fromStream(streamed); @@ -142,17 +182,11 @@ class _RecorderBottomSheetState extends State { final bodyText = utf8.decode(response.bodyBytes, allowMalformed: true); if (bodyText.isNotEmpty) { final decoded = jsonDecode(bodyText); - print('------2-------$decoded'); _controller.speakerProfileId.value = decoded['speakerProfileId']; _storage.write('open_voice_replication', true); _storage.write('speakerProfileId', decoded['speakerProfileId']); } } else { - final bodyText = utf8.decode(response.bodyBytes, allowMalformed: true); - if (bodyText.isNotEmpty) { - final decoded = jsonDecode(bodyText); - print('------2-------$decoded'); - } throw Exception( 'Failed to create personal voice: ${response.statusCode}'); } diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index b072c4118..39e28bfc9 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/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)) } + "setVoiceFlocking" -> { + val speakerProfileId = call.argument("speakerProfileId") ?: "" + result.success(azureTtsHelper.setVoiceFlocking(speakerProfileId)) + } + "speakText" -> { val text = call.argument("text") ?: return result.error( "INVALID_ARGUMENTS", diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index 261825b98..4b47782aa 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/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 var lastSpeakTime = 0L + private var enableVoiceFlocking = false + private var speakerProfileId: String = "" + /** * 初始化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("[😀-🟿]+"), "") // 简化表情符号移除 .trim() - // 优化:简化SSML结构,减少嵌套层级 - return """ - - - - $processedText - - - - """.trimIndent() + if(enableVoiceFlocking) { + return """ + + + + $processedText + + + + """.trimIndent() + } else { + return """ + + + + $processedText + + + + """.trimIndent() + } } /**