diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index a2f47b6..d052df5 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,2 +1,4 @@
+
+
diff --git a/assets/porcupine_params_zh.pv b/assets/porcupine_params_zh.pv
new file mode 100644
index 0000000..c20d6cd
Binary files /dev/null and b/assets/porcupine_params_zh.pv differ
diff --git a/assets/zhongzhong_zh_android.ppn b/assets/zhongzhong_zh_android.ppn
new file mode 100644
index 0000000..70e69a6
Binary files /dev/null and b/assets/zhongzhong_zh_android.ppn differ
diff --git a/example/lib/main.dart b/example/lib/main.dart
index ad9b26c..a8af8e7 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -3,6 +3,8 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:ai_chat_assistant/ai_chat_assistant.dart';
+import 'pages/home.dart';
+
class VehicleCommandClent extends VehicleCommandHandler {
@override
AIChatCommandCubit? get commandCubit => super.commandCubit;
@@ -13,7 +15,6 @@ class VehicleCommandClent extends VehicleCommandHandler {
print('执行车控命令: ${command.type}, 参数: ${command.params}');
if (commandCubit != null) {
-
commandCubit?.emit(AIChatCommandState(
commandId: command.commandId,
commandType: command.type,
@@ -64,6 +65,8 @@ void main() async {
AIChatAssistantManager.instance.setupCommandHandle(commandHandler: VehicleCommandClent());
runApp(const MyApp());
+
+ AIChatAssistantManager.instance.setWakeWordDetection(true);
}
class MyApp extends StatelessWidget {
@@ -77,18 +80,7 @@ class MyApp extends StatelessWidget {
primarySwatch: Colors.blue,
useMaterial3: true,
),
- home: const ExampleHomePage(),
- );
- }
-}
-
-class ExampleHomePage extends StatelessWidget {
- const ExampleHomePage({super.key});
-
- @override
- Widget build(BuildContext context) {
- return const Scaffold(
- body: ChatAssistantApp(),
+ home: HomePage(),
);
}
}
diff --git a/example/lib/pages/home.dart b/example/lib/pages/home.dart
new file mode 100644
index 0000000..d126557
--- /dev/null
+++ b/example/lib/pages/home.dart
@@ -0,0 +1,62 @@
+import 'package:flutter/material.dart';
+import 'package:ai_chat_assistant/ai_chat_assistant.dart';
+
+import 'setting.dart';
+
+class ChatAssistantPage extends StatelessWidget {
+ const ChatAssistantPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: ChatAssistantApp(),
+ );
+ }
+}
+
+class HomePage extends StatefulWidget {
+ HomePage({super.key});
+
+ final items = [
+ BottomNavigationBarItem(
+ icon: Icon(Icons.home),
+ label: 'Home',
+ ),
+ BottomNavigationBarItem(
+ icon: Icon(Icons.settings),
+ label: 'Settings',
+ ),
+ ];
+
+ @override
+ State createState() => _HomePageState();
+}
+
+class _HomePageState extends State {
+ int _selectedIndex = 0;
+
+ final List _pages = [
+ ChatAssistantPage(),
+ SettingPage(),
+ ];
+
+ void _onItemTapped(int index) {
+ setState(() {
+ _selectedIndex = index;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: _pages.elementAt(_selectedIndex),
+ bottomNavigationBar: BottomNavigationBar(
+ items: widget.items,
+ selectedItemColor: Colors.blue,
+ unselectedItemColor: Colors.grey,
+ currentIndex: _selectedIndex,
+ onTap: _onItemTapped,
+ ),
+ );
+ }
+}
diff --git a/example/lib/pages/setting.dart b/example/lib/pages/setting.dart
new file mode 100644
index 0000000..32c3ea7
--- /dev/null
+++ b/example/lib/pages/setting.dart
@@ -0,0 +1,29 @@
+import 'package:flutter/material.dart';
+
+class SettingPage extends StatefulWidget {
+ const SettingPage({super.key});
+
+ @override
+ State createState() => _SettingPageState();
+}
+
+class _SettingPageState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('设置'),
+ ),
+ body: const Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text('设置页面'),
+ SizedBox(height: 20),
+ Text('这里可以添加更多设置选项'),
+ ],
+ ),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 9340850..892b6ed 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -201,6 +201,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.2.3"
+ flutter_voice_processor:
+ dependency: transitive
+ description:
+ name: flutter_voice_processor
+ sha256: "8ae3fc196d6060a13392e4ca557f7a2f4a6b87898ae519787f6929f986538572"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.1.2"
flutter_web_plugins:
dependency: transitive
description: flutter
@@ -430,6 +438,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
+ porcupine_flutter:
+ dependency: transitive
+ description:
+ name: porcupine_flutter
+ sha256: "5618b20a00c44aab80763a70cab993037dd73d924dd2a24154c36dc13bdf5c8e"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "3.0.5"
provider:
dependency: transitive
description:
diff --git a/lib/manager.dart b/lib/manager.dart
index e913c61..cda3f8a 100644
--- a/lib/manager.dart
+++ b/lib/manager.dart
@@ -1,5 +1,11 @@
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';
@@ -44,9 +50,103 @@ class AIChatAssistantManager {
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 停止并释放
}
}
diff --git a/lib/widgets/chat_popup.dart b/lib/widgets/chat_popup.dart
index e0f89e8..bfd6082 100644
--- a/lib/widgets/chat_popup.dart
+++ b/lib/widgets/chat_popup.dart
@@ -1,4 +1,6 @@
+import 'dart:async';
import 'dart:ui';
+import 'package:ai_chat_assistant/manager.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../pages/full_screen.dart';
@@ -30,6 +32,7 @@ class _ChatPopupState extends State with SingleTickerProviderStateMix
final double _iconSizeDefault = 80.0;
bool _isShowingPopup = false;
late AnimationController _partScreenAnimationController;
+ StreamSubscription? _wakeWordSubscription;
@override
void initState() {
@@ -38,11 +41,21 @@ class _ChatPopupState extends State with SingleTickerProviderStateMix
duration: const Duration(milliseconds: 250),
vsync: this,
);
+
+ // 订阅唤醒词事件
+ _wakeWordSubscription = AIChatAssistantManager.instance.onWakeWordDetected.listen((_) async {
+ // 在这里处理唤醒事件,例如显示聊天窗口
+ debugPrint("唤醒词被检测到,执行UI操作!");
+ final messageService = MessageService.instance;
+ _insertOverlay();
+ await messageService.startVoiceInput();
+ });
}
@override
void dispose() {
_partScreenAnimationController.dispose();
+ _wakeWordSubscription?.cancel();
super.dispose();
}
diff --git a/pubspec.lock b/pubspec.lock
index 204a954..8da4e02 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -157,6 +157,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.2.3"
+ flutter_voice_processor:
+ dependency: transitive
+ description:
+ name: flutter_voice_processor
+ sha256: "8ae3fc196d6060a13392e4ca557f7a2f4a6b87898ae519787f6929f986538572"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.1.2"
flutter_web_plugins:
dependency: transitive
description: flutter
@@ -354,6 +362,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
+ porcupine_flutter:
+ dependency: "direct main"
+ description:
+ name: porcupine_flutter
+ sha256: "5618b20a00c44aab80763a70cab993037dd73d924dd2a24154c36dc13bdf5c8e"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "3.0.5"
provider:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index ad238e2..2d78a38 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -19,6 +19,8 @@ dependencies:
permission_handler: ^12.0.0
provider: ^6.1.5
flutter_tts: ^4.2.0
+ # 语音唤醒
+ porcupine_flutter: ^3.0.5
# basic_intl: 0.2.0
t_basic_intl:
path: packages/basic_intl
@@ -33,6 +35,8 @@ flutter:
uses-material-design: true
assets:
- assets/images/
+ - assets/zhongzhong_zh_android.ppn
+ - assets/porcupine_params_zh.pv
plugin:
platforms:
android: