wolfplus 2 years ago
parent
commit
4fdc23ea4e
  1. 13
      lib/core/bindings/initial_binding.dart
  2. 40
      lib/core/controllers/app_controller.dart
  3. 9
      lib/core/routes/app_pages.dart
  4. 46
      lib/core/views/main_view.dart
  5. 47
      lib/core/widgets/common_bottom_nav.dart
  6. 167
      lib/data/services/voice_recognition_service.dart
  7. 384
      lib/data/services/volcano_asr_service.dart
  8. 2
      lib/main.dart
  9. 141
      lib/modules/chat/controllers/chat_controller.dart
  10. 72
      lib/modules/chat/controllers/voice_input_controller.dart
  11. 2
      lib/modules/explore/views/explore_view.dart
  12. 2
      lib/modules/home/views/home_view.dart
  13. 3
      lib/modules/profile/views/profile_view.dart

13
lib/core/bindings/initial_binding.dart

@ -1,13 +1,8 @@
import 'package:get/get.dart';
import '../../core/controllers/app_controller.dart';
import '../../core/controllers/permission_controller.dart';
import '../../modules/home/controllers/home_controller.dart';
import '../../modules/explore/controllers/explore_controller.dart';
import '../../modules/profile/controllers/profile_controller.dart';
import '../../data/services/volcano_tts_service.dart';
import '../../data/services/audio_service.dart';
import '../../data/services/notification_service.dart';
import '../../data/services/voice_recognition_service.dart';
import '../../data/services/volcano_ai_service.dart';
/// 初始绑定,用于管理全局依赖
@ -16,22 +11,16 @@ class InitialBinding extends Bindings {
void dependencies() {
// 初始化所有服务
final notificationService = Get.put(NotificationService(), permanent: true);
final voiceRecognitionService = Get.put(VoiceRecognitionService(), permanent: true);
final volcanoTtsService = Get.put(VolcanoTtsService(), permanent: true);
final audioService = Get.put(AudioServiceManager(), permanent: true);
final volcanoAiService = Get.put(VolcanoAIService(), permanent: true);
// 注册 Controllers
Get.put(AppController(), permanent: true);
Get.put(HomeController(), permanent: true);
Get.put(ExploreController(), permanent: true);
Get.put(ProfileController(), permanent: true);
// 只注册全局控制器
Get.put(PermissionController(), permanent: true);
// 异步初始化
Future.wait([
notificationService.init(),
voiceRecognitionService.onInit(),
// 如果其他服务也需要异步初始化,在这里添加
]);
}

40
lib/core/controllers/app_controller.dart

@ -1,40 +0,0 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
class AppController extends GetxController {
static AppController get to => Get.find();
final _isDarkMode = false.obs;
final _currentIndex = 0.obs;
final _storage = GetStorage();
bool get isDarkMode => _isDarkMode.value;
int get currentIndex => _currentIndex.value;
@override
void onInit() {
super.onInit();
_loadThemeMode();
}
void _loadThemeMode() {
_isDarkMode.value = _storage.read('isDarkMode') ?? false;
}
Future<void> toggleThemeMode() async {
_isDarkMode.value = !_isDarkMode.value;
Get.changeThemeMode(_isDarkMode.value ? ThemeMode.dark : ThemeMode.light);
await _storage.write('isDarkMode', _isDarkMode.value);
}
void changeIndex(int index) {
_currentIndex.value = index;
}
@override
void onClose() {
// 在这里清理全局资源
super.onClose();
}
}

9
lib/core/routes/app_pages.dart

@ -1,22 +1,16 @@
import 'package:get/get.dart';
import '../bindings/initial_binding.dart';
import '../views/main_view.dart';
import '../../modules/home/views/home_view.dart';
import '../../modules/home/bindings/home_binding.dart';
import '../../modules/explore/views/explore_view.dart';
import '../../modules/explore/bindings/explore_binding.dart';
import '../../modules/profile/views/profile_view.dart';
import '../../modules/profile/bindings/profile_binding.dart';
import '../../modules/chat/bindings/chat_binding.dart';
import '../../modules/chat/views/chat_view.dart';
import './app_routes.dart';
abstract class AppPages {
static final pages = [
GetPage(
name: Routes.initial,
page: () => const MainView(),
binding: InitialBinding(),
),
GetPage(
name: Routes.home,
page: () => const HomeView(),
@ -35,6 +29,7 @@ abstract class AppPages {
GetPage(
name: Routes.profile,
page: () => const ProfileView(),
binding: ProfileBinding(),
),
];
}

46
lib/core/views/main_view.dart

@ -1,46 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/app_controller.dart';
import '../../modules/home/views/home_view.dart';
import '../../modules/explore/views/explore_view.dart';
import '../../modules/profile/views/profile_view.dart';
class MainView extends GetView<AppController> {
const MainView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Obx(() {
switch (controller.currentIndex) {
case 0:
return const HomeView();
case 1:
return const ExploreView();
case 2:
return const ProfileView();
default:
return const HomeView();
}
}),
bottomNavigationBar: Obx(() => BottomNavigationBar(
currentIndex: controller.currentIndex,
onTap: controller.changeIndex,
items: [
BottomNavigationBarItem(
icon: const Icon(Icons.home),
label: 'home'.tr,
),
BottomNavigationBarItem(
icon: const Icon(Icons.explore),
label: 'explore'.tr,
),
BottomNavigationBarItem(
icon: const Icon(Icons.person),
label: 'profile'.tr,
),
],
)),
);
}
}

47
lib/core/widgets/common_bottom_nav.dart

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../routes/app_routes.dart';
class CommonBottomNav extends StatelessWidget {
final int currentIndex;
const CommonBottomNav({
Key? key,
required this.currentIndex,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: currentIndex,
onTap: (index) {
// Navigate to the appropriate page based on index
switch (index) {
case 0:
Get.offAllNamed(Routes.home);
break;
case 1:
Get.offAllNamed(Routes.explore);
break;
case 2:
Get.offAllNamed(Routes.profile);
break;
}
},
items: [
BottomNavigationBarItem(
icon: const Icon(Icons.home),
label: 'home'.tr,
),
BottomNavigationBarItem(
icon: const Icon(Icons.explore),
label: 'explore'.tr,
),
BottomNavigationBarItem(
icon: const Icon(Icons.person),
label: 'profile'.tr,
),
],
);
}
}

167
lib/data/services/voice_recognition_service.dart

@ -1,167 +0,0 @@
// voice_recognition_service.dart
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter/foundation.dart';
import 'volcano_asr_service.dart';
import 'package:get/get.dart';
class VoiceRecognitionService extends GetxService {
VolcanoAsrService? _asrService;
bool _isInitialized = false;
bool _isStoppingRecognition = false;
final isRecognizing = false.obs;
Function(String)? onRecognitionResult;
Function(String)? onError;
Function()? onRecognitionComplete;
Function(String)? onSentenceComplete;
Function(String)? onConnectionStatusChanged;
VoiceRecognitionService();
void _logError(String message, [dynamic error, StackTrace? stackTrace]) {
debugPrint('🔴 VoiceRecognitionService Error: $message');
if (error != null) debugPrint('Error details: $error');
if (stackTrace != null) debugPrint('Stack trace: $stackTrace');
}
@override
Future<void> onInit() async {
super.onInit();
if (!_isInitialized) {
try {
await _initializeServices();
_isInitialized = true;
debugPrint('✅ VoiceRecognitionService initialized successfully');
} catch (e, stackTrace) {
_logError('Failed to initialize services', e, stackTrace);
rethrow;
}
}
}
Future<void> _initializeServices() async {
try {
final appKey = dotenv.env['VOLCANO_APP_KEY'];
final accessKey = dotenv.env['VOLCANO_ACCESS_KEY'];
if (appKey == null || accessKey == null) {
throw Exception('Volcano ASR service configuration is missing. Please check your .env file.');
}
_asrService = VolcanoAsrService(
appKey: appKey,
accessKey: accessKey,
onResult: _handleRecognitionResult,
onError: _handleError,
onConnectionStatusChanged: _handleConnectionStatusChanged,
);
await _setupServices();
} catch (e, stackTrace) {
_logError('Error during service initialization', e, stackTrace);
rethrow;
}
}
Future<void> _setupServices() async {
try {
_asrService?.onResult = (result) {
debugPrint('ASR Result: $result');
onRecognitionResult?.call(result);
onSentenceComplete?.call(result);
};
_asrService?.onError = (error) {
_logError('ASR service error', error);
onError?.call('ASR error: $error');
};
_asrService?.onConnectionStatusChanged = (status) {
final isConnected = status == 'connected';
debugPrint('ASR Connection status: $status');
if (!isConnected && isRecognizing.value && !_isStoppingRecognition) {
stopRecognition();
onRecognitionComplete?.call();
}
onConnectionStatusChanged?.call(status);
};
} catch (e, stackTrace) {
_logError('Error during services setup', e, stackTrace);
rethrow;
}
}
Future<void> startRecognition() async {
if (isRecognizing.value || _asrService == null) {
debugPrint('⚠️ Cannot start recognition: ${isRecognizing.value ? 'Already recognizing' : 'ASR service not initialized'}');
return;
}
try {
_isStoppingRecognition = false;
debugPrint('Starting voice recognition...');
await _asrService?.startRecognition();
isRecognizing.value = true;
} catch (e, stackTrace) {
_logError('Failed to start recognition', e, stackTrace);
onError?.call('Failed to start recognition: $e');
await stopRecognition();
}
}
Future<void> stopRecognition() async {
if (!isRecognizing.value || _asrService == null || _isStoppingRecognition) return;
try {
_isStoppingRecognition = true;
debugPrint('Stopping voice recognition...');
isRecognizing.value = false;
await _asrService?.stopRecognition().timeout(
const Duration(seconds: 3),
onTimeout: () {
debugPrint('ASR service stop timeout, forcing cleanup');
_cleanupResources();
},
);
final complete = onRecognitionComplete;
if (complete != null) {
Future.microtask(complete);
}
} catch (e, stackTrace) {
_logError('Failed to stop recognition', e, stackTrace);
onError?.call('Failed to stop recognition: $e');
_cleanupResources();
} finally {
_isStoppingRecognition = false;
}
}
void _cleanupResources() {
if (_asrService != null) {
_asrService!.onResult = null;
_asrService!.onError = null;
_asrService!.onConnectionStatusChanged = null;
}
isRecognizing.value = false;
_isStoppingRecognition = false;
}
void _handleRecognitionResult(String result) {
onRecognitionResult?.call(result);
}
void _handleError(String error) {
onError?.call(error);
}
void _handleConnectionStatusChanged(String status) {
onConnectionStatusChanged?.call(status);
}
@override
void onClose() {
debugPrint('Cleaning up VoiceRecognitionService...');
stopRecognition().then((_) {
debugPrint('VoiceRecognitionService cleanup completed');
super.onClose();
}).catchError((e) {
debugPrint('Error during VoiceRecognitionService cleanup: $e');
super.onClose();
});
}
}

384
lib/data/services/volcano_asr_service.dart

@ -1,384 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:uuid/uuid.dart';
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
import 'package:record/record.dart';
/// 自定义异常类
class AsrServiceException implements Exception {
final String message;
AsrServiceException(this.message);
@override
String toString() => message;
}
/// Volcano ASR 服务,负责音频采集、数据打包、WebSocket 连接和数据传输
class VolcanoAsrService extends GetxService {
// 服务地址
static const String _baseUrl = 'wss://openspeech.bytedance.com/api/v3/sauc/bigmodel';
// 配置参数
final String appKey;
final String accessKey;
// WebSocket 与录音相关
WebSocket? _webSocket;
AudioRecorder? _audioRecorder;
StreamSubscription<List<int>>? _audioStreamSubscription;
// 状态管理
bool _isConnected = false;
bool _isRecording = false;
int _currentSequence = 1;
int _serverSequence = 0; // 添加服务器序列号跟踪
bool _isInitialized = false;
bool _waitingForInitResponse = false;
Completer<void>? _initCompleter;
// 回调
Function(String)? onResult;
Function(String)? onError;
Function(String)? onConnectionStatusChanged;
VolcanoAsrService({
required this.appKey,
required this.accessKey,
this.onResult,
this.onError,
this.onConnectionStatusChanged,
});
Future<void> _connectWebSocket() async {
if (_webSocket != null) {
debugPrint('⚠️ ASR: WebSocket already exists');
return;
}
final connectId = const Uuid().v4();
final headers = {
'X-Api-App-Key': appKey,
'X-Api-Access-Key': accessKey,
'X-Api-Resource-Id': 'volc.bigasr.sauc.duration',
'X-Api-Connect-Id': connectId,
'Connection': 'Upgrade', // 添加 WebSocket 升级头
'Upgrade': 'websocket',
};
try {
_webSocket = await WebSocket.connect(
_baseUrl,
headers: headers,
protocols: ['binary'], // 指定二进制协议
);
debugPrint('ℹ️ ASR: WebSocket connected');
_webSocket!.listen(
_handleWebSocketMessage,
onError: (error) {
debugPrint('🔴 ASR: WebSocket error: $error');
onError?.call('WebSocket error: $error');
_handleDisconnect();
},
onDone: () {
debugPrint('ℹ️ ASR: WebSocket closed');
if (_waitingForInitResponse) {
_initCompleter?.completeError('WebSocket closed during initialization');
_initCompleter = null;
}
_handleDisconnect();
},
cancelOnError: false, // 不要在错误时自动取消
);
_isConnected = true;
_currentSequence = 1; // 重置序列号
_isInitialized = false; // 重置初始化状态
onConnectionStatusChanged?.call('connected');
// 发送初始化请求前等待一下
await Future.delayed(const Duration(milliseconds: 100));
await _sendInitRequest(connectId);
} catch (e) {
debugPrint('🔴 ASR: Failed to connect: $e');
rethrow;
}
}
Future<void> _sendInitRequest(String requestId) async {
_waitingForInitResponse = true;
_initCompleter = Completer<void>();
final initRequest = {
'audio': {
'format': 'raw',
'sample_rate': 16000,
'channels': 1,
'encoding': 'pcm',
'bit_depth': 16,
},
'request': {
'request_id': requestId,
'language': 'zh',
'model_name': 'bigmodel',
'enable_punc': true,
'enable_itn': true,
'enable_voice_detection': true,
}
};
final jsonBytes = utf8.encode(json.encode(initRequest));
debugPrint('ℹ️ ASR: Sending init request: ${json.encode(initRequest)}');
final header = Uint8List(4)
..[0] = 0x11 // version=1, header_size=1
..[1] = 0x11 // message_type=1(FULL_CLIENT_REQUEST), flags=1(POS_SEQUENCE)
..[2] = 0x10 // serialization=1(JSON), compression=0
..[3] = 0x00; // reserved
final seqBytes = ByteData(4)..setInt32(0, _currentSequence, Endian.big);
final sizeBytes = ByteData(4)..setInt32(0, jsonBytes.length, Endian.big);
final fullRequest = Uint8List(header.length + 8 + jsonBytes.length)
..setAll(0, header)
..setAll(4, seqBytes.buffer.asUint8List())
..setAll(8, sizeBytes.buffer.asUint8List())
..setAll(12, jsonBytes);
_webSocket?.add(fullRequest);
debugPrint('ℹ️ ASR: Init request sent with sequence: $_currentSequence');
// 设置超时
Future.delayed(const Duration(seconds: 10), () {
if (_waitingForInitResponse) {
debugPrint('⚠️ ASR: Init request timeout after 10 seconds');
_waitingForInitResponse = false;
_initCompleter?.completeError('Init request timeout');
_initCompleter = null;
}
});
try {
await _initCompleter?.future;
} catch (e) {
debugPrint('🔴 ASR: Init request failed: $e');
rethrow;
}
}
void _handleWebSocketMessage(dynamic message) {
debugPrint('ℹ️ ASR: Received message type: ${message.runtimeType}');
if (message is String) {
debugPrint('ℹ️ ASR: Received string message: $message');
try {
final data = json.decode(message);
if (data is Map) {
if (data['error'] != null) {
debugPrint('🔴 ASR: Server error: ${data['error']}');
if (_waitingForInitResponse) {
_waitingForInitResponse = false;
_initCompleter?.completeError(data['error']);
_initCompleter = null;
}
onError?.call(data['error']);
return;
}
}
} catch (e) {
debugPrint('⚠️ ASR: Failed to parse string message: $e');
}
}
if (message is! Uint8List || message.length < 12) {
debugPrint('⚠️ ASR: Invalid binary message format');
return;
}
final version = message[0] >> 4;
final headerSize = message[0] & 0x0F;
final messageType = message[1] >> 4;
final flags = message[1] & 0x0F;
final serialization = message[2] >> 4;
final compression = message[2] & 0x0F;
debugPrint('ℹ️ ASR: Binary message: version=$version, headerSize=$headerSize, type=$messageType, flags=$flags, serialization=$serialization, compression=$compression');
final sequence = ByteData.view(message.buffer, 4, 4).getInt32(0, Endian.big);
debugPrint('ℹ️ ASR: Message sequence: $sequence');
_serverSequence = sequence;
final payloadSize = ByteData.view(message.buffer, 8, 4).getInt32(0, Endian.big);
if (message.length < 12 + payloadSize) {
debugPrint('⚠️ ASR: Incomplete message received');
return;
}
if (messageType == 0x0F) { // INIT_RESPONSE
if (_waitingForInitResponse) {
debugPrint('ℹ️ ASR: Received init response (type 15)');
if (payloadSize > 0) {
final payload = message.sublist(12, 12 + payloadSize);
final jsonStr = utf8.decode(payload);
debugPrint('ℹ️ ASR: Init response payload: $jsonStr');
try {
final response = json.decode(jsonStr);
if (response is Map) {
if (response['error'] != null) {
final error = response['error'];
debugPrint('🔴 ASR: Init response error: $error');
_waitingForInitResponse = false;
_initCompleter?.completeError(error);
_initCompleter = null;
return;
}
debugPrint('ℹ️ ASR: Init response success');
_waitingForInitResponse = false;
_currentSequence = (_serverSequence + 1);
_isInitialized = true;
_initCompleter?.complete();
_initCompleter = null;
return;
}
} catch (e) {
debugPrint('⚠️ ASR: Failed to parse init response: $e');
}
} else {
debugPrint('ℹ️ ASR: Empty init response success');
_waitingForInitResponse = false;
_currentSequence = (_serverSequence + 1);
_isInitialized = true;
_initCompleter?.complete();
_initCompleter = null;
return;
}
}
} else if (messageType == 0x09) { // FULL_SERVER_RESPONSE
final payload = message.sublist(12, 12 + payloadSize);
final jsonStr = utf8.decode(payload);
debugPrint('ℹ️ ASR: Received binary payload: $jsonStr');
try {
final response = json.decode(jsonStr);
if (response is Map && response['result'] != null) {
final result = response['result'];
if (result['text'] != null && result['text'].isNotEmpty) {
debugPrint('ℹ️ ASR: Result: ${result['text']}');
onResult?.call(result['text']);
}
}
} catch (e) {
debugPrint('⚠️ ASR: Failed to parse binary payload: $e');
}
}
}
Future<void> _startRecording() async {
_audioRecorder ??= AudioRecorder();
try {
if (!await _audioRecorder!.hasPermission()) {
throw Exception('Microphone permission denied');
}
const config = RecordConfig(
encoder: AudioEncoder.pcm16bits,
bitRate: 256000,
sampleRate: 16000,
numChannels: 1,
);
// 启动录音流
final stream = await _audioRecorder!.startStream(config);
_isRecording = true;
// 监听音频流
_audioStreamSubscription = stream.listen(
(data) {
if (_isRecording && _isConnected) {
_sendAudioData(data, false);
}
},
onError: (error) {
debugPrint('🔴 ASR: Stream error: $error');
_stopRecording();
},
);
} catch (e) {
debugPrint('🔴 ASR: Recording error: $e');
await _stopRecording();
rethrow;
}
}
void _sendAudioData(List<int> audioData, bool isLast) {
if (!_isConnected || _webSocket == null || !_isInitialized) {
debugPrint('⚠️ ASR: Cannot send audio data: not ready');
return;
}
final header = Uint8List(4)
..[0] = 0x11 // version=1, header_size=1
..[1] = isLast ? 0x23 : 0x21 // message_type=2(AUDIO_ONLY), flags=3/1
..[2] = 0x00 // no serialization, no compression
..[3] = 0x00; // reserved
final seqBytes = ByteData(4)..setInt32(0, _currentSequence, Endian.big);
final sizeBytes = ByteData(4)..setInt32(0, audioData.length, Endian.big);
final packet = Uint8List(12 + audioData.length)
..setAll(0, header)
..setAll(4, seqBytes.buffer.asUint8List())
..setAll(8, sizeBytes.buffer.asUint8List())
..setAll(12, audioData);
_webSocket?.add(packet);
debugPrint('🔍 ASR: Sent audio data with sequence: $_currentSequence');
_currentSequence++; // 递增序列号
}
void _handleDisconnect() {
_webSocket = null;
_isConnected = false;
_isInitialized = false; // 重置初始化状态
_currentSequence = 1; // 重置序列号
onConnectionStatusChanged?.call('disconnected');
_stopRecording();
}
Future<void> _stopRecording() async {
_isRecording = false;
try {
await _audioStreamSubscription?.cancel();
_audioStreamSubscription = null;
await _audioRecorder?.stop();
_audioRecorder = null;
} catch (e) {
debugPrint('⚠️ ASR: Error stopping recording: $e');
}
}
Future<void> startRecognition() async {
await _connectWebSocket();
await _startRecording();
}
Future<void> stopRecognition() async {
await _stopRecording();
_webSocket?.close();
_handleDisconnect();
}
@override
void onClose() {
_audioStreamSubscription?.cancel();
stopRecognition();
super.onClose();
}
}

2
lib/main.dart

@ -61,7 +61,7 @@ class MainApp extends StatelessWidget {
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialBinding: InitialBinding(),
initialRoute: Routes.initial,
initialRoute: Routes.home,
getPages: AppPages.pages,
debugShowCheckedModeBanner: false,
);

141
lib/modules/chat/controllers/chat_controller.dart

@ -5,7 +5,6 @@ import 'dart:convert';
import '../../../data/services/volcano_ai_service.dart';
import '../../../data/services/volcano_tts_service.dart';
import '../models/message_model.dart';
import '../../../data/services/voice_recognition_service.dart';
import '../controllers/voice_input_controller.dart';
import 'package:flutter/services.dart';
@ -20,7 +19,6 @@ class ChatController extends GetxController {
final VolcanoAIService _aiService = VolcanoAIService();
late final VolcanoTtsService _ttsService;
late final VoiceRecognitionService _recognitionService;
static const String _storagePrefix = 'chat_history_';
final messages = <Message>[].obs;
@ -50,7 +48,6 @@ class ChatController extends GetxController {
messageController = TextEditingController();
scrollController = ScrollController();
_initTtsService();
_initVoiceRecognitionService();
_initFromArguments();
_initializeController();
@ -99,118 +96,6 @@ class ChatController extends GetxController {
_ttsService.isEnabled.value = true;
}
void _initVoiceRecognitionService() {
_recognitionService = Get.find<VoiceRecognitionService>();
_setupVoiceRecognitionCallbacks();
}
void _setupVoiceRecognitionCallbacks() {
_recognitionService.onRecognitionResult = (result) {
if (isVoiceMuted.value) return;
recordingText.value = result;
handleRecognizing(result);
};
_recognitionService.onError = (error) {
// 不要立即显示错误,先尝试重新连接
_handleRecognitionError(error);
};
_recognitionService.onRecognitionComplete = () {
stopRecording();
if (!isVoiceMuted.value && recordingText.value.isNotEmpty) {
handleVoiceResult(recordingText.value);
}
};
_recognitionService.onConnectionStatusChanged = (status) {
if (status == 'disconnected' && isRecording.value) {
// 如果在录音过程中断开连接,尝试重新连接
_handleWebSocketDisconnection();
}
};
// 添加句子完成的回调
_recognitionService.onSentenceComplete = (text) {
if (!isVoiceMuted.value && text.isNotEmpty) {
handleVoiceResult(text);
// 继续录音,等待下一句话
if (isVoiceInputVisible.value) {
startRecording();
}
}
};
}
Future<void> _handleRecognitionError(String error) async {
// 如果是权限相关错误,请求权限
if (error.toLowerCase().contains('permission')) {
try {
const platform = MethodChannel('com.example.deep_voice/audio');
await platform.invokeMethod('requestAudioPermissions');
// 权限请求后重试
if (isVoiceInputVisible.value) {
await Future.delayed(const Duration(seconds: 1));
startRecording();
}
return;
} catch (e) {
debugPrint('Failed to request permissions: $e');
}
}
// 如果是连接错误,尝试重新连接
if (error.toLowerCase().contains('connection') || error.toLowerCase().contains('websocket')) {
_handleWebSocketDisconnection();
return;
}
// 其他错误显示给用户
Get.snackbar(
'Error',
'语音识别失败: $error',
snackPosition: SnackPosition.BOTTOM,
);
stopVoiceInput();
}
Future<void> _handleWebSocketDisconnection() async {
if (!isVoiceInputVisible.value) return;
isVoiceConnecting.value = true;
stopRecording();
// 保存当前的语音文本
final currentText = recordingText.value;
try {
// 等待一段时间后重试
await Future.delayed(const Duration(seconds: 2));
if (!isVoiceInputVisible.value) return;
// 重新初始化语音识别
await _recognitionService.onInit();
// 恢复之前的状态
if (currentText.isNotEmpty) {
recordingText.value = currentText;
}
isVoiceConnecting.value = false;
if (isVoiceInputVisible.value) {
startRecording();
}
} catch (e) {
debugPrint('Failed to reconnect: $e');
Get.snackbar(
'Error',
'重新连接失败,请稍后再试',
snackPosition: SnackPosition.BOTTOM,
);
stopVoiceInput();
}
}
Future<void> _initializeController() async {
try {
await _loadChatHistory();
@ -584,40 +469,17 @@ class ChatController extends GetxController {
onClosePanel: stopVoiceInput,
onRecognizing: handleRecognizing,
));
// 初始化语音识别
_initializeVoiceRecognition();
}
Future<void> _initializeVoiceRecognition() async {
try {
await _recognitionService.onInit();
isVoiceConnecting.value = false;
if (isVoiceInputVisible.value) {
startRecording();
}
} catch (e) {
debugPrint('Failed to initialize voice recognition: $e');
Get.snackbar(
'Error',
'初始化语音识别失败: $e',
snackPosition: SnackPosition.BOTTOM,
);
stopVoiceInput();
}
}
void startRecording() {
if (!isVoiceConnecting.value && isVoiceInputVisible.value) {
isRecording.value = true;
_recognitionService.startRecognition();
}
}
void stopRecording() {
if (isRecording.value) {
isRecording.value = false;
_recognitionService.stopRecognition();
}
}
@ -631,9 +493,6 @@ class ChatController extends GetxController {
isRecording.value = false;
recordingText.value = '';
// 停止语音识别服务
_recognitionService.stopRecognition();
// 如果存在临时语音消息但没有实际内容,则移除它
if (_currentVoiceMessage != null && _currentVoiceMessage!.content == '🎤 ...') {
messages.remove(_currentVoiceMessage);

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

@ -1,10 +1,7 @@
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
import '../../../data/services/voice_recognition_service.dart';
class VoiceInputController extends GetxController {
final VoiceRecognitionService _recognitionService;
// Observable states
final isConnecting = true.obs;
final isMuted = false.obs;
@ -20,59 +17,17 @@ class VoiceInputController extends GetxController {
required this.onRecordingResult,
required this.onClosePanel,
required this.onRecognizing,
}) : _recognitionService = Get.find<VoiceRecognitionService>();
});
@override
void onInit() {
super.onInit();
_setupRecognitionCallbacks();
_initializeRecognition();
}
void _setupRecognitionCallbacks() {
_recognitionService.onRecognitionResult = (result) {
if (isMuted.value) return;
recognizedText.value = result;
onRecognizing(result);
};
_recognitionService.onError = (error) {
Get.snackbar(
'Error',
'语音识别失败: $error',
snackPosition: SnackPosition.BOTTOM,
);
stopRecording();
};
_recognitionService.onRecognitionComplete = () {
stopRecording();
if (!isMuted.value && recognizedText.value.isNotEmpty) {
onRecordingResult(recognizedText.value);
recognizedText.value = '';
startRecording();
}
};
}
Future<void> _initializeRecognition() async {
try {
isConnecting.value = true;
await _recognitionService.startRecognition();
isConnecting.value = false;
} catch (e) {
Get.snackbar(
'Error',
'初始化语音识别失败: $e',
snackPosition: SnackPosition.BOTTOM,
);
onClosePanel();
}
// 初始化完成后更新状态
isConnecting.value = false;
}
Future<void> startRecording() async {
try {
await _recognitionService.startRecognition();
isRecording.value = true;
recognizedText.value = '';
} catch (e) {
@ -86,14 +41,7 @@ class VoiceInputController extends GetxController {
Future<void> stopRecording() async {
if (!isRecording.value) return;
try {
await _recognitionService.stopRecognition();
} catch (e) {
debugPrint('Error stopping recording: $e');
} finally {
isRecording.value = false;
}
isRecording.value = false;
}
void toggleMute() {
@ -105,24 +53,12 @@ class VoiceInputController extends GetxController {
// 停止录音
stopRecording();
// 清理回调
_recognitionService.onRecognitionResult = null;
_recognitionService.onError = null;
_recognitionService.onRecognitionComplete = null;
// 重置状态
isConnecting.value = false;
isMuted.value = false;
isRecording.value = false;
recognizedText.value = '';
// 确保语音识别服务停止
try {
_recognitionService.stopRecognition();
} catch (e) {
debugPrint('Error stopping recognition service: $e');
}
super.onClose();
}
}

2
lib/modules/explore/views/explore_view.dart

@ -5,6 +5,7 @@ import '../controllers/explore_controller.dart';
import '../../chat/controllers/chat_controller.dart';
import '../../chat/views/chat_view.dart';
import '../../../data/providers/agent_provider.dart';
import '../../../core/widgets/common_bottom_nav.dart';
class ExploreView extends GetView<ExploreController> {
const ExploreView({Key? key}) : super(key: key);
@ -152,6 +153,7 @@ class ExploreView extends GetView<ExploreController> {
),
itemBuilder: (context, index) => _buildChatItem(controller.chatList[index]),
)),
bottomNavigationBar: const CommonBottomNav(currentIndex: 1),
);
}
}

2
lib/modules/home/views/home_view.dart

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/home_controller.dart';
import '../../../core/widgets/common_bottom_nav.dart';
class HomeView extends GetView<HomeController> {
const HomeView({Key? key}) : super(key: key);
@ -275,6 +276,7 @@ class HomeView extends GetView<HomeController> {
),
),
),
bottomNavigationBar: const CommonBottomNav(currentIndex: 0),
);
}
}

3
lib/modules/profile/views/profile_view.dart

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../core/controllers/app_controller.dart';
import '../controllers/profile_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../data/services/volcano_tts_service.dart';
import '../../../core/widgets/common_bottom_nav.dart';
class ProfileView extends GetView<ProfileController> {
const ProfileView({Key? key}) : super(key: key);
@ -134,6 +134,7 @@ class ProfileView extends GetView<ProfileController> {
),
],
),
bottomNavigationBar: const CommonBottomNav(currentIndex: 2),
);
}
}
Loading…
Cancel
Save