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