Browse Source

m

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
3053367511
  1. 2
      android/app/build.gradle.kts
  2. 12
      android/app/src/main/AndroidManifest.xml
  3. 99
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  4. 10
      android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt
  5. 165
      android/app/src/main/kotlin/com/example/deep_voice/TextToSpeechHelper.kt
  6. 11
      android/app/src/main/res/drawable/ic_notification.xml
  7. 30
      lib/core/bindings/initial_binding.dart
  8. 30
      lib/core/controllers/permission_controller.dart
  9. 75
      lib/data/services/audio_service.dart
  10. 359
      lib/data/services/background_agent_service.dart
  11. 346
      lib/data/services/microsoft_tts_service.dart
  12. 78
      lib/data/services/my_audio_handler.dart
  13. 15
      lib/data/services/volcano_tts_service.dart
  14. 3
      lib/main.dart
  15. 517
      lib/modules/chat/controllers/chat_controller.dart
  16. 12
      lib/modules/chat/controllers/voice_input_controller.dart
  17. 11
      lib/modules/chat/views/chat_view.dart
  18. 308
      lib/modules/microsoft_tts_continuous_example.dart
  19. 202
      lib/modules/microsoft_tts_example.dart
  20. 25
      lib/modules/profile/views/profile_view.dart

2
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
}

12
android/app/src/main/AndroidManifest.xml

@ -4,11 +4,15 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Android 13+ 前台服务特定类型权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<!-- Android 13+ 通知权限 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- 添加存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
@ -16,6 +20,8 @@
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- 添加悬浮窗权限,可能有助于解决前台服务启动问题 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
android:label="deep_voice"
@ -30,14 +36,16 @@
<!-- Audio Service -->
<service android:name="com.ryanheise.audioservice.AudioService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true">
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>

99
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<String>("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<String>("subscriptionKey")
val serviceRegion = call.argument<String>("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<String>("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<String>("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<String>("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()
}
}

10
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
@ -15,13 +16,14 @@ class SpeechRecognitionHelper {
fun initialize(subscriptionKey: String, serviceRegion: String) {
try {
val config = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion)
// 设置识别语言,例如中文
config.speechRecognitionLanguage = "zh-CN"
recognizer = SpeechRecognizer(config)
// 直接使用默认麦克风输入,不传递自定义音频处理选项
val audioConfig = AudioConfig.fromDefaultMicrophoneInput()
recognizer = SpeechRecognizer(config, audioConfig)
Log.d(TAG, "Speech SDK initialized successfully")
} catch (e: Exception) {
} catch (e: Exception) {
Log.e(TAG, "初始化失败: ${e.message}")
}
}
}
// 开始一次性语音识别

165
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<SpeechSynthesisResult> = 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<SpeechSynthesisResult> = 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)
}
}

11
android/app/src/main/res/drawable/ic_notification.xml

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="#FFFFFF">
<path
android:fillColor="#FFFFFF"
android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
</vector>

30
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;
});
// 其他服务初始化可以在这里添加
}
}

30
lib/core/controllers/permission_controller.dart

@ -11,13 +11,37 @@ class PermissionController extends GetxController {
Future<void> 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('悬浮窗权限已授予');
}
}
}
}
}

75
lib/data/services/audio_service.dart

