184 lines
6.0 KiB
Dart
184 lines
6.0 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_vosk_wakeword/flutter_vosk_wakeword.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<String, dynamic>?)> 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<AIChatCommandState> get commandStateStream {
|
|
if (_commandCubit == null) {
|
|
throw StateError('AIChatAssistant 未初始化');
|
|
}
|
|
return _commandCubit!.stream;
|
|
}
|
|
|
|
// --- Porcupine 唤醒词属性 ---
|
|
|
|
PorcupineManager? _porcupineManager;
|
|
final StreamController<void> _wakeWordController = StreamController<void>.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<void> get onWakeWordDetected => _wakeWordController.stream;
|
|
|
|
/// 当前是否检测到了唤醒词
|
|
bool get isWakeWordDetected => _wakeWordDetected;
|
|
|
|
/// 将 Flutter asset 写入设备文件系统,返回其真实路径
|
|
Future<String> _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<void> startVoskWakeword() async {
|
|
FlutterVoskWakeword.instance.initialize(
|
|
wakeWords: ['你好众众', '你好', '众众', '测试', '哈喽', '唤醒'], // 唤醒词列表
|
|
);
|
|
|
|
debugPrint('Starting Vosk Wakeword detection...');
|
|
FlutterVoskWakeword.instance.start();
|
|
|
|
FlutterVoskWakeword.instance.recognitionStream.listen((event) {
|
|
debugPrint('Vosk Wakeword detected: ${event.text}');
|
|
_wakeWordDetected = true;
|
|
// 通知所有监听者
|
|
_wakeWordController.add(null);
|
|
// 监听到之后就停止,这个时候已经弹出了对话框
|
|
// 对话框消失就可以继续监听 recognitionStream 的订阅不会断开
|
|
FlutterVoskWakeword.instance.stop();
|
|
});
|
|
}
|
|
|
|
Future<void> stopVoskWakeword() async {
|
|
if (_wakeWordDetected) {
|
|
_wakeWordDetected = false;
|
|
}
|
|
if (FlutterVoskWakeword.instance != null) {
|
|
FlutterVoskWakeword.instance.stop();
|
|
}
|
|
}
|
|
|
|
/// 开启或关闭唤醒词检测
|
|
Future<void> 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 停止并释放
|
|
FlutterVoskWakeword.instance.dispose();
|
|
debugPrint('AIChatAssistant 单例销毁');
|
|
}
|
|
}
|