import 'dart:async'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:path_provider/path_provider.dart'; import 'package:porcupine_flutter/porcupine_error.dart'; import 'package:porcupine_flutter/porcupine_manager.dart'; import 'bloc/ai_chat_cubit.dart'; import 'bloc/command_state.dart'; import 'models/vehicle_cmd.dart'; /// 车辆命令处理器抽象类 abstract class VehicleCommandHandler { AIChatCommandCubit? commandCubit; /// 执行车辆控制命令 Future<(bool, Map?)> executeCommand(VehicleCommand command); } class AIChatAssistantManager { static final AIChatAssistantManager _instance = AIChatAssistantManager._internal(); static AIChatAssistantManager get instance => _instance; AIChatAssistantManager._internal() { // 初始化代码 debugPrint('AIChatAssistant 单例创建'); _commandCubit = AIChatCommandCubit(); } AIChatCommandCubit? _commandCubit; VehicleCommandHandler? commandClient; /// 初始化命令处理器 void setupCommandHandle({ required VehicleCommandHandler commandHandler, }) { commandClient = commandHandler; commandClient?.commandCubit = _commandCubit; } /// 获取命令状态流 Stream get commandStateStream { if (_commandCubit == null) { throw StateError('AIChatAssistant 未初始化'); } return _commandCubit!.stream; } // --- Porcupine 唤醒词属性 --- PorcupineManager? _porcupineManager; final StreamController _wakeWordController = StreamController.broadcast(); bool _isWakeWordEnabled = false; bool _wakeWordDetected = false; // PicoVoice AccessKey final String _picoAccessKey = "CRkyWZ3DRFXVogXEhE2eK2/ZN1q9YEYweTnjVWlve86nKAid5i1zsQ=="; // 资产逻辑路径 (用于 rootBundle.load) static const String _keywordAssetPath = "packages/ai_chat_assistant/assets/zhongzhong_zh_android.ppn"; static const String _modelAssetPath = "packages/ai_chat_assistant/assets/porcupine_params_zh.pv"; // 真实文件系统路径 (Porcupine 使用) String? _keywordFilePath; String? _modelFilePath; /// 监听唤醒词事件的 Stream Stream get onWakeWordDetected => _wakeWordController.stream; /// 当前是否检测到了唤醒词 bool get isWakeWordDetected => _wakeWordDetected; /// 将 Flutter asset 写入设备文件系统,返回其真实路径 Future _extractAsset(String assetPath, String fileName) async { final dir = await getApplicationSupportDirectory(); final file = File('${dir.path}/$fileName'); if (!file.existsSync()) { final data = await rootBundle.load(assetPath); await file.writeAsBytes(data.buffer.asUint8List(), flush: true); debugPrint('Asset "$assetPath" extracted to "${file.path}"'); } return file.path; } /// 开启或关闭唤醒词检测 Future setWakeWordDetection(bool enable) async { if (enable == _isWakeWordEnabled && _porcupineManager != null) { return; // 状态未改变 } _isWakeWordEnabled = enable; if (enable) { try { // 确保关键词和模型文件已提取到文件系统 _keywordFilePath ??= await _extractAsset(_keywordAssetPath, "zhongzhong_zh_android.ppn"); _modelFilePath ??= await _extractAsset(_modelAssetPath, "porcupine_params_zh.pv"); _porcupineManager = await PorcupineManager.fromKeywordPaths( _picoAccessKey, [_keywordFilePath!], _wakeWordDetectedCallback, modelPath: _modelFilePath, // 中文模型必须指定 modelPath errorCallback: _porcupineErrorCallback, sensitivities: [0.6], // 可以调整灵敏度 ); await _porcupineManager?.start(); debugPrint("Porcupine manager started for wake-word detection."); } on PorcupineException catch (e) { debugPrint("Failed to init Porcupine: ${e.message}"); _isWakeWordEnabled = false; } } else { await _porcupineManager?.stop(); await _porcupineManager?.delete(); _porcupineManager = null; _wakeWordDetected = false; // 关闭时重置状态 debugPrint("Porcupine manager stopped and deleted."); } } // 唤醒词检测回调 void _wakeWordDetectedCallback(int keywordIndex) { // 我们只用了一个关键词,所以索引总是 0 if (keywordIndex == 0) { _wakeWordDetected = true; debugPrint("Wake-word '众众' detected!"); // 通知所有监听者 _wakeWordController.add(null); // 可选:一段时间后自动重置状态 Future.delayed(const Duration(seconds: 2), () { _wakeWordDetected = false; }); } } void _porcupineErrorCallback(PorcupineException error) { debugPrint("Porcupine error: ${error.message}"); } /// 释放资源 void dispose() { _commandCubit?.close(); _commandCubit = null; _wakeWordController.close(); setWakeWordDetection(false); // 确保 Porcupine 停止并释放 } }