@ -11,69 +11,96 @@ class AudioServiceManager extends GetxService {
bool _isInitialized = false;
final _lock = Lock();
// 添加一个标志,表示是否已经尝试过初始化
bool _hasAttemptedInit = false;
Future<AudioServiceManager> init() async {
print('初始化 AudioService');
print('初始化 AudioServiceManager');
if (_isInitialized) {
print('AudioService 已经初始化');
print('AudioServiceManager 已经初始化');
return this;
}
// 如果已经尝试过初始化但失败了,不再重试
if (_hasAttemptedInit) {
print('AudioServiceManager 之前初始化失败,不再重试');
return this;
}
await _cleanupExistingService();
_hasAttemptedInit = true;
try {
// 使用锁确保初始化的原子性
await _lock.synchronized(() async {
if (_isInitialized) return;
// 初始化 AudioService
_audioHandler = await AudioService.init(
// 使用 AudioService.init 初始化音频服务
_audioHandler = await AudioService.init<MyAudioHandler>(
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<void> _cleanupExistingService() async {
_isInitialized = false;
// 获取音频处理器
MyAudioHandler? get audioHandler => _audioHandler;
// 停止现有的音频处理器
// 发送媒体按钮事件
Future<void> sendMediaButtonEvent() async {
if (_audioHandler != null) {
await _audioHandler!.stop();
_audioHandler = null;
}
// 停止 AudioService
try {
await AudioService.stop();
await _audioHandler!.customAction('media_button');
} catch (e) {
print('停止 AudioService 时出错: $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();
}
}

359
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,27 +10,212 @@ class BackgroundAgentService extends GetxService {
final VolcanoAIService _aiService;
final VolcanoTtsService _ttsService;
final VoiceRecognitionService _voiceRecognitionService;
final List<Message> _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<String>? _recognitionCompleter;
// 添加一个标志,表示是否已经收到最终结果
bool _hasFinalResult = false;
// 添加一个计时器,用于在一段时间没有新的识别结果时提交当前结果
Timer? _silenceTimer;
// 添加一个计时器,用于检测用户长时间没有说话
Timer? _noSpeechTimer;
// 最后一次识别到语音的时间
DateTime? _lastSpeechTime;
// 添加一个标志,表示系统是否正在播放 TTS
bool _isSpeaking = false;
// 添加一个订阅,用于监听 TTS 状态变化
StreamSubscription? _ttsSpeakingSubscription;
BackgroundAgentService()
: _aiService = VolcanoAIService(),
_ttsService = Get.find<VolcanoTtsService>();
_ttsService = Get.find<VolcanoTtsService>(),
_voiceRecognitionService = Get.find<VoiceRecognitionService>() {
// 监听 TTS 播放状态
_ttsSpeakingSubscription = _ttsService.isPlaying.listen((speaking) {
_isSpeaking = speaking;
print('TTS 播放状态变化: $_isSpeaking');
// 如果 TTS 停止播放,且正在进行语音识别,重新启动无语音超时计时器
if (!speaking && _isListening && _hasRecognizedSpeech && _noSpeechTimer == null) {
_startNoSpeechTimer();
}
});
}
// 处理蓝牙耳机按钮触发的交互
Future<void> 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<void> _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<String>();
// 启动无语音超时计时器
_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 = '';
try {
await for (final chunk in _aiService.sendMessageStream(
messages: messages,
systemPrompt: systemPrompt,
@ -43,20 +229,33 @@ class BackgroundAgentService extends GetxService {
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) {
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<void> 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<String> 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();
}
}

346
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<String> _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<bool> 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<bool> 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<String> 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<String> 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<bool> 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<bool> speakMultiple(List<String> 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<void> _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<void> 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 '''
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN">
<voice name="$voice">
<prosody rate="$rateStr" pitch="$pitchStr">
$text
</prosody>
</voice>
</speak>
''';
}
/// 将 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<void> dispose() async {
if (!_isInitialized) return;
try {
await _channel.invokeMethod('dispose');
_isInitialized = false;
} on PlatformException catch (e) {
throw MicrosoftTtsException('释放资源失败: ${e.message}');
}
}
}

78
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<dynamic> _customEventController =
StreamController.broadcast();
bool _isDisposed = false;
late final BackgroundAgentService _backgroundAgent;
late final String _systemPrompt;
BackgroundAgentService? _backgroundAgent;
String _systemPrompt = '';
// 添加一个标志,表示是否正在处理交互
bool _isHandlingInteraction = false;
Stream<dynamic> 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 {
if (Get.isRegistered<BackgroundAgentService>()) {
_backgroundAgent = Get.find<BackgroundAgentService>();
} 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,11 +117,26 @@ class MyAudioHandler extends BaseAudioHandler {
}
Future<void> _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) {
// 应用在前台,导航到聊天页面
print('应用在前台,导航到聊天页面');
final agent = AgentProvider.getAgentById('personal_assistant');
if (agent != null) {
// 导航到聊天页面,并标记需要在进入时播放语音应答
@ -126,29 +152,47 @@ class MyAudioHandler extends BaseAudioHandler {
}
} else {
// 应用在后台,使用 BackgroundAgent 处理交互
await _backgroundAgent.handleAgentInteraction(_systemPrompt);
print('应用在后台,使用 BackgroundAgent 处理交互');
if (_backgroundAgent != null) {
// 调用 BackgroundAgent 的语音识别和 AI 交互功能
await _backgroundAgent!.handleAgentInteraction(_systemPrompt);
} else {
print('BackgroundAgent 未初始化,无法处理交互');
}
}
} catch (e) {
print('处理媒体按钮交互失败: $e');
} finally {
_isHandlingInteraction = false;
}
}
@override
Future<void> play() async {
print('收到播放命令');
try {
await _handleInteraction();
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
);
} catch (e) {
print('处理播放命令失败: $e');
}
}
@override
Future<void> pause() async {
print('收到暂停命令');
try {
await _handleInteraction();
_updatePlaybackState(
playing: false,
processingState: AudioProcessingState.ready,
);
} catch (e) {
print('处理暂停命令失败: $e');
}
}
@override
@ -170,13 +214,21 @@ class MyAudioHandler extends BaseAudioHandler {
@override
Future<void> skipToPrevious() async {
print('收到上一曲命令');
try {
await _handleInteraction();
} catch (e) {
print('处理上一曲命令失败: $e');
}
}
@override
Future<void> skipToNext() async {
print('收到下一曲命令');
try {
await _handleInteraction();
} catch (e) {
print('处理下一曲命令失败: $e');
}
}
@override
@ -188,20 +240,34 @@ class MyAudioHandler extends BaseAudioHandler {
Future<dynamic> customAction(String name,
[Map<String, dynamic>? extras]) async {
print('收到自定义命令: $name');
try {
if (name == 'media_button') {
print('收到媒体按钮事件');
await _handleInteraction();
}
} catch (e) {
print('处理自定义命令失败: $e');
}
return null;
}
@override
Future<void> onTaskRemoved() async {
print('服务被系统移除');
try {
await stop();
} catch (e) {
print('处理服务移除失败: $e');
}
}
@override
Future<void> onNotificationDeleted() async {
print('通知被用户移除');
try {
await stop();
} catch (e) {
print('处理通知移除失败: $e');
}
}
}

15
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');
}

3
lib/main.dart

@ -41,6 +41,9 @@ void main() async {
// 初始化存储
await GetStorage.init();
// AudioService 将在 AudioServiceManager 中初始化
// 不再需要在这里调用 AudioService.init
runApp(const MainApp());
}

517
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<VoiceInputController>()) {
final voiceController = Get.find<VoiceInputController>();
voiceController.stopRecording();
Get.delete<VoiceInputController>();
}
// 不再需要禁用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<VoiceInputController>()) {
try {
final voiceController = Get.find<VoiceInputController>();
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<VoiceInputController>()) {
Get.delete<VoiceInputController>();
}
} 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<void> _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<VoiceInputController>()) {
final voiceController = Get.find<VoiceInputController>();
voiceController.stopRecording();
Get.delete<VoiceInputController>();
}
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<VoiceInputController>()) {
try {
final voiceController = Get.find<VoiceInputController>();
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);
}
});
// 开始按住说话
void startPressToTalk() {
isRecordingVoice.value = true;
// 删除语音输入控制器
try {
if (Get.isRegistered<VoiceInputController>()) {
Get.delete<VoiceInputController>();
}
} catch (e) {
debugPrint('Error deleting VoiceInputController: $e');
startVoiceInput();
}
// 确保按住说话状态被重置
// 结束按住说话
void endPressToTalk() {
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);
}
}
// 处理AI响应
Future<void> _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;
}
stopVoiceInput();
}
}

