From 61659e87c84b8ee8800c293d006dfbec9d247fd8 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Tue, 25 Feb 2025 19:18:06 +0000 Subject: [PATCH] m --- android/app/build.gradle.kts | 2 +- android/app/src/main/AndroidManifest.xml | 12 +- .../com/example/deep_voice/MainActivity.kt | 99 +++- .../deep_voice/SpeechRecognitionHelper.kt | 18 +- .../example/deep_voice/TextToSpeechHelper.kt | 165 ++++++ .../src/main/res/drawable/ic_notification.xml | 11 + lib/core/bindings/initial_binding.dart | 30 +- .../controllers/permission_controller.dart | 30 +- lib/data/services/audio_service.dart | 81 ++- .../services/background_agent_service.dart | 391 ++++++++++++- lib/data/services/microsoft_tts_service.dart | 346 ++++++++++++ lib/data/services/my_audio_handler.dart | 146 +++-- lib/data/services/volcano_tts_service.dart | 15 + lib/main.dart | 3 + .../chat/controllers/chat_controller.dart | 521 +++++++++--------- .../controllers/voice_input_controller.dart | 12 +- lib/modules/chat/views/chat_view.dart | 17 +- .../microsoft_tts_continuous_example.dart | 308 +++++++++++ lib/modules/microsoft_tts_example.dart | 202 +++++++ lib/modules/profile/views/profile_view.dart | 25 +- 20 files changed, 2054 insertions(+), 380 deletions(-) create mode 100644 android/app/src/main/kotlin/com/example/deep_voice/TextToSpeechHelper.kt create mode 100644 android/app/src/main/res/drawable/ic_notification.xml create mode 100644 lib/data/services/microsoft_tts_service.dart create mode 100644 lib/modules/microsoft_tts_continuous_example.dart create mode 100644 lib/modules/microsoft_tts_example.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 7da3502ce..4c990a7d4 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -32,7 +32,7 @@ android { // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 23 // Updated to meet record_android plugin requirements - targetSdk = flutter.targetSdkVersion + targetSdk = 33 // 设置为 Android 13 (API 33),以匹配 FOREGROUND_SERVICE_MEDIA_PLAYBACK 权限的要求 versionCode = flutter.versionCode versionName = flutter.versionName } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9bb05f38c..7ba6fb350 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,11 +4,15 @@ + + + + @@ -16,6 +20,8 @@ + + + android:exported="true" + android:enabled="true"> + android:exported="true" + android:enabled="true"> diff --git a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt index d1c44bb99..20c61e635 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt @@ -13,17 +13,19 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.EventChannel class MainActivity: AudioServiceActivity() { - private val CHANNEL = "com.example.deep_voice/speech_recognition" - private val EVENT_CHANNEL = "com.example.deep_voice/speech_recognition_events" + private val SPEECH_RECOGNITION_CHANNEL = "com.example.deep_voice/speech_recognition" + private val SPEECH_RECOGNITION_EVENT_CHANNEL = "com.example.deep_voice/speech_recognition_events" + private val TTS_CHANNEL = "com.example.deep_voice/text_to_speech" private val TAG = "MainActivity" private val speechHelper = SpeechRecognitionHelper() + private val ttsHelper = TextToSpeechHelper() private var eventSink: EventChannel.EventSink? = null override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) - // 设置方法通道 - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> + // 设置语音识别方法通道 + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SPEECH_RECOGNITION_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "initialize" -> { val subscriptionKey = call.argument("subscriptionKey") @@ -150,8 +152,92 @@ class MainActivity: AudioServiceActivity() { } } - // 设置事件通道 - EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL).setStreamHandler( + // 设置 TTS 方法通道 + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, TTS_CHANNEL).setMethodCallHandler { call, result -> + when (call.method) { + "initialize" -> { + val subscriptionKey = call.argument("subscriptionKey") + val serviceRegion = call.argument("serviceRegion") + + if (subscriptionKey == null || serviceRegion == null) { + result.error("INVALID_ARGUMENTS", "subscriptionKey and serviceRegion are required", null) + return@setMethodCallHandler + } + + try { + ttsHelper.initialize(subscriptionKey, serviceRegion) + result.success(true) + } catch (e: Exception) { + result.error("INITIALIZATION_ERROR", e.message, null) + } + } + "setVoice" -> { + val voiceName = call.argument("voiceName") + + if (voiceName == null) { + result.error("INVALID_ARGUMENTS", "voiceName is required", null) + return@setMethodCallHandler + } + + try { + ttsHelper.setVoice(voiceName) + result.success(true) + } catch (e: Exception) { + result.error("SET_VOICE_ERROR", e.message, null) + } + } + "speakText" -> { + val text = call.argument("text") + + if (text == null) { + result.error("INVALID_ARGUMENTS", "text is required", null) + return@setMethodCallHandler + } + + ttsHelper.speakText(text, object : TextToSpeechHelper.TTSCallback { + override fun onSuccess(message: String) { + result.success(message) + } + + override fun onError(error: String) { + result.error("TTS_ERROR", error, null) + } + }) + } + "speakSsml" -> { + val ssml = call.argument("ssml") + + if (ssml == null) { + result.error("INVALID_ARGUMENTS", "ssml is required", null) + return@setMethodCallHandler + } + + ttsHelper.speakSsml(ssml, object : TextToSpeechHelper.TTSCallback { + override fun onSuccess(message: String) { + result.success(message) + } + + override fun onError(error: String) { + result.error("TTS_ERROR", error, null) + } + }) + } + "dispose" -> { + try { + ttsHelper.dispose() + result.success(true) + } catch (e: Exception) { + result.error("DISPOSE_ERROR", e.message, null) + } + } + else -> { + result.notImplemented() + } + } + } + + // 设置语音识别事件通道 + EventChannel(flutterEngine.dartExecutor.binaryMessenger, SPEECH_RECOGNITION_EVENT_CHANNEL).setStreamHandler( object : EventChannel.StreamHandler { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { eventSink = events @@ -172,6 +258,7 @@ class MainActivity: AudioServiceActivity() { override fun onDestroy() { speechHelper.dispose() + ttsHelper.dispose() super.onDestroy() } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt index 488bd0f88..f5f4e7ee7 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt @@ -2,6 +2,7 @@ package com.example.deep_voice import android.util.Log import com.microsoft.cognitiveservices.speech.* +import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.util.EventHandler import java.util.concurrent.ExecutionException import java.util.function.Consumer @@ -14,14 +15,15 @@ class SpeechRecognitionHelper { // 初始化 SDK fun initialize(subscriptionKey: String, serviceRegion: String) { try { - val config = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) - // 设置识别语言,例如中文 - config.speechRecognitionLanguage = "zh-CN" - recognizer = SpeechRecognizer(config) - Log.d(TAG, "Speech SDK initialized successfully") - } catch (e: Exception) { - Log.e(TAG, "初始化失败: ${e.message}") - } + val config = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) + config.speechRecognitionLanguage = "zh-CN" + // 直接使用默认麦克风输入,不传递自定义音频处理选项 + val audioConfig = AudioConfig.fromDefaultMicrophoneInput() + recognizer = SpeechRecognizer(config, audioConfig) + Log.d(TAG, "Speech SDK initialized successfully") +} catch (e: Exception) { + Log.e(TAG, "初始化失败: ${e.message}") +} } // 开始一次性语音识别 diff --git a/android/app/src/main/kotlin/com/example/deep_voice/TextToSpeechHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/TextToSpeechHelper.kt new file mode 100644 index 000000000..433fb1c1d --- /dev/null +++ b/android/app/src/main/kotlin/com/example/deep_voice/TextToSpeechHelper.kt @@ -0,0 +1,165 @@ +package com.example.deep_voice + +import android.util.Log +import com.microsoft.cognitiveservices.speech.* +import java.util.concurrent.Future + +/** + * Microsoft Text-to-Speech Helper + * + * 该类封装了微软语音 SDK 的 TTS 功能,提供简单的接口供 Flutter 调用 + */ +class TextToSpeechHelper { + private val TAG = "TextToSpeechHelper" + private var speechConfig: SpeechConfig? = null + private var synthesizer: SpeechSynthesizer? = null + private var isInitialized = false + + /** + * 初始化 TTS 引擎 + * + * @param subscriptionKey Azure 语音服务订阅密钥 + * @param serviceRegion Azure 语音服务区域 + */ + fun initialize(subscriptionKey: String, serviceRegion: String) { + try { + speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) + // 默认设置中文女声 + speechConfig?.setSpeechSynthesisVoiceName("zh-CN-XiaoxiaoNeural") + synthesizer = SpeechSynthesizer(speechConfig) + isInitialized = true + Log.d(TAG, "TTS 引擎初始化成功") + } catch (e: Exception) { + Log.e(TAG, "TTS 引擎初始化失败: ${e.message}") + throw e + } + } + + /** + * 设置语音 + * + * @param voiceName 语音名称,例如 "zh-CN-XiaoxiaoNeural" + */ + fun setVoice(voiceName: String) { + if (!isInitialized) { + throw Exception("TTS 引擎尚未初始化") + } + + try { + speechConfig?.setSpeechSynthesisVoiceName(voiceName) + // 重新创建合成器以应用新的语音设置 + synthesizer?.close() + synthesizer = SpeechSynthesizer(speechConfig) + Log.d(TAG, "已设置语音: $voiceName") + } catch (e: Exception) { + Log.e(TAG, "设置语音失败: ${e.message}") + throw e + } + } + + /** + * 合成文本为语音并播放 + * + * @param text 要合成的文本 + * @param callback 回调接口,用于返回结果或错误 + */ + fun speakText(text: String, callback: TTSCallback) { + if (!isInitialized) { + callback.onError("TTS 引擎尚未初始化") + return + } + + try { + Log.d(TAG, "开始合成文本: $text") + val task: Future = synthesizer!!.SpeakTextAsync(text) + + // 异步获取结果 + val result = task.get() + + when (result.reason) { + ResultReason.SynthesizingAudioCompleted -> { + Log.d(TAG, "语音合成完成") + callback.onSuccess("语音合成完成") + } + ResultReason.Canceled -> { + val cancellation = SpeechSynthesisCancellationDetails.fromResult(result) + Log.e(TAG, "语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}") + callback.onError("语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}") + } + else -> { + Log.e(TAG, "语音合成失败: ${result.reason}") + callback.onError("语音合成失败: ${result.reason}") + } + } + + result.close() + } catch (e: Exception) { + Log.e(TAG, "语音合成异常: ${e.message}") + callback.onError("语音合成异常: ${e.message}") + } + } + + /** + * 合成 SSML 为语音并播放 + * + * @param ssml SSML 格式的文本 + * @param callback 回调接口,用于返回结果或错误 + */ + fun speakSsml(ssml: String, callback: TTSCallback) { + if (!isInitialized) { + callback.onError("TTS 引擎尚未初始化") + return + } + + try { + Log.d(TAG, "开始合成 SSML") + val task: Future = synthesizer!!.SpeakSsmlAsync(ssml) + + // 异步获取结果 + val result = task.get() + + when (result.reason) { + ResultReason.SynthesizingAudioCompleted -> { + Log.d(TAG, "语音合成完成") + callback.onSuccess("语音合成完成") + } + ResultReason.Canceled -> { + val cancellation = SpeechSynthesisCancellationDetails.fromResult(result) + Log.e(TAG, "语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}") + callback.onError("语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}") + } + else -> { + Log.e(TAG, "语音合成失败: ${result.reason}") + callback.onError("语音合成失败: ${result.reason}") + } + } + + result.close() + } catch (e: Exception) { + Log.e(TAG, "语音合成异常: ${e.message}") + callback.onError("语音合成异常: ${e.message}") + } + } + + /** + * 释放资源 + */ + fun dispose() { + try { + synthesizer?.close() + speechConfig?.close() + isInitialized = false + Log.d(TAG, "TTS 引擎已释放") + } catch (e: Exception) { + Log.e(TAG, "释放 TTS 引擎失败: ${e.message}") + } + } + + /** + * TTS 回调接口 + */ + interface TTSCallback { + fun onSuccess(message: String) + fun onError(error: String) + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_notification.xml b/android/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 000000000..a4a624c71 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 296bb43f0..7a531f52d 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -1,10 +1,10 @@ import 'package:get/get.dart'; import '../../core/controllers/permission_controller.dart'; -import '../../data/services/volcano_tts_service.dart'; import '../../data/services/audio_service.dart'; import '../../data/services/notification_service.dart'; import '../../data/services/volcano_ai_service.dart'; import '../../data/services/voice_recognition_service.dart'; +import '../../data/services/volcano_tts_service.dart'; /// 初始绑定,用于管理全局依赖 class InitialBinding extends Bindings { @@ -12,18 +12,34 @@ class InitialBinding extends Bindings { void dependencies() { // 初始化所有服务 final notificationService = Get.put(NotificationService(), permanent: true); - final volcanoTtsService = Get.put(VolcanoTtsService(), permanent: true); final audioService = Get.put(AudioServiceManager(), permanent: true); final volcanoAiService = Get.put(VolcanoAIService(), permanent: true); final voiceRecognitionService = Get.put(VoiceRecognitionService(), permanent: true); + final volcanoTtsService = Get.put(VolcanoTtsService(), permanent: true); // 只注册全局控制器 Get.put(PermissionController(), permanent: true); - // 异步初始化 - Future.wait([ - notificationService.init(), - // 如果其他服务也需要异步初始化,在这里添加 - ]); + // 异步初始化各个服务 + _initializeServices(notificationService, audioService); + } + + // 单独提取初始化服务的方法,以便更好地处理错误 + void _initializeServices(NotificationService notificationService, AudioServiceManager audioService) { + // 初始化通知服务 + notificationService.init().catchError((error) { + print('通知服务初始化失败: $error'); + return null; + }); + + // 初始化音频服务 + audioService.init().then((_) { + print('AudioServiceManager 初始化完成,可以接收蓝牙耳机按键事件'); + }).catchError((error) { + print('音频服务初始化失败: $error'); + return null; + }); + + // 其他服务初始化可以在这里添加 } } \ No newline at end of file diff --git a/lib/core/controllers/permission_controller.dart b/lib/core/controllers/permission_controller.dart index 9effd79ad..6a07b3c96 100644 --- a/lib/core/controllers/permission_controller.dart +++ b/lib/core/controllers/permission_controller.dart @@ -11,13 +11,37 @@ class PermissionController extends GetxController { Future requestPermissions() async { if (Platform.isAndroid || Platform.isIOS) { - await [ + // 请求所有必要的权限 + final permissions = [ Permission.microphone, Permission.storage, Permission.bluetooth, Permission.bluetoothConnect, - Permission.notification, - ].request(); + ]; + + // Android 13+ 需要额外请求 POST_NOTIFICATIONS 权限 + if (Platform.isAndroid) { + permissions.add(Permission.notification); + } + + // 请求权限并打印结果 + final statuses = await permissions.request(); + + // 打印权限状态,便于调试 + statuses.forEach((permission, status) { + print('权限 $permission: $status'); + }); + + // 请求悬浮窗权限(需要特殊处理) + if (Platform.isAndroid) { + if (!await Permission.systemAlertWindow.isGranted) { + print('请求悬浮窗权限'); + final status = await Permission.systemAlertWindow.request(); + print('悬浮窗权限状态: $status'); + } else { + print('悬浮窗权限已授予'); + } + } } } } \ No newline at end of file diff --git a/lib/data/services/audio_service.dart b/lib/data/services/audio_service.dart index 7027cdc8f..ac086a5b2 100644 --- a/lib/data/services/audio_service.dart +++ b/lib/data/services/audio_service.dart @@ -11,69 +11,96 @@ class AudioServiceManager extends GetxService { bool _isInitialized = false; final _lock = Lock(); + // 添加一个标志,表示是否已经尝试过初始化 + bool _hasAttemptedInit = false; + Future init() async { - print('初始化 AudioService'); + print('初始化 AudioServiceManager'); if (_isInitialized) { - print('AudioService 已经初始化'); + print('AudioServiceManager 已经初始化'); return this; } - await _cleanupExistingService(); - + // 如果已经尝试过初始化但失败了,不再重试 + if (_hasAttemptedInit) { + print('AudioServiceManager 之前初始化失败,不再重试'); + return this; + } + + _hasAttemptedInit = true; + try { // 使用锁确保初始化的原子性 await _lock.synchronized(() async { if (_isInitialized) return; - // 初始化 AudioService - _audioHandler = await AudioService.init( + // 使用 AudioService.init 初始化音频服务 + _audioHandler = await AudioService.init( builder: () => MyAudioHandler(), - config: const AudioServiceConfig( + config: AudioServiceConfig( androidNotificationChannelId: 'com.example.deep_voice.channel.audio', androidNotificationChannelName: 'Deep Voice Audio Service', + // 使用新创建的通知图标 androidNotificationIcon: 'drawable/ic_notification', - androidNotificationOngoing: true, + // 设置为 false,避免在没有用户交互时启动前台服务 + androidNotificationOngoing: false, + // 设置为 true,当暂停时停止前台服务 androidStopForegroundOnPause: true, androidShowNotificationBadge: true, notificationColor: Colors.blue, + // 设置为 false,避免自动启动前台服务 + fastForwardInterval: const Duration(seconds: 10), + rewindInterval: const Duration(seconds: 10), + preloadArtwork: false, ), ); + // 注册自定义音频处理器 + await Get.put(_audioHandler!, permanent: true); + _isInitialized = true; + print('AudioServiceManager 初始化成功'); }); return this; } catch (e) { - print('AudioService初始化失败: $e'); - await _cleanupExistingService(); - rethrow; + print('AudioServiceManager初始化失败: $e'); + _isInitialized = false; // 确保失败时重置状态 + return this; // 即使失败也返回实例,避免空指针异常 } } - Future _cleanupExistingService() async { - _isInitialized = false; + // 获取音频处理器 + MyAudioHandler? get audioHandler => _audioHandler; - // 停止现有的音频处理器 + // 发送媒体按钮事件 + Future sendMediaButtonEvent() async { if (_audioHandler != null) { - await _audioHandler!.stop(); - _audioHandler = null; - } - - // 停止 AudioService - try { - await AudioService.stop(); - } catch (e) { - print('停止 AudioService 时出错: $e'); + try { + await _audioHandler!.customAction('media_button'); + } catch (e) { + print('发送媒体按钮事件失败: $e'); + } + } else { + print('AudioHandler 未初始化,无法发送媒体按钮事件'); } - - // 等待一小段时间确保清理完成 - await Future.delayed(const Duration(milliseconds: 100)); } @override void onClose() async { - await _cleanupExistingService(); + _isInitialized = false; + + // 停止现有的音频处理器 + if (_audioHandler != null) { + try { + await _audioHandler!.stop(); + } catch (e) { + print('停止 AudioHandler 失败: $e'); + } + _audioHandler = null; + } + super.onClose(); } } \ No newline at end of file diff --git a/lib/data/services/background_agent_service.dart b/lib/data/services/background_agent_service.dart index a8bae39f7..7ad62fe1a 100644 --- a/lib/data/services/background_agent_service.dart +++ b/lib/data/services/background_agent_service.dart @@ -2,6 +2,7 @@ import 'package:get/get.dart'; import 'dart:async'; import 'volcano_ai_service.dart'; import 'volcano_tts_service.dart'; +import 'voice_recognition_service.dart'; import '../../modules/chat/models/message_model.dart'; class BackgroundAgentService extends GetxService { @@ -9,54 +10,252 @@ class BackgroundAgentService extends GetxService { final VolcanoAIService _aiService; final VolcanoTtsService _ttsService; + final VoiceRecognitionService _voiceRecognitionService; final List _messageHistory = []; String _pendingTtsText = ''; static const int _minTtsLength = 20; bool _isProcessing = false; + bool _isListening = false; + StreamSubscription? _recognitionSubscription; + + // 可观察的状态 + final RxBool isListening = false.obs; + final RxString recognizedText = ''.obs; + + // 添加一个标志,表示是否已经识别到语音 + bool _hasRecognizedSpeech = false; + + // 添加一个标志,表示是否应该继续循环交互 + bool _shouldContinueInteraction = false; + + // 添加一个 Completer 用于在识别到最终结果时完成 + Completer? _recognitionCompleter; + + // 添加一个标志,表示是否已经收到最终结果 + bool _hasFinalResult = false; + + // 添加一个计时器,用于在一段时间没有新的识别结果时提交当前结果 + Timer? _silenceTimer; + + // 添加一个计时器,用于检测用户长时间没有说话 + Timer? _noSpeechTimer; + + // 最后一次识别到语音的时间 + DateTime? _lastSpeechTime; + + // 添加一个标志,表示系统是否正在播放 TTS + bool _isSpeaking = false; + + // 添加一个订阅,用于监听 TTS 状态变化 + StreamSubscription? _ttsSpeakingSubscription; BackgroundAgentService() : _aiService = VolcanoAIService(), - _ttsService = Get.find(); + _ttsService = Get.find(), + _voiceRecognitionService = Get.find() { + // 监听 TTS 播放状态 + _ttsSpeakingSubscription = _ttsService.isPlaying.listen((speaking) { + _isSpeaking = speaking; + print('TTS 播放状态变化: $_isSpeaking'); + + // 如果 TTS 停止播放,且正在进行语音识别,重新启动无语音超时计时器 + if (!speaking && _isListening && _hasRecognizedSpeech && _noSpeechTimer == null) { + _startNoSpeechTimer(); + } + }); + } + // 处理蓝牙耳机按钮触发的交互 Future handleAgentInteraction(String systemPrompt) async { - if (_isProcessing) return; + if (_isProcessing) { + print('已经在处理交互,忽略此次请求'); + return; + } + + // 设置循环交互标志为 true + _shouldContinueInteraction = true; + + try { + // 循环进行交互,直到用户 10 秒没有说话或手动停止 + while (_shouldContinueInteraction) { + await _processSingleInteraction(systemPrompt); + } + } finally { + // 确保在交互结束时停止语音识别 + if (_isListening) { + await stopVoiceRecognition(); + } + } + } + + // 启动无语音超时计时器 + void _startNoSpeechTimer() { + // 如果系统正在播放 TTS,不启动计时器 + if (_isSpeaking) { + print('系统正在播放 TTS,不启动无语音超时计时器'); + return; + } + + // 取消之前的计时器 + _noSpeechTimer?.cancel(); + + // 设置新的计时器,如果 10 秒内没有新的识别结果,且系统没有在播放 TTS,则认为用户已经停止交互 + _noSpeechTimer = Timer(const Duration(seconds: 10), () { + // 再次检查是否正在播放 TTS + if (!_isSpeaking && _isListening && _recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { + print('10秒内没有新的语音输入,且系统没有在播放 TTS,退出循环交互'); + _recognitionCompleter!.complete(''); + } + }); + + print('启动无语音超时计时器,10秒后检查'); + } + + // 处理单次交互 + Future _processSingleInteraction(String systemPrompt) async { _isProcessing = true; + _hasRecognizedSpeech = false; + _hasFinalResult = false; try { + // 先播放一个简短的提示音或提示语,表示开始监听 + try { + await _ttsService.speak("我在听"); + } catch (e) { + print('播放提示音失败: $e'); + // 继续执行,不要因为提示音失败而中断整个流程 + } + + // 开始语音识别 + bool recognitionStarted = false; + String userInput = ''; + + try { + // 尝试启动语音识别 + await startVoiceRecognition(); + recognitionStarted = true; + + // 创建一个 Completer 来处理语音识别完成 + _recognitionCompleter = Completer(); + + // 启动无语音超时计时器 + _startNoSpeechTimer(); + + // 等待语音识别完成 + userInput = await _recognitionCompleter!.future; + + // 取消无语音超时计时器 + _noSpeechTimer?.cancel(); + _noSpeechTimer = null; + + // 如果没有识别到语音,则停止循环交互 + if (!_hasRecognizedSpeech) { + print('没有识别到用户语音,退出循环交互'); + + // 停止语音识别 + if (_isListening) { + await stopVoiceRecognition(); + } + + try { + await _ttsService.speak("没有听到您说话,已退出语音交互"); + } catch (e) { + print('播放退出提示失败: $e'); + } + + // 设置标志,停止循环交互 + _shouldContinueInteraction = false; + _isProcessing = false; + return; + } + + // 注意:不再停止语音识别,保持语音识别状态 + // 只有在退出交互时才停止语音识别 + } catch (e) { + print('语音识别过程出错: $e'); + // 如果语音识别失败,尝试使用默认问候语 + userInput = '你好,请帮我回答一个问题'; + } finally { + // 清理资源,但保持语音识别状态 + _recognitionCompleter = null; + _silenceTimer?.cancel(); + _silenceTimer = null; + _noSpeechTimer?.cancel(); + _noSpeechTimer = null; + } + + if (userInput.isEmpty) { + try { + await _ttsService.speak("没有听到您说话"); + } catch (_) {} + + // 如果没有识别到语音,停止循环交互 + _shouldContinueInteraction = false; + _isProcessing = false; + + // 停止语音识别 + if (_isListening) { + await stopVoiceRecognition(); + } + return; + } + + // 保存用户消息到历史记录 + _messageHistory.add(Message( + role: 'user', + content: userInput, + timestamp: DateTime.now(), + )); + // 构建用于生成回应的消息列表 final messages = [ {'role': 'system', 'content': systemPrompt}, - {'role': 'user', 'content': '请用一句简短的话回应我,要体现你的特点,不要超过12个字。'}, + {'role': 'user', 'content': userInput}, ]; + String fullResponse = ''; - await for (final chunk in _aiService.sendMessageStream( - messages: messages, - systemPrompt: systemPrompt, - )) { - fullResponse += chunk; - - // 累积文本并处理TTS - _pendingTtsText += chunk; - if (_ttsService.isEnabled.value) { - if (_pendingTtsText.length >= _minTtsLength) { - int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText); - if (lastSentenceEnd > 0) { - String textToSpeak = _pendingTtsText.substring(0, lastSentenceEnd + 1); - await _ttsService.speak(textToSpeak); - _pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1); + try { + await for (final chunk in _aiService.sendMessageStream( + messages: messages, + systemPrompt: systemPrompt, + )) { + fullResponse += chunk; + + // 累积文本并处理TTS + _pendingTtsText += chunk; + if (_ttsService.isEnabled.value) { + if (_pendingTtsText.length >= _minTtsLength) { + int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText); + if (lastSentenceEnd > 0) { + String textToSpeak = _pendingTtsText.substring(0, lastSentenceEnd + 1); + try { + await _ttsService.speak(textToSpeak); + } catch (e) { + print('播放TTS失败: $e'); + } + _pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1); + } } } } + } catch (e) { + print('AI响应生成失败: $e'); + // 如果AI响应失败,使用默认回复 + fullResponse = '抱歉,我现在无法回答您的问题。请稍后再试。'; } // 处理剩余的文本 if (_ttsService.isEnabled.value && _pendingTtsText.isNotEmpty) { - await _ttsService.speak(_pendingTtsText); + try { + await _ttsService.speak(_pendingTtsText); + } catch (e) { + print('播放剩余TTS失败: $e'); + } } _pendingTtsText = ''; - // 保存对话历史 + // 保存助手回复到历史记录 _messageHistory.add(Message( role: 'assistant', content: fullResponse, @@ -64,16 +263,154 @@ class BackgroundAgentService extends GetxService { )); // 限制历史记录长度 - if (_messageHistory.length > 10) { - _messageHistory.removeAt(0); + if (_messageHistory.length > 20) { + _messageHistory.removeRange(0, _messageHistory.length - 20); } + + // 短暂暂停,然后开始下一轮交互 + await Future.delayed(const Duration(milliseconds: 500)); + } catch (e) { print('Background agent interaction failed: $e'); + try { + await _ttsService.speak("抱歉,出现了一些问题"); + } catch (_) {} + + // 发生错误时停止循环交互 + _shouldContinueInteraction = false; } finally { _isProcessing = false; } } + // 停止循环交互 + void stopContinuousInteraction() { + _shouldContinueInteraction = false; + print('手动停止循环交互'); + } + + // 开始语音识别 + Future startVoiceRecognition() async { + if (_isListening) return; + + try { + // 确保语音识别服务已初始化 + if (!await _voiceRecognitionService.initialize()) { + throw Exception('无法初始化语音识别服务'); + } + + // 开始连续识别 + final recognitionStream = await _voiceRecognitionService.startContinuousRecognition(); + _isListening = true; + isListening.value = true; + recognizedText.value = ''; + _hasRecognizedSpeech = false; + _hasFinalResult = false; + _lastSpeechTime = null; + + // 监听识别结果 + _recognitionSubscription = recognitionStream.listen((event) { + if (event.type == RecognitionEventType.finalResult) { + recognizedText.value = event.text; + if (event.text.isNotEmpty) { + _hasRecognizedSpeech = true; + _hasFinalResult = true; + _lastSpeechTime = DateTime.now(); + + // 收到最终结果,立即完成识别过程 + if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { + print('收到最终识别结果,立即处理: ${event.text}'); + _recognitionCompleter!.complete(event.text); + } + } + print('最终识别结果: ${event.text}'); + } else if (event.type == RecognitionEventType.intermediateResult) { + recognizedText.value = event.text; + if (event.text.isNotEmpty) { + _hasRecognizedSpeech = true; + _lastSpeechTime = DateTime.now(); + + // 如果系统正在播放TTS,检测到用户开始说话时立即中断TTS播放 + if (_isSpeaking && event.text.trim().isNotEmpty) { + print('检测到用户开始说话,中断TTS播放'); + _ttsService.stop(); // 停止当前TTS播放 + } + + // 取消之前的静默计时器 + _silenceTimer?.cancel(); + + // 取消之前的无语音超时计时器 + _noSpeechTimer?.cancel(); + _noSpeechTimer = null; + + // 设置新的静默计时器,如果 2 秒内没有新的识别结果,则认为用户已经停止说话 + _silenceTimer = Timer(const Duration(seconds: 2), () { + if (_hasRecognizedSpeech && !_hasFinalResult && + _recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { + print('用户停止说话 2 秒,使用当前识别结果: ${recognizedText.value}'); + _recognitionCompleter!.complete(recognizedText.value); + } + }); + } + print('中间识别结果: ${event.text}'); + } else if (event.type == RecognitionEventType.error) { + print('识别错误: ${event.error}'); + } + }, onError: (error) { + print('语音识别流错误: $error'); + _isListening = false; + isListening.value = false; + + // 发生错误时完成 completer + if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { + _recognitionCompleter!.completeError(error); + } + }); + + } catch (e) { + print('启动语音识别失败: $e'); + _isListening = false; + isListening.value = false; + rethrow; + } + } + + // 停止语音识别并返回识别的文本 + Future stopVoiceRecognition() async { + if (!_isListening) return ''; + + try { + // 取消静默计时器 + _silenceTimer?.cancel(); + _silenceTimer = null; + + // 取消无语音超时计时器 + _noSpeechTimer?.cancel(); + _noSpeechTimer = null; + + // 取消订阅 + await _recognitionSubscription?.cancel(); + _recognitionSubscription = null; + + // 停止语音识别 + await _voiceRecognitionService.stopContinuousRecognition(); + + // 获取最终识别结果 + final result = recognizedText.value; + + // 重置状态 + _isListening = false; + isListening.value = false; + + return result; + } catch (e) { + print('停止语音识别失败: $e'); + _isListening = false; + isListening.value = false; + return recognizedText.value; // 返回当前已识别的文本 + } + } + int _findLastSentenceEnd(String text) { final sentenceEnds = [ text.lastIndexOf('。'), @@ -92,4 +429,14 @@ class BackgroundAgentService extends GetxService { void clearHistory() { _messageHistory.clear(); } + + @override + void onClose() { + _recognitionSubscription?.cancel(); + _silenceTimer?.cancel(); + _noSpeechTimer?.cancel(); + _ttsSpeakingSubscription?.cancel(); + _shouldContinueInteraction = false; + super.onClose(); + } } \ No newline at end of file diff --git a/lib/data/services/microsoft_tts_service.dart b/lib/data/services/microsoft_tts_service.dart new file mode 100644 index 000000000..9a39b34c5 --- /dev/null +++ b/lib/data/services/microsoft_tts_service.dart @@ -0,0 +1,346 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +/// 微软 Text-to-Speech 服务异常 +class MicrosoftTtsException implements Exception { + final String message; + MicrosoftTtsException(this.message); + + @override + String toString() => message; +} + +/// 微软 Text-to-Speech 服务 +/// +/// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互, +/// 提供文本转语音功能。 +class MicrosoftTtsService extends GetxService { + static const MethodChannel _channel = MethodChannel('com.example.deep_voice/text_to_speech'); + + bool _isInitialized = false; + late final String _subscriptionKey; + late final String _serviceRegion; + + // 当前使用的语音 + String _currentVoice = 'zh-CN-XiaoxiaoNeural'; + String get currentVoice => _currentVoice; + + // 语音合成队列 + final List _textQueue = []; + bool _isProcessingQueue = false; + bool _isSpeaking = false; + + // 可观察状态 + final isEnabled = true.obs; + final isSpeaking = false.obs; + + MicrosoftTtsService() { + _loadConfig(); + } + + /// 从环境变量加载配置 + void _loadConfig() { + _subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; + _serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; + + if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { + throw MicrosoftTtsException('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); + } + } + + /// 初始化微软 TTS SDK + /// + /// 返回 true 表示初始化成功,否则抛出 PlatformException + Future initialize() async { + if (_isInitialized) return true; + + try { + final bool result = await _channel.invokeMethod('initialize', { + 'subscriptionKey': _subscriptionKey, + 'serviceRegion': _serviceRegion, + }); + + _isInitialized = result; + return result; + } on PlatformException catch (e) { + throw MicrosoftTtsException('初始化失败: ${e.message}'); + } + } + + /// 设置语音 + /// + /// [voiceName] 语音名称,例如 "zh-CN-XiaoxiaoNeural" + /// + /// 返回 true 表示设置成功,否则抛出 PlatformException + Future setVoice(String voiceName) async { + if (!_isInitialized) { + await initialize(); + } + + try { + final bool result = await _channel.invokeMethod('setVoice', { + 'voiceName': voiceName, + }); + + if (result) { + _currentVoice = voiceName; + } + + return result; + } on PlatformException catch (e) { + throw MicrosoftTtsException('设置语音失败: ${e.message}'); + } + } + + /// 将文本转换为语音并播放 + /// + /// [text] 要转换的文本 + /// + /// 返回合成结果消息,否则抛出 PlatformException + Future speakText(String text) async { + if (!_isInitialized) { + await initialize(); + } + + if (!isEnabled.value) { + return "TTS 服务已禁用"; + } + + try { + _isSpeaking = true; + isSpeaking.value = true; + + final String result = await _channel.invokeMethod('speakText', { + 'text': text, + }); + + _isSpeaking = false; + isSpeaking.value = false; + + return result; + } on PlatformException catch (e) { + _isSpeaking = false; + isSpeaking.value = false; + throw MicrosoftTtsException('语音合成失败: ${e.message}'); + } + } + + /// 将 SSML 转换为语音并播放 + /// + /// [ssml] SSML 格式的文本 + /// + /// 返回合成结果消息,否则抛出 PlatformException + Future speakSsml(String ssml) async { + if (!_isInitialized) { + await initialize(); + } + + if (!isEnabled.value) { + return "TTS 服务已禁用"; + } + + try { + _isSpeaking = true; + isSpeaking.value = true; + + final String result = await _channel.invokeMethod('speakSsml', { + 'ssml': ssml, + }); + + _isSpeaking = false; + isSpeaking.value = false; + + return result; + } on PlatformException catch (e) { + _isSpeaking = false; + isSpeaking.value = false; + throw MicrosoftTtsException('SSML 语音合成失败: ${e.message}'); + } + } + + /// 添加文本到队列并开始处理 + /// + /// [text] 要添加到队列的文本 + /// [rate] 可选,语速,范围 -100 到 100,默认为 0 + /// [pitch] 可选,音调,范围 -100 到 100,默认为 0 + /// + /// 返回 true 表示成功添加到队列 + Future speak(String text, {int rate = 0, int pitch = 0}) async { + if (!isEnabled.value) { + return false; + } + + if (text.isEmpty) { + return false; + } + + // 生成 SSML + final ssml = generateSsml( + text: text, + rate: rate, + pitch: pitch, + ); + + // 添加到队列 + _textQueue.add(ssml); + + // 如果队列未在处理中,开始处理 + if (!_isProcessingQueue) { + _processQueue(); + } + + return true; + } + + /// 连续播放多段文本 + /// + /// [texts] 要连续播放的文本列表 + /// [rate] 可选,语速,范围 -100 到 100,默认为 0 + /// [pitch] 可选,音调,范围 -100 到 100,默认为 0 + /// + /// 返回 true 表示成功添加到队列 + Future speakMultiple(List texts, {int rate = 0, int pitch = 0}) async { + if (!isEnabled.value) { + return false; + } + + if (texts.isEmpty) { + return false; + } + + // 将所有文本添加到队列 + for (final text in texts) { + if (text.isNotEmpty) { + final ssml = generateSsml( + text: text, + rate: rate, + pitch: pitch, + ); + _textQueue.add(ssml); + } + } + + // 如果队列未在处理中,开始处理 + if (!_isProcessingQueue) { + _processQueue(); + } + + return true; + } + + /// 处理语音合成队列 + Future _processQueue() async { + if (_textQueue.isEmpty || _isProcessingQueue) { + return; + } + + _isProcessingQueue = true; + + try { + while (_textQueue.isNotEmpty) { + // 如果服务被禁用,清空队列并退出 + if (!isEnabled.value) { + _textQueue.clear(); + break; + } + + // 获取队列中的下一个 SSML + final ssml = _textQueue.removeAt(0); + + // 播放 SSML + await speakSsml(ssml); + } + } catch (e) { + print('处理语音队列时出错: $e'); + } finally { + _isProcessingQueue = false; + } + } + + /// 停止当前语音合成并清空队列 + Future stop() async { + // 清空队列 + _textQueue.clear(); + + // 如果当前正在播放,尝试停止 + if (_isSpeaking) { + try { + await _channel.invokeMethod('dispose'); + await initialize(); // 重新初始化以确保资源正确释放和重建 + _isSpeaking = false; + isSpeaking.value = false; + } catch (e) { + print('停止语音合成时出错: $e'); + } + } + } + + /// 生成 SSML 文本 + /// + /// [text] 要转换的文本 + /// [voiceName] 可选,语音名称,默认使用当前设置的语音 + /// [rate] 可选,语速,范围 -100 到 100,默认为 0 + /// [pitch] 可选,音调,范围 -100 到 100,默认为 0 + /// + /// 返回 SSML 格式的文本 + String generateSsml({ + required String text, + String? voiceName, + int rate = 0, + int pitch = 0, + }) { + final voice = voiceName ?? _currentVoice; + final rateValue = rate.clamp(-100, 100); + final pitchValue = pitch.clamp(-100, 100); + + // 将 rate 和 pitch 转换为 SSML 格式的值 + final String rateStr = _convertRateToSsml(rateValue); + final String pitchStr = _convertPitchToSsml(pitchValue); + + return ''' + + + + $text + + + + '''; + } + + /// 将 rate 值转换为 SSML 格式 + String _convertRateToSsml(int rate) { + if (rate == 0) return '0%'; + + // 将 -100 到 100 的范围映射到 -90% 到 100% + if (rate < 0) { + // 负值映射到 -90% 到 0% + return '${(rate * 0.9).round()}%'; + } else { + // 正值映射到 0% 到 100% + return '${rate}%'; + } + } + + /// 将 pitch 值转换为 SSML 格式 + String _convertPitchToSsml(int pitch) { + if (pitch == 0) return '0%'; + + // 将 -100 到 100 的范围映射到 -50% 到 50% + return '${(pitch * 0.5).round()}%'; + } + + /// 释放资源 + Future dispose() async { + if (!_isInitialized) return; + + try { + await _channel.invokeMethod('dispose'); + _isInitialized = false; + } on PlatformException catch (e) { + throw MicrosoftTtsException('释放资源失败: ${e.message}'); + } + } +} \ No newline at end of file diff --git a/lib/data/services/my_audio_handler.dart b/lib/data/services/my_audio_handler.dart index 4fb751fae..5c845b786 100644 --- a/lib/data/services/my_audio_handler.dart +++ b/lib/data/services/my_audio_handler.dart @@ -6,14 +6,17 @@ import 'package:flutter/widgets.dart'; import 'background_agent_service.dart'; import '../../data/providers/agent_provider.dart'; -/// 自定义 AudioHandler,用于捕获媒体按键事件(例如蓝牙耳机双击) +/// 自定义 AudioHandler,用于捕获媒体按键事件(例如蓝牙耳机按钮) class MyAudioHandler extends BaseAudioHandler { // 定义一个 StreamController 用于向 UI 发送自定义事件 final StreamController _customEventController = StreamController.broadcast(); bool _isDisposed = false; - late final BackgroundAgentService _backgroundAgent; - late final String _systemPrompt; + BackgroundAgentService? _backgroundAgent; + String _systemPrompt = ''; + + // 添加一个标志,表示是否正在处理交互 + bool _isHandlingInteraction = false; Stream get customEventStream => _customEventController.stream; @@ -21,6 +24,7 @@ class MyAudioHandler extends BaseAudioHandler { _initializeHandler(); _initializeBackgroundAgent(); _initializeSystemPrompt(); + print('MyAudioHandler 已创建,准备接收媒体按钮事件'); } void _initializeSystemPrompt() { @@ -35,9 +39,15 @@ class MyAudioHandler extends BaseAudioHandler { void _initializeBackgroundAgent() { try { - _backgroundAgent = Get.put(BackgroundAgentService(), permanent: true); + if (Get.isRegistered()) { + _backgroundAgent = Get.find(); + } else { + _backgroundAgent = Get.put(BackgroundAgentService(), permanent: true); + } + print('BackgroundAgent 初始化成功'); } catch (e) { print('初始化 BackgroundAgent 失败: $e'); + _backgroundAgent = null; } } @@ -82,6 +92,7 @@ class MyAudioHandler extends BaseAudioHandler { ); mediaItem.add(item); + print('AudioHandler 媒体项已设置'); } catch (e) { print('初始化 AudioHandler 失败: $e'); } @@ -106,49 +117,82 @@ class MyAudioHandler extends BaseAudioHandler { } Future _handleInteraction() async { - if (_systemPrompt.isEmpty) return; + print('处理媒体按钮交互'); + + // 防止重复处理 + if (_isHandlingInteraction) { + print('已经在处理交互,忽略此次请求'); + return; + } + + _isHandlingInteraction = true; + + try { + if (_systemPrompt.isEmpty) { + print('系统提示为空,无法激活语音助手'); + return; + } - if (Get.context != null && - WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) { - // 应用在前台,导航到聊天页面 - final agent = AgentProvider.getAgentById('personal_assistant'); - if (agent != null) { - // 导航到聊天页面,并标记需要在进入时播放语音应答 - Get.toNamed('/chat', arguments: { - 'agentId': agent.id, - 'agentName': agent.name, - 'agentAvatar': agent.avatarUrl, - 'agentSubtitle': agent.description, - 'systemPrompt': agent.systemPrompt, - 'welcomeMessage': agent.welcomeMessage, - 'playVoiceOnEnter': true, - }); + if (Get.context != null && + WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) { + // 应用在前台,导航到聊天页面 + print('应用在前台,导航到聊天页面'); + final agent = AgentProvider.getAgentById('personal_assistant'); + if (agent != null) { + // 导航到聊天页面,并标记需要在进入时播放语音应答 + Get.toNamed('/chat', arguments: { + 'agentId': agent.id, + 'agentName': agent.name, + 'agentAvatar': agent.avatarUrl, + 'agentSubtitle': agent.description, + 'systemPrompt': agent.systemPrompt, + 'welcomeMessage': agent.welcomeMessage, + 'playVoiceOnEnter': true, + }); + } + } else { + // 应用在后台,使用 BackgroundAgent 处理交互 + print('应用在后台,使用 BackgroundAgent 处理交互'); + if (_backgroundAgent != null) { + // 调用 BackgroundAgent 的语音识别和 AI 交互功能 + await _backgroundAgent!.handleAgentInteraction(_systemPrompt); + } else { + print('BackgroundAgent 未初始化,无法处理交互'); + } } - } else { - // 应用在后台,使用 BackgroundAgent 处理交互 - await _backgroundAgent.handleAgentInteraction(_systemPrompt); + } catch (e) { + print('处理媒体按钮交互失败: $e'); + } finally { + _isHandlingInteraction = false; } } @override Future play() async { print('收到播放命令'); - await _handleInteraction(); - _updatePlaybackState( - playing: true, - processingState: AudioProcessingState.ready, - ); + try { + await _handleInteraction(); + _updatePlaybackState( + playing: true, + processingState: AudioProcessingState.ready, + ); + } catch (e) { + print('处理播放命令失败: $e'); + } } @override Future pause() async { print('收到暂停命令'); - await _handleInteraction(); - - _updatePlaybackState( - playing: false, - processingState: AudioProcessingState.ready, - ); + try { + await _handleInteraction(); + _updatePlaybackState( + playing: false, + processingState: AudioProcessingState.ready, + ); + } catch (e) { + print('处理暂停命令失败: $e'); + } } @override @@ -170,13 +214,21 @@ class MyAudioHandler extends BaseAudioHandler { @override Future skipToPrevious() async { print('收到上一曲命令'); - await _handleInteraction(); + try { + await _handleInteraction(); + } catch (e) { + print('处理上一曲命令失败: $e'); + } } @override Future skipToNext() async { print('收到下一曲命令'); - await _handleInteraction(); + try { + await _handleInteraction(); + } catch (e) { + print('处理下一曲命令失败: $e'); + } } @override @@ -188,20 +240,34 @@ class MyAudioHandler extends BaseAudioHandler { Future customAction(String name, [Map? extras]) async { print('收到自定义命令: $name'); - if (name == 'media_button') { - await _handleInteraction(); + try { + if (name == 'media_button') { + print('收到媒体按钮事件'); + await _handleInteraction(); + } + } catch (e) { + print('处理自定义命令失败: $e'); } + return null; } @override Future onTaskRemoved() async { print('服务被系统移除'); - await stop(); + try { + await stop(); + } catch (e) { + print('处理服务移除失败: $e'); + } } @override Future onNotificationDeleted() async { print('通知被用户移除'); - await stop(); + try { + await stop(); + } catch (e) { + print('处理通知移除失败: $e'); + } } } diff --git a/lib/data/services/volcano_tts_service.dart b/lib/data/services/volcano_tts_service.dart index 7d7438dbc..8a6c06ac8 100644 --- a/lib/data/services/volcano_tts_service.dart +++ b/lib/data/services/volcano_tts_service.dart @@ -556,6 +556,21 @@ class VolcanoTtsService extends GetxService { _sentenceQueue.clear(); _isFetching = false; isPlaying.value = false; + + // 确保清空所有待处理的音频数据 + try { + // 取消所有正在进行的WebSocket连接 + await _channel?.sink.close(); + _channel = null; + + // 重置播放器状态 + await _audioPlayer.pause(); + await _audioPlayer.seek(Duration.zero); + + print('已清空所有待播放的文字和音频'); + } catch (e) { + print('清空音频缓存时出错: $e'); + } } catch (e) { print('停止播放失败: $e'); } diff --git a/lib/main.dart b/lib/main.dart index 767d70961..4879e8904 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -41,6 +41,9 @@ void main() async { // 初始化存储 await GetStorage.init(); + // AudioService 将在 AudioServiceManager 中初始化 + // 不再需要在这里调用 AudioService.init + runApp(const MainApp()); } diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 0b75ddcc7..85b914341 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -60,16 +60,270 @@ class ChatController extends GetxController { isVoiceMode.value = !isVoiceMode.value; } - // 开始按住说话 - void startPressToTalk() { - isRecordingVoice.value = true; - startVoiceInput(); + // 辅助方法:管理TTS状态 + void _manageTtsState(bool enable) { + if (!enable && _ttsService.isEnabled.value) { + // 需要禁用TTS + _ttsService.stop(); // 先停止当前播放 + _ttsService.isEnabled.value = false; + } else if (enable && !_ttsService.isEnabled.value) { + // 需要启用TTS + _ttsService.isEnabled.value = true; + } } - - // 结束按住说话 - void endPressToTalk() { + + // 开始语音输入 + void startVoiceInput() { + // 确保先停止任何可能正在进行的语音识别会话 + if (Get.isRegistered()) { + final voiceController = Get.find(); + voiceController.stopRecording(); + Get.delete(); + } + + // 不再需要禁用TTS,因为已启用回声消除 + + isVoiceInputVisible.value = true; + isVoiceConnecting.value = true; + isRecording.value = false; + recordingText.value = ''; + + // 当语音面板打开时,确保ListView滚动到适当位置,防止最后的消息被遮挡 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!_isDisposed && scrollController.hasClients && messages.isNotEmpty) { + // 计算需要额外滚动的距离(语音面板高度) + final extraScrollDistance = 120.0; + + // 获取当前滚动位置 + final currentPosition = scrollController.position.pixels; + final maxScrollExtent = scrollController.position.maxScrollExtent; + + // 如果已经接近底部,则向上滚动一定距离,确保最后的消息可见 + if (maxScrollExtent - currentPosition < extraScrollDistance) { + scrollController.animateTo( + currentPosition + extraScrollDistance, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + } + }); + + // 创建语音输入控制器 + Get.put(VoiceInputController( + onRecordingResult: handleVoiceResult, + onClosePanel: stopVoiceInput, + onRecognizing: handleRecognizing, + )); + } + + void startRecording() { + if (!isVoiceConnecting.value && isVoiceInputVisible.value) { + // 不再需要禁用TTS,因为已启用回声消除 + + isRecording.value = true; + } + } + + void stopRecording() { + if (isRecording.value) { + isRecording.value = false; + } + } + + void toggleMute() { + isVoiceMuted.value = !isVoiceMuted.value; + } + + void stopVoiceInput() { + // 确保停止语音识别 + if (Get.isRegistered()) { + try { + final voiceController = Get.find(); + voiceController.stopRecording(); + } catch (e) { + debugPrint('Error stopping voice recording: $e'); + } + } + + isVoiceInputVisible.value = false; + isVoiceConnecting.value = true; + isRecording.value = false; + recordingText.value = ''; + + // 不再需要恢复TTS状态,因为已启用回声消除 + + // 如果存在临时语音消息但没有实际内容,则移除它 + if (_currentVoiceMessage != null && _currentVoiceMessage!.content == '🎤 ...') { + messages.remove(_currentVoiceMessage); + } + _currentVoiceMessage = null; + + // 当语音面板关闭时,确保ListView滚动回适当位置 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!_isDisposed && scrollController.hasClients && messages.isNotEmpty) { + // 滚动到底部,确保最新消息可见 + _scrollToBottom(animate: true); + } + }); + + // 删除语音输入控制器 + try { + if (Get.isRegistered()) { + Get.delete(); + } + } catch (e) { + debugPrint('Error deleting VoiceInputController: $e'); + } + + // 确保按住说话状态被重置 isRecordingVoice.value = false; - stopVoiceInput(); + } + + void handleRecognizing(String text) { + if (text.isNotEmpty) { + // 更新识别中的文本,但不创建消息 + recordingText.value = text; + } + } + + void handleVoiceResult(String text) { + if (text.isNotEmpty) { + // 创建一个新的用户消息 + final userMessage = Message( + role: 'user', + content: text, + timestamp: DateTime.now(), + ); + + // 添加到消息列表 + messages.add(userMessage); + + // 保存聊天历史 + _saveChatHistory(); + + // 滚动到底部 + _scrollToBottom(animate: true); + + // 重新启用TTS,以便AI回复时可以播放 + _manageTtsState(true); + + // 发送给AI处理 + _processAIResponse(userMessage); + } + } + + // 处理AI响应 + Future _processAIResponse(Message userMessage) async { + if (_isDisposed) return; + + try { + isLoading.value = true; + + currentStreamMessage.value = ''; + _currentAssistantMessage = Message( + role: 'assistant', + content: '', + timestamp: DateTime.now(), + ); + messages.add(_currentAssistantMessage!); + _pendingTtsText = ''; + + await for (final chunk in _aiService.sendMessageStream( + messages: messages + .map((m) => { + 'role': m.role, + 'content': m.content, + }) + .toList(), + systemPrompt: systemPrompt, + )) { + if (_isDisposed) break; + + currentStreamMessage.value += chunk; + _updateAssistantMessage(currentStreamMessage.value); + + // 累积文本并合成 + _pendingTtsText += chunk; + if (!_isDisposed && _ttsService.isEnabled.value) { + // 检查是否达到最小长度 + if (_pendingTtsText.length >= _minTtsLength) { + // 找到最后一个句子结束的位置 + int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText); + if (lastSentenceEnd > 0) { + // 播放到最后一个句子结束的位置 + String textToSpeak = + _pendingTtsText.substring(0, lastSentenceEnd + 1); + _ttsService.speak(textToSpeak); + // 保留剩余的文本 + _pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1); + } + } + } + } + + // 处理剩余的文本 + if (!_isDisposed && + _ttsService.isEnabled.value && + _pendingTtsText.isNotEmpty) { + _ttsService.speak(_pendingTtsText); + } + _pendingTtsText = ''; + + if (!_isDisposed) { + await _saveChatHistory(); + } + } catch (e) { + if (!_isDisposed) { + if (_currentAssistantMessage != null) { + messages.remove(_currentAssistantMessage); + } + Get.snackbar( + 'Error', + 'Failed to get response from AI: $e', + snackPosition: SnackPosition.BOTTOM, + ); + } + } finally { + if (!_isDisposed) { + _currentAssistantMessage = null; + currentStreamMessage.value = ''; + isLoading.value = false; + } + } + } + + // 确保滚动到底部的方法,使用多种策略确保成功 + void _ensureScrollToBottom() { + // 立即尝试滚动 + _scrollToBottom(); + + // 延迟100ms后再次尝试滚动(等待视图构建) + Future.delayed(const Duration(milliseconds: 100), () { + if (!_isDisposed) _scrollToBottom(animate: true); + }); + + // 延迟500ms后再次尝试滚动(确保所有元素都已加载) + Future.delayed(const Duration(milliseconds: 500), () { + if (!_isDisposed) _scrollToBottom(animate: true); + }); + + // 使用帧回调确保在渲染后滚动 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!_isDisposed) _scrollToBottom(animate: true); + }); + } + + // 滚动监听器 + void _scrollListener() { + if (_isDisposed) return; + + // 检测是否接近底部 + if (scrollController.hasClients) { + final maxScroll = scrollController.position.maxScrollExtent; + final currentScroll = scrollController.offset; + isAtBottom.value = (maxScroll - currentScroll) < 50; + } } @override @@ -467,7 +721,11 @@ class ChatController extends GetxController { } void toggleTTS() { + // 使用 VolcanoTtsService 的 toggleEnabled 方法 _ttsService.toggleEnabled(); + if (!_ttsService.isEnabled.value) { + _ttsService.stop(); + } } /// 生成简单的问候语 @@ -520,248 +778,17 @@ class ChatController extends GetxController { } } - // 开始语音输入 - void startVoiceInput() { - // 确保先停止任何可能正在进行的语音识别会话 - if (Get.isRegistered()) { - final voiceController = Get.find(); - voiceController.stopRecording(); - Get.delete(); - } - - isVoiceInputVisible.value = true; - isVoiceConnecting.value = true; - isRecording.value = false; - recordingText.value = ''; - - // 当语音面板打开时,确保ListView滚动到适当位置,防止最后的消息被遮挡 - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!_isDisposed && scrollController.hasClients && messages.isNotEmpty) { - // 计算需要额外滚动的距离(语音面板高度) - final extraScrollDistance = 120.0; - - // 获取当前滚动位置 - final currentPosition = scrollController.position.pixels; - final maxScrollExtent = scrollController.position.maxScrollExtent; - - // 如果已经接近底部,则向上滚动一定距离,确保最后的消息可见 - if (maxScrollExtent - currentPosition < extraScrollDistance) { - scrollController.animateTo( - currentPosition + extraScrollDistance, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } - } - }); - - // 创建语音输入控制器 - Get.put(VoiceInputController( - onRecordingResult: handleVoiceResult, - onClosePanel: stopVoiceInput, - onRecognizing: handleRecognizing, - )); - } - - void startRecording() { - if (!isVoiceConnecting.value && isVoiceInputVisible.value) { - isRecording.value = true; - } - } - - void stopRecording() { - if (isRecording.value) { - isRecording.value = false; - } - } - - void toggleMute() { - isVoiceMuted.value = !isVoiceMuted.value; - } - - void stopVoiceInput() { - // 确保停止语音识别 - if (Get.isRegistered()) { - try { - final voiceController = Get.find(); - voiceController.stopRecording(); - } catch (e) { - debugPrint('Error stopping voice recording: $e'); - } - } - - isVoiceInputVisible.value = false; - isVoiceConnecting.value = true; - isRecording.value = false; - recordingText.value = ''; - - // 如果存在临时语音消息但没有实际内容,则移除它 - if (_currentVoiceMessage != null && _currentVoiceMessage!.content == '🎤 ...') { - messages.remove(_currentVoiceMessage); - } - _currentVoiceMessage = null; - - // 当语音面板关闭时,确保ListView滚动回适当位置 - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!_isDisposed && scrollController.hasClients && messages.isNotEmpty) { - // 滚动到底部,确保最新消息可见 - _scrollToBottom(animate: true); - } - }); - - // 删除语音输入控制器 - try { - if (Get.isRegistered()) { - Get.delete(); - } - } catch (e) { - debugPrint('Error deleting VoiceInputController: $e'); - } + // 开始按住说话 + void startPressToTalk() { + isRecordingVoice.value = true; - // 确保按住说话状态被重置 - isRecordingVoice.value = false; - } - - void handleRecognizing(String text) { - if (text.isNotEmpty) { - // 更新识别中的文本,但不创建消息 - recordingText.value = text; - } - } - - void handleVoiceResult(String text) { - if (text.isNotEmpty) { - // 创建一个新的用户消息 - final userMessage = Message( - role: 'user', - content: text, - timestamp: DateTime.now(), - ); - - // 添加到消息列表 - messages.add(userMessage); - - // 保存聊天历史 - _saveChatHistory(); - - // 滚动到底部 - _scrollToBottom(animate: true); - - // 发送给AI处理 - _processAIResponse(userMessage); - } + startVoiceInput(); } - // 处理AI响应 - Future _processAIResponse(Message userMessage) async { - if (_isDisposed) return; - - try { - isLoading.value = true; - - currentStreamMessage.value = ''; - _currentAssistantMessage = Message( - role: 'assistant', - content: '', - timestamp: DateTime.now(), - ); - messages.add(_currentAssistantMessage!); - _pendingTtsText = ''; - - await for (final chunk in _aiService.sendMessageStream( - messages: messages - .map((m) => { - 'role': m.role, - 'content': m.content, - }) - .toList(), - systemPrompt: systemPrompt, - )) { - if (_isDisposed) break; - - currentStreamMessage.value += chunk; - _updateAssistantMessage(currentStreamMessage.value); - - // 累积文本并合成 - _pendingTtsText += chunk; - if (!_isDisposed && _ttsService.isEnabled.value) { - // 检查是否达到最小长度 - if (_pendingTtsText.length >= _minTtsLength) { - // 找到最后一个句子结束的位置 - int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText); - if (lastSentenceEnd > 0) { - // 播放到最后一个句子结束的位置 - String textToSpeak = - _pendingTtsText.substring(0, lastSentenceEnd + 1); - _ttsService.speak(textToSpeak); - // 保留剩余的文本 - _pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1); - } - } - } - } - - // 处理剩余的文本 - if (!_isDisposed && - _ttsService.isEnabled.value && - _pendingTtsText.isNotEmpty) { - _ttsService.speak(_pendingTtsText); - } - _pendingTtsText = ''; - - if (!_isDisposed) { - await _saveChatHistory(); - } - } catch (e) { - if (!_isDisposed) { - if (_currentAssistantMessage != null) { - messages.remove(_currentAssistantMessage); - } - Get.snackbar( - 'Error', - 'Failed to get response from AI: $e', - snackPosition: SnackPosition.BOTTOM, - ); - } - } finally { - if (!_isDisposed) { - _currentAssistantMessage = null; - currentStreamMessage.value = ''; - isLoading.value = false; - } - } - } - - // 确保滚动到底部的方法,使用多种策略确保成功 - void _ensureScrollToBottom() { - // 立即尝试滚动 - _scrollToBottom(); - - // 延迟100ms后再次尝试滚动(等待视图构建) - Future.delayed(const Duration(milliseconds: 100), () { - if (!_isDisposed) _scrollToBottom(animate: true); - }); - - // 延迟500ms后再次尝试滚动(确保所有元素都已加载) - Future.delayed(const Duration(milliseconds: 500), () { - if (!_isDisposed) _scrollToBottom(animate: true); - }); - - // 使用帧回调确保在渲染后滚动 - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!_isDisposed) _scrollToBottom(animate: true); - }); - } - - // 滚动监听器 - void _scrollListener() { - if (_isDisposed) return; + // 结束按住说话 + void endPressToTalk() { + isRecordingVoice.value = false; - // 检测是否接近底部 - if (scrollController.hasClients) { - final maxScroll = scrollController.position.maxScrollExtent; - final currentScroll = scrollController.offset; - isAtBottom.value = (maxScroll - currentScroll) < 50; - } + stopVoiceInput(); } } diff --git a/lib/modules/chat/controllers/voice_input_controller.dart b/lib/modules/chat/controllers/voice_input_controller.dart index f023775de..030543d6f 100644 --- a/lib/modules/chat/controllers/voice_input_controller.dart +++ b/lib/modules/chat/controllers/voice_input_controller.dart @@ -57,7 +57,7 @@ class VoiceInputController extends GetxController { } Future startContinuousRecognition() async { - if (isConnecting.value || isMuted.value) return; + if (isConnecting.value) return; if (isRecording.value) return; // 已经在录音中 try { @@ -136,8 +136,6 @@ class VoiceInputController extends GetxController { // 重新启动识别 Future restartRecognition() async { - if (isMuted.value) return; - try { // 先停止当前识别 await stopRecording(); @@ -171,13 +169,9 @@ class VoiceInputController extends GetxController { } void toggleMute() { + // 只切换静音状态,不再影响录音 + // 因为已启用回声消除,不需要在录音时停止TTS isMuted.value = !isMuted.value; - if (isRecording.value && isMuted.value) { - stopRecording(); - } else if (!isMuted.value && !isRecording.value) { - // 如果取消静音,自动开始识别 - startContinuousRecognition(); - } } // 手动开始录音(用户点击按钮) diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart index 7c3d702f6..30ca9867e 100644 --- a/lib/modules/chat/views/chat_view.dart +++ b/lib/modules/chat/views/chat_view.dart @@ -118,7 +118,7 @@ class ChatView extends GetView { padding: EdgeInsets.only( top: 16.h, bottom: controller.isVoiceInputVisible.value - ? 200.h + ? 160.h : (controller.isInputCollapsed.value ? 70.h : 90.h), ), itemCount: controller.messages.length, @@ -131,7 +131,7 @@ class ChatView extends GetView { if (!controller.isLoading.value) return const SizedBox.shrink(); return Positioned( bottom: controller.isVoiceInputVisible.value - ? 210.h + ? 170.h : (controller.isInputCollapsed.value ? 80.h : 100.h), left: 0, right: 0, @@ -168,10 +168,15 @@ class ChatView extends GetView { Widget _buildInputSection() { return Obx(() { if (controller.isVoiceInputVisible.value) { - return VoiceInputPanel( - onRecordingResult: controller.handleVoiceResult, - onClose: controller.stopVoiceInput, - onRecognizing: controller.handleRecognizing, + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + VoiceInputPanel( + onRecordingResult: controller.handleVoiceResult, + onClose: controller.stopVoiceInput, + onRecognizing: controller.handleRecognizing, + ), + ], ); } return _buildTextInput(); diff --git a/lib/modules/microsoft_tts_continuous_example.dart b/lib/modules/microsoft_tts_continuous_example.dart new file mode 100644 index 000000000..24c5e0990 --- /dev/null +++ b/lib/modules/microsoft_tts_continuous_example.dart @@ -0,0 +1,308 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../data/services/microsoft_tts_service.dart'; + +/// 微软 TTS 连续语音输出示例页面 +class MicrosoftTtsContinuousExample extends StatefulWidget { + const MicrosoftTtsContinuousExample({Key? key}) : super(key: key); + + @override + State createState() => _MicrosoftTtsContinuousExampleState(); +} + +class _MicrosoftTtsContinuousExampleState extends State { + final MicrosoftTtsService _ttsService = Get.find(); + + // 语音列表 + final List> _voices = [ + {'name': '晓晓(女声)', 'value': 'zh-CN-XiaoxiaoNeural'}, + {'name': '云扬(男声)', 'value': 'zh-CN-YunyangNeural'}, + {'name': '晓双(女声)', 'value': 'zh-CN-XiaoshuangNeural'}, + {'name': '云皓(男声)', 'value': 'zh-CN-YunhaoNeural'}, + {'name': '晓墨(女声)', 'value': 'zh-CN-XiaomoNeural'}, + {'name': '云泽(男声)', 'value': 'zh-CN-YunzeNeural'}, + ]; + + String _selectedVoice = 'zh-CN-XiaoxiaoNeural'; + double _rate = 0; + double _pitch = 0; + bool _isLoading = false; + String _statusMessage = ''; + + // 预设的连续语音文本 + final List _presetTexts = [ + '欢迎使用微软语音合成服务,这是连续语音输出的第一段文本。', + '这是第二段文本,用于测试连续语音输出功能。', + '现在是第三段文本,我们正在测试微软语音合成服务的连续合成能力。', + '最后一段测试文本,感谢您的收听。', + ]; + + // 自定义文本列表 + final List _textControllers = []; + + @override + void initState() { + super.initState(); + // 初始化文本控制器 + for (final text in _presetTexts) { + _textControllers.add(TextEditingController(text: text)); + } + } + + @override + void dispose() { + // 释放文本控制器 + for (final controller in _textControllers) { + controller.dispose(); + } + super.dispose(); + } + + /// 播放连续文本 + Future _speakContinuous() async { + final texts = _textControllers.map((controller) => controller.text).toList(); + + if (texts.every((text) => text.isEmpty)) { + _showSnackBar('请至少输入一段文本'); + return; + } + + setState(() { + _isLoading = true; + _statusMessage = '正在合成语音...'; + }); + + try { + // 设置语音 + await _ttsService.setVoice(_selectedVoice); + + // 停止之前的播放 + await _ttsService.stop(); + + // 连续播放多段文本 + final result = await _ttsService.speakMultiple( + texts.where((text) => text.isNotEmpty).toList(), + rate: _rate.round(), + pitch: _pitch.round(), + ); + + setState(() { + _statusMessage = result ? '语音合成已加入队列' : '语音合成失败'; + }); + } catch (e) { + _showSnackBar('语音合成失败: $e'); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + /// 停止播放 + Future _stopSpeaking() async { + try { + await _ttsService.stop(); + setState(() { + _statusMessage = '语音合成已停止'; + }); + } catch (e) { + _showSnackBar('停止语音合成失败: $e'); + } + } + + /// 添加文本输入框 + void _addTextInput() { + setState(() { + _textControllers.add(TextEditingController()); + }); + } + + /// 删除文本输入框 + void _removeTextInput(int index) { + if (_textControllers.length <= 1) { + _showSnackBar('至少需要保留一个文本输入框'); + return; + } + + setState(() { + _textControllers[index].dispose(); + _textControllers.removeAt(index); + }); + } + + /// 显示提示信息 + void _showSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('微软连续语音合成示例'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: ListView( + children: [ + // 语音选择 + DropdownButtonFormField( + value: _selectedVoice, + decoration: const InputDecoration( + labelText: '选择语音', + border: OutlineInputBorder(), + ), + items: _voices.map((voice) { + return DropdownMenuItem( + value: voice['value'], + child: Text(voice['name']!), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() { + _selectedVoice = value; + }); + } + }, + ), + const SizedBox(height: 16), + + // 语速调节 + Row( + children: [ + const Text('语速:'), + Expanded( + child: Slider( + min: -100, + max: 100, + divisions: 20, + value: _rate, + label: _rate.round().toString(), + onChanged: (value) { + setState(() { + _rate = value; + }); + }, + ), + ), + Text('${_rate.round()}%'), + ], + ), + + // 音调调节 + Row( + children: [ + const Text('音调:'), + Expanded( + child: Slider( + min: -100, + max: 100, + divisions: 20, + value: _pitch, + label: _pitch.round().toString(), + onChanged: (value) { + setState(() { + _pitch = value; + }); + }, + ), + ), + Text('${_pitch.round()}%'), + ], + ), + + const SizedBox(height: 16), + + // 文本输入列表标题 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '连续语音文本', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ElevatedButton.icon( + onPressed: _addTextInput, + icon: const Icon(Icons.add), + label: const Text('添加文本'), + ), + ], + ), + + const SizedBox(height: 8), + + // 文本输入列表 + ...List.generate(_textControllers.length, (index) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: _textControllers[index], + maxLines: 3, + decoration: InputDecoration( + labelText: '文本 ${index + 1}', + border: const OutlineInputBorder(), + ), + ), + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => _removeTextInput(index), + ), + ], + ), + ); + }), + + const SizedBox(height: 16), + + // 操作按钮 + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _isLoading ? null : _speakContinuous, + icon: const Icon(Icons.play_arrow), + label: const Text('播放连续语音'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + onPressed: _stopSpeaking, + icon: const Icon(Icons.stop), + label: const Text('停止'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), + ), + ), + ], + ), + + const SizedBox(height: 16), + + // 状态信息 + Obx(() => Text( + _ttsService.isSpeaking.value + ? '正在播放语音...' + : _statusMessage, + style: const TextStyle(fontStyle: FontStyle.italic), + textAlign: TextAlign.center, + )), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/modules/microsoft_tts_example.dart b/lib/modules/microsoft_tts_example.dart new file mode 100644 index 000000000..51ebeff45 --- /dev/null +++ b/lib/modules/microsoft_tts_example.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../data/services/microsoft_tts_service.dart'; + +/// 微软 TTS 示例页面 +class MicrosoftTtsExample extends StatefulWidget { + const MicrosoftTtsExample({Key? key}) : super(key: key); + + @override + State createState() => _MicrosoftTtsExampleState(); +} + +class _MicrosoftTtsExampleState extends State { + final TextEditingController _textController = TextEditingController(); + final MicrosoftTtsService _ttsService = Get.find(); + + // 语音列表 + final List> _voices = [ + {'name': '晓晓(女声)', 'value': 'zh-CN-XiaoxiaoNeural'}, + {'name': '云扬(男声)', 'value': 'zh-CN-YunyangNeural'}, + {'name': '晓双(女声)', 'value': 'zh-CN-XiaoshuangNeural'}, + {'name': '云皓(男声)', 'value': 'zh-CN-YunhaoNeural'}, + {'name': '晓墨(女声)', 'value': 'zh-CN-XiaomoNeural'}, + {'name': '云泽(男声)', 'value': 'zh-CN-YunzeNeural'}, + ]; + + String _selectedVoice = 'zh-CN-XiaoxiaoNeural'; + double _rate = 0; + double _pitch = 0; + bool _isLoading = false; + String _statusMessage = ''; + + @override + void initState() { + super.initState(); + _textController.text = '欢迎使用微软语音合成服务,这是一个示例文本。'; + } + + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } + + /// 播放文本 + Future _speakText() async { + if (_textController.text.isEmpty) { + _showSnackBar('请输入要合成的文本'); + return; + } + + setState(() { + _isLoading = true; + _statusMessage = '正在合成语音...'; + }); + + try { + // 设置语音 + await _ttsService.setVoice(_selectedVoice); + + // 生成 SSML + final ssml = _ttsService.generateSsml( + text: _textController.text, + rate: _rate.round(), + pitch: _pitch.round(), + ); + + // 播放 SSML + final result = await _ttsService.speakSsml(ssml); + + setState(() { + _statusMessage = result; + }); + } catch (e) { + _showSnackBar('语音合成失败: $e'); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + /// 显示提示信息 + void _showSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('微软语音合成示例'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 文本输入框 + TextField( + controller: _textController, + maxLines: 5, + decoration: const InputDecoration( + labelText: '输入要合成的文本', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + + // 语音选择 + DropdownButtonFormField( + value: _selectedVoice, + decoration: const InputDecoration( + labelText: '选择语音', + border: OutlineInputBorder(), + ), + items: _voices.map((voice) { + return DropdownMenuItem( + value: voice['value'], + child: Text(voice['name']!), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() { + _selectedVoice = value; + }); + } + }, + ), + const SizedBox(height: 16), + + // 语速调节 + Row( + children: [ + const Text('语速:'), + Expanded( + child: Slider( + min: -100, + max: 100, + divisions: 20, + value: _rate, + label: _rate.round().toString(), + onChanged: (value) { + setState(() { + _rate = value; + }); + }, + ), + ), + Text('${_rate.round()}%'), + ], + ), + + // 音调调节 + Row( + children: [ + const Text('音调:'), + Expanded( + child: Slider( + min: -100, + max: 100, + divisions: 20, + value: _pitch, + label: _pitch.round().toString(), + onChanged: (value) { + setState(() { + _pitch = value; + }); + }, + ), + ), + Text('${_pitch.round()}%'), + ], + ), + + const SizedBox(height: 16), + + // 播放按钮 + ElevatedButton( + onPressed: _isLoading ? null : _speakText, + child: _isLoading + ? const CircularProgressIndicator() + : const Text('播放'), + ), + + const SizedBox(height: 16), + + // 状态信息 + Text( + _statusMessage, + style: const TextStyle(fontStyle: FontStyle.italic), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index fd22bf44a..c384cbfc2 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -5,6 +5,9 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../data/services/volcano_tts_service.dart'; import '../../../core/widgets/common_bottom_nav.dart'; import '../../speech_demo/speech_demo_page.dart'; +import '../../../modules/microsoft_tts_example.dart'; +import '../../../modules/microsoft_tts_continuous_example.dart'; +import '../../../data/services/microsoft_tts_service.dart'; class ProfileView extends GetView { const ProfileView({Key? key}) : super(key: key); @@ -65,10 +68,10 @@ class ProfileView extends GetView { _buildMenuItem( title: '测试语音合成', icon: Icons.record_voice_over, - subtitle: '测试火山语音TTS功能', + subtitle: '测试微软语音TTS功能', onTap: () async { try { - final tts = Get.find(); + final tts = Get.find(); // 检查TTS服务状态 print('TTS服务状态: enabled=${tts.isEnabled.value}'); @@ -110,6 +113,24 @@ class ProfileView extends GetView { }, ), const Divider(), + _buildMenuItem( + title: '微软语音合成测试', + icon: Icons.record_voice_over_outlined, + subtitle: '测试微软 Azure TTS 功能', + onTap: () { + Get.to(() => const MicrosoftTtsExample()); + }, + ), + const Divider(), + _buildMenuItem( + title: '微软连续语音合成测试', + icon: Icons.queue_music_outlined, + subtitle: '测试微软 Azure TTS 连续语音功能', + onTap: () { + Get.to(() => const MicrosoftTtsContinuousExample()); + }, + ), + const Divider(), _buildMenuItem( title: '语音识别测试', icon: Icons.mic,