12
lib/modules/chat/controllers/voice_input_controller.dart

@ -57,7 +57,7 @@ class VoiceInputController extends GetxController {
}
Future<void> 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<void> 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();
}
}
// 手动开始录音(用户点击按钮)

11
lib/modules/chat/views/chat_view.dart

@ -118,7 +118,7 @@ class ChatView extends GetView<ChatController> {
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<ChatController> {
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<ChatController> {
Widget _buildInputSection() {
return Obx(() {
if (controller.isVoiceInputVisible.value) {
return VoiceInputPanel(
return Column(
mainAxisSize: MainAxisSize.min,
children: [
VoiceInputPanel(
onRecordingResult: controller.handleVoiceResult,
onClose: controller.stopVoiceInput,
onRecognizing: controller.handleRecognizing,
),
],
);
}
return _buildTextInput();

308
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<MicrosoftTtsContinuousExample> createState() => _MicrosoftTtsContinuousExampleState();
}
class _MicrosoftTtsContinuousExampleState extends State<MicrosoftTtsContinuousExample> {
final MicrosoftTtsService _ttsService = Get.find<MicrosoftTtsService>();
// 语音列表
final List<Map<String, String>> _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<String> _presetTexts = [
'欢迎使用微软语音合成服务,这是连续语音输出的第一段文本。',
'这是第二段文本,用于测试连续语音输出功能。',
'现在是第三段文本,我们正在测试微软语音合成服务的连续合成能力。',
'最后一段测试文本,感谢您的收听。',
];
// 自定义文本列表
final List<TextEditingController> _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<void> _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<void> _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<String>(
value: _selectedVoice,
decoration: const InputDecoration(
labelText: '选择语音',
border: OutlineInputBorder(),
),
items: _voices.map((voice) {
return DropdownMenuItem<String>(
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,
)),
],
),
),
);
}
}

202
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<MicrosoftTtsExample> createState() => _MicrosoftTtsExampleState();
}
class _MicrosoftTtsExampleState extends State<MicrosoftTtsExample> {
final TextEditingController _textController = TextEditingController();
final MicrosoftTtsService _ttsService = Get.find<MicrosoftTtsService>();
// 语音列表
final List<Map<String, String>> _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<void> _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<String>(
value: _selectedVoice,
decoration: const InputDecoration(
labelText: '选择语音',
border: OutlineInputBorder(),
),
items: _voices.map((voice) {
return DropdownMenuItem<String>(
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,
),
],
),
),
);
}
}

25
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<ProfileController> {
const ProfileView({Key? key}) : super(key: key);
@ -65,10 +68,10 @@ class ProfileView extends GetView<ProfileController> {
_buildMenuItem(
title: '测试语音合成',
icon: Icons.record_voice_over,
subtitle: '测试火山语音TTS功能',
subtitle: '测试微软语音TTS功能',
onTap: () async {
try {
final tts = Get.find<VolcanoTtsService>();
final tts = Get.find<MicrosoftTtsService>();
// 检查TTS服务状态
print('TTS服务状态: enabled=${tts.isEnabled.value}');
@ -110,6 +113,24 @@ class ProfileView extends GetView<ProfileController> {
},
),
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,

Loading…
Cancel
Save