feat: 添加唤醒功能

This commit is contained in:
2025-09-28 18:42:56 +08:00
parent c6765ab29c
commit 5be50b4dbb
18 changed files with 746 additions and 28 deletions

View File

@@ -30,6 +30,10 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<service
android:name="com.tsystems.flutter_vosk_wakeword.WakewordService"
android:exported="false"
android:foregroundServiceType="microphone" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

View File

@@ -66,7 +66,9 @@ void main() async {
runApp(const MyApp());
AIChatAssistantManager.instance.setWakeWordDetection(true);
// AIChatAssistantManager.instance.setWakeWordDetection(true);
AIChatAssistantManager.instance.startVoskWakeword();
}
class MyApp extends StatelessWidget {

View File

@@ -3,6 +3,7 @@ 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';
@@ -86,6 +87,36 @@ class AIChatAssistantManager {
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);
// 可选:一段时间后自动重置状态
Future.delayed(const Duration(seconds: 2), () {
_wakeWordDetected = false;
});
});
}
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) {
@@ -148,5 +179,7 @@ class AIChatAssistantManager {
_commandCubit = null;
_wakeWordController.close();
setWakeWordDetection(false); // 确保 Porcupine 停止并释放
FlutterVoskWakeword.instance.dispose();
debugPrint('AIChatAssistant 单例销毁');
}
}

View File

@@ -0,0 +1 @@
91a26295-76d2-4da7-a414-4b2ebf9ddeb0

View File

@@ -0,0 +1 @@
2d4432c8-7421-492b-93c2-ad728fd1947d

View File

@@ -1,3 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tsystems.flutter_vosk_wakeword">
<permission
android:name="com.tsystems.flutter_vosk_wakeword.RECORD_AUDIO"
android:protectionLevel="signature" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
</manifest>

View File

@@ -0,0 +1,81 @@
package com.tsystems.flutter_vosk_wakeword
import android.Manifest
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.os.Process
import android.util.Log
import androidx.annotation.RequiresPermission
import org.vosk.Recognizer
import org.vosk.android.RecognitionListener
import java.util.concurrent.Executors
class AudioRecorder(private val recognizer: Recognizer,
private val listener: RecognitionListener) {
companion object {
private const val TAG = "AudioRecorder"
private const val SAMPLE_RATE = 16000
private const val BUFFER_SIZE_FACTOR = 2
}
private var audioRecord: AudioRecord? = null
private val executor = Executors.newSingleThreadExecutor()
fun isRecording(): Boolean = audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
fun start() {
if (isRecording()) return
val bufferSize = AudioRecord.getMinBufferSize(
SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
) * BUFFER_SIZE_FACTOR
// 权限检查应该在调用此方法之前在App层完成
audioRecord = AudioRecord(
MediaRecorder.AudioSource.VOICE_RECOGNITION,
SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize
)
if (audioRecord?.state == AudioRecord.STATE_INITIALIZED) {
audioRecord?.startRecording()
executor.execute {
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
val buffer = ByteArray(bufferSize)
while (isRecording()) {
val nread = audioRecord?.read(buffer, 0, buffer.size) ?: 0
if (nread > 0) {
Log.i(TAG, "Read $nread bytes from AudioRecord")
val ac = recognizer.acceptWaveForm(buffer, nread)
Log.i(TAG, "Recognizer acceptWaveForm returned: $ac")
if (ac) {
listener.onResult(recognizer.result)
} else {
listener.onPartialResult(recognizer.partialResult)
}
}
}
listener.onFinalResult(recognizer.finalResult)
}
Log.i(TAG, "Audio recording started.")
} else {
Log.e(TAG, "AudioRecord initialization failed.")
listener.onError(Exception("AudioRecord initialization failed."))
}
}
fun stop() {
if (isRecording()) {
audioRecord?.stop()
audioRecord?.release()
audioRecord = null
Log.i(TAG, "Audio recording stopped.")
}
}
}

View File

@@ -0,0 +1,54 @@
package com.tsystems.flutter_vosk_wakeword
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.core.content.ContextCompat
/**
* Service 和 Plugin 之间安全地传递事件
*/
class EventReceiver(private val onEvent: (type: String, text: String?) -> Unit) : BroadcastReceiver() {
companion object {
private const val ACTION_WAKEWORD_EVENT = "com.tsystems.flutter_vosk_wakeword.WAKEWORD_EVENT"
private const val EXTRA_EVENT_TYPE = "EXTRA_EVENT_TYPE"
private const val EXTRA_EVENT_TEXT = "EXTRA_EVENT_TEXT"
fun broadcastPartial(context: Context, text: String) = broadcast(context, "partial", text)
fun broadcastFinal(context: Context, text: String) = broadcast(context, "finalResult", text)
fun broadcastWakeWord(context: Context, text: String) = broadcast(context, "wakeword", text)
fun broadcastError(context: Context, text: String) = broadcast(context, "error", text)
private fun broadcast(context: Context, type: String, text: String?) {
val intent = Intent(ACTION_WAKEWORD_EVENT).apply {
setPackage(context.packageName)
putExtra(EXTRA_EVENT_TYPE, type)
putExtra(EXTRA_EVENT_TEXT, text)
}
context.sendBroadcast(intent)
}
}
override fun onReceive(context: Context?, intent: Intent?) {
val type = intent?.getStringExtra(EXTRA_EVENT_TYPE)
val text = intent?.getStringExtra(EXTRA_EVENT_TEXT)
if (type != null) {
onEvent(type, text)
}
}
fun register(context: Context) {
ContextCompat.registerReceiver(
context,
this,
IntentFilter(ACTION_WAKEWORD_EVENT),
ContextCompat.RECEIVER_NOT_EXPORTED
)
}
fun unregister(context: Context) {
context.unregisterReceiver(this)
}
}

View File

@@ -1,38 +1,88 @@
package com.tsystems.flutter_vosk_wakeword
import android.content.Intent
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
// # vosk
// vosk
import org.vosk.LibVosk
/** FlutterVoskWakewordPlugin */
class FlutterVoskWakewordPlugin: FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var channel : MethodChannel
class FlutterVoskWakewordPlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler {
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_vosk_wakeword")
channel.setMethodCallHandler(this)
}
private lateinit var methodChannel : MethodChannel
private lateinit var eventChannel: EventChannel
private var eventSink: EventChannel.EventSink? = null
private lateinit var context: android.content.Context
private var eventReceiver: EventReceiver? = null
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_vosk_wakeword/methods")
methodChannel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "flutter_vosk_wakeword/events")
eventChannel.setStreamHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
val callName = call.method
if (callName == "initialize") {
// val modelPath = call.argument<String>("modelPath")
val wakeWords = call.argument<ArrayList<String>>("wakeWords")
if (wakeWords == null) {
result.error("INVALID_ARGUMENTS", "wakeWords is null", null)
return
}
val intent = Intent(context, WakewordService::class.java).apply {
action = WakewordService.ACTION_INITIALIZE
putExtra(WakewordService.EXTRA_WAKE_WORDS, wakeWords)
}
context.startService(intent)
} else if (callName == "start") {
val intent = Intent(context, WakewordService::class.java).apply {
action = WakewordService.ACTION_START
}
context.startService(intent)
result.success(null)
} else if (callName == "stop") {
val intent = Intent(context, WakewordService::class.java).apply {
action = WakewordService.ACTION_STOP
}
context.startService(intent)
result.success(null)
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
context.stopService(Intent(context, WakewordService::class.java)) }
// --- EventChannel.StreamHandler Callbacks ---
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
// 注册广播接收器以从 Service 接收事件
eventReceiver = EventReceiver { eventType, text ->
// 将事件发送回 Dart
eventSink?.success(mapOf("type" to eventType, "text" to text))
}
eventReceiver?.register(context)
}
override fun onCancel(arguments: Any?) {
eventSink = null
eventReceiver?.unregister(context)
eventReceiver = null
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}

View File

@@ -0,0 +1,21 @@
package com.tsystems.flutter_vosk_wakeword
import org.json.JSONObject
object VoskRecognizer {
fun parsePartialResult(json: String): String {
return try {
JSONObject(json).getString("partial")
} catch (e: Exception) {
""
}
}
fun parseFinalResult(json: String): String {
return try {
JSONObject(json).getString("text")
} catch (e: Exception) {
""
}
}
}

View File

@@ -0,0 +1,27 @@
package com.tsystems.flutter_vosk_wakeword
import org.json.JSONArray
class WakewordDetector(private val wakeWords: ArrayList<String>) {
/**
* 检查部分识别文本中是否包含任何一个唤醒词
*/
fun checkForWakeWord(partialText: String): Boolean {
if (partialText.isBlank()) return false
// 简单包含匹配,可以根据需求改为更复杂的匹配逻辑
return wakeWords.any { wakeWord -> partialText.contains(wakeWord, ignoreCase = true) }
}
/**
* 为Vosk生成语法字符串这可以提高唤醒词的识别准确率
* 例如,对于 ["你好小帅", "你好小美"],生成 "[\"你好小帅\", \"你好小美\", \"[unk]\"]"
*/
fun getVoskGrammar(): String {
val jsonArray = JSONArray()
wakeWords.forEach { jsonArray.put(it) }
// 添加 [unk] 允许识别唤醒词之外的其他词语
// jsonArray.put("[unk]")
return jsonArray.toString()
}
}

View File

@@ -0,0 +1,269 @@
package com.tsystems.flutter_vosk_wakeword
import android.Manifest
import android.app.Service
import android.content.Intent
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Process
import android.util.Log
import androidx.annotation.RequiresPermission
import org.vosk.Model
import org.vosk.Recognizer
import org.vosk.android.RecognitionListener
import org.vosk.android.StorageService
import java.io.IOException
import kotlin.collections.contentToString
class WakewordService: Service(), RecognitionListener {
companion object {
const val ACTION_INITIALIZE = "ACTION_INITIALIZE"
const val ACTION_START = "ACTION_START"
const val ACTION_STOP = "ACTION_STOP"
const val EXTRA_WAKE_WORDS = "EXTRA_WAKE_WORDS"
private const val TAG = "WakewordService"
private const val ASSET_MODEL_DIR = "model-cn"
private const val LOCAL_MODEL_DIR = "model"
// 冷却时间: 同一唤醒词触发后在此毫秒内不再重复广播
private const val DETECTION_COOLDOWN_MS = 2500L
}
private val mainHandle = Handler(Looper.getMainLooper())
private var model: Model? = null
private var recognizer: Recognizer? = null
private var audioRecorder: AudioRecorder? = null
private var wakeWordDetector: WakewordDetector? = null
private var pendingStart = false
private var modelLoading = false
// 去重/状态控制
private var lastWakeWord: String? = null
private var lastDetectTs: Long = 0L
private var suppressUntilFinal = false
override fun onCreate() {
super.onCreate()
Log.w(TAG, "onCreate pid=${Process.myPid()} thread=${Thread.currentThread().name}")
dumpAssets()
}
private fun dumpAssets() {
try {
val root = assets.list("")?.joinToString()
Log.w(TAG, "assets root=$root")
val cn = assets.list("model-cn")?.joinToString()
Log.w(TAG, "assets/model-cn=$cn")
} catch (e: Exception) {
Log.e(TAG, "dumpAssets error", e)
}
}
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val action = intent?.action
when (action) {
ACTION_INITIALIZE -> {
val wakeWords = intent.getStringArrayListExtra(EXTRA_WAKE_WORDS)
Log.i(TAG, "ACTION_INITIALIZE wakeWords=${wakeWords.toString()}")
if (wakeWords != null && wakeWords.isNotEmpty()) {
initialize(wakeWords)
} else {
Log.e(TAG, "No valid wake words found in Intent extras.")
}
}
ACTION_START -> {
pendingStart = true
Log.i(TAG, "ACTION_START received (modelReady=${model != null}, loading=$modelLoading)")
startRecognition()
}
ACTION_STOP -> {
Log.i(TAG, "ACTION_STOP received")
stopRecognition()
}
}
return START_STICKY
}
private fun initialize(wakeWords: ArrayList<String>) {
if (model != null) {
Log.i(TAG, "Model already loaded; skip initialize.")
return
}
if (modelLoading) {
Log.i(TAG, "Model already loading; skip duplicate initialize.")
return
}
Log.i(TAG, "initialize() begin")
try {
stopRecognition()
wakeWordDetector = WakewordDetector(wakeWords)
modelLoading = true
Log.i(TAG, "Submitting StorageService.unpack source=model-cn target=model")
StorageService.unpack(this,
ASSET_MODEL_DIR,
LOCAL_MODEL_DIR,
{ loadedModel ->
Log.i(TAG, "Vosk model loaded successfully")
this.model = loadedModel
modelLoading = false
if (pendingStart) {
mainHandle.post {
startRecognition()
}
}
},
{ exception ->
modelLoading = false
val errorMsg = "Failed to unpack the model: ${exception.message}"
Log.e(TAG, errorMsg, exception)
EventReceiver.broadcastError(this, errorMsg)
})
} catch (e: IOException) {
modelLoading = false
Log.e(TAG, "Failed to load Vosk model", e)
EventReceiver.broadcastError(this, "Failed to load Vosk model: ${e.message}")
}
}
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
private fun startRecognition() {
val modelReady = model != null
Log.i(TAG, "startRecognition(modelReady=$modelReady pendingStart=$pendingStart loading=$modelLoading)")
if (!pendingStart) {
Log.i(TAG, "No pending start request; abort.")
return
}
if (!modelReady) {
Log.i(TAG, "Model not ready yet; start deferred.")
return
}
if (audioRecorder?.isRecording() == true) {
Log.w(TAG, "Already recording.")
return
}
try {
val grammar = wakeWordDetector?.getVoskGrammar()
val currentRecognizer = Recognizer(model, 16000.0f, grammar)
recognizer = currentRecognizer
audioRecorder = AudioRecorder(currentRecognizer, this)
audioRecorder?.start()
Log.i(TAG, "Recognition started.")
resetDetectionState()
} catch (e: Exception) {
val errorMsg = "Failed to start recognition: ${e.message}"
Log.e(TAG, "Failed to start recognition", e)
EventReceiver.broadcastError(this, errorMsg)
}
}
private fun stopRecognition() {
Log.i(TAG, "stopRecognition()")
audioRecorder?.stop()
audioRecorder = null
recognizer?.close()
recognizer = null
Log.i(TAG, "Recognition stopped.")
resetDetectionState()
}
private fun resetDetectionState() {
suppressUntilFinal = false
lastWakeWord = null
lastDetectTs = 0L
}
// --- RecognitionListener Callbacks ---
override fun onPartialResult(hypothesis: String?) {
hypothesis ?: return
val partialText = VoskRecognizer.parsePartialResult(hypothesis)
if (partialText.isBlank()) return
Log.d(TAG, "partial=$partialText suppress=$suppressUntilFinal")
if (suppressUntilFinal) return
if (wakeWordDetector?.checkForWakeWord(partialText) == true) {
val now = System.currentTimeMillis()
if (shouldBroadcast(partialText, now)) {
broadcastWake(partialText, now, source = "partial")
// 抑制后续重复直到 finalResult 或超时
suppressUntilFinal = true
}
}
}
override fun onResult(hypothesis: String?) {
hypothesis ?: return
val finalText = VoskRecognizer.parseFinalResult(hypothesis)
if (finalText.isBlank()) return
Log.d(TAG, "result=$finalText suppress=$suppressUntilFinal")
if (wakeWordDetector?.checkForWakeWord(finalText) == true) {
val now = System.currentTimeMillis()
if (shouldBroadcast(finalText, now)) {
broadcastWake(finalText, now, source = "final")
}
}
// final / result 结束一轮,允许下一次
suppressUntilFinal = false
}
override fun onFinalResult(hypothesis: String?) {
// 已在 onResult 处理; 这里可以忽略或调用 onResult
onResult(hypothesis)
}
private fun shouldBroadcast(word: String, now: Long): Boolean {
val cooldownPassed = (now - lastDetectTs) > DETECTION_COOLDOWN_MS
val differentWord = word != lastWakeWord
return cooldownPassed || differentWord
}
private fun broadcastWake(word: String, now: Long, source: String) {
lastWakeWord = word
lastDetectTs = now
Log.i(TAG, "Wake word detected($source): $word")
EventReceiver.broadcastWakeWord(this, word)
// 重置识别器状态,为下一次唤醒做准备
recognizer?.reset()
// 可选:重建 recognizer 减少同一发音尾部抖动:
// try {
// val grammar = wakeWordDetector?.getVoskGrammar()
// recognizer?.close()
// recognizer = Recognizer(model, 16000.0f, grammar)
// } catch (e: Exception) {
// Log.w(TAG, "Recognizer reset failed: ${e.message}")
// }
}
override fun onError(e: Exception?) {
Log.e(TAG, "Recognition error", e)
EventReceiver.broadcastError(this, e?.message ?: "Unknown recognition error")
suppressUntilFinal = false
}
override fun onTimeout() {
Log.w(TAG, "Recognition timeout.")
// 超时视为一轮结束
suppressUntilFinal = false
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
stopRecognition()
model?.close()
model = null
super.onDestroy()
Log.i(TAG, "WakewordService destroyed.")
}
}

View File

@@ -1,8 +1,8 @@
// 导出核心管理类
export 'src/vosk_wakeword_manager.dart';
import 'src/interface/flutter_vosk_wakeword_platform_interface.dart';
// 导出错误定义
export 'src/vosk_wakeword_error.dart';
class FlutterVoskWakeword {
Future<String?> getPlatformVersion() {
return FlutterVoskWakewordPlatform.instance.getPlatformVersion();
}
}
// 导出数据模型
export 'src/model/recognition_event.dart';

View File

@@ -26,4 +26,7 @@ abstract class FlutterVoskWakewordPlatform extends PlatformInterface {
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}

View File

@@ -0,0 +1,39 @@
// recognition event type
enum RecognitionEventType {
// partial
partial,
// final result
finalResult,
// wakeword
wakeword,
// error
error
}
// recognition event
class RecognitionEvent {
final RecognitionEventType type;
final String? text;
RecognitionEvent({required this.type, this.text});
factory RecognitionEvent.fromJson(Map<String, dynamic> json) {
return RecognitionEvent(
type: RecognitionEventType.values.firstWhere((e) => e.name == json['type']),
text: json['text'],
);
}
Map<String, dynamic> toJson() {
return {
'type': type.name,
'text': text,
};
}
@override
String toString() {
return 'RecognitionEvent{type: $type, text: $text}';
}
}

View File

@@ -0,0 +1,51 @@
/// Vosk 插件的基础异常类
class VoskWakewordException implements Exception {
final String? message;
VoskWakewordException([this.message]);
@override
String toString() => '$runtimeType: $message';
}
/// 当原生代码无法分配内存时抛出
class VoskWakewordMemoryException extends VoskWakewordException {
VoskWakewordMemoryException(super.message);
}
/// 当模型文件或IO操作失败时抛出
class VoskWakewordIOException extends VoskWakewordException {
VoskWakewordIOException(super.message);
}
/// 当传入无效参数时抛出
class VoskWakewordInvalidArgumentException extends VoskWakewordException {
VoskWakewordInvalidArgumentException(super.message);
}
/// 当状态不正确例如未初始化就调用start时抛出
class VoskWakewordInvalidStateException extends VoskWakewordException {
VoskWakewordInvalidStateException(super.message);
}
/// 当发生未预期的运行时错误时抛出
class VoskWakewordRuntimeException extends VoskWakewordException {
VoskWakewordRuntimeException(super.message);
}
/// 将原生端返回的错误码转换为对应的 Exception
Exception voskStatusToException(String code, String? message) {
switch (code) {
case 'VoskWakewordMemoryException':
return VoskWakewordMemoryException(message);
case 'VoskWakewordIOException':
return VoskWakewordIOException(message);
case 'VoskWakewordInvalidArgumentException':
return VoskWakewordInvalidArgumentException(message);
case 'VoskWakewordInvalidStateException':
return VoskWakewordInvalidStateException(message);
case 'VoskWakewordRuntimeException':
return VoskWakewordRuntimeException(message);
default:
return VoskWakewordException("Unexpected error: $code, message: $message");
}
}

View File

@@ -0,0 +1,77 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'vosk_wakeword_error.dart';
import 'model/recognition_event.dart';
/// Vosk 唤醒词插件的主接口 (单例模式)
///
/// 这个类负责向原生代码发送指令,并通过 Stream 接收识别事件。
/// 所有的音频采集和处理都在原生端完成。
class FlutterVoskWakeword {
// 私有构造函数
FlutterVoskWakeword._internal();
// 获取唯一实例
static FlutterVoskWakeword get instance => _instance;
// 静态、唯一的实例
static final FlutterVoskWakeword _instance = FlutterVoskWakeword._internal();
// Factory 构造函数返回唯一实例
factory FlutterVoskWakeword() => _instance;
static const MethodChannel _methodChannel = MethodChannel('flutter_vosk_wakeword/methods');
static const EventChannel _eventChannel = EventChannel('flutter_vosk_wakeword/events');
Stream<RecognitionEvent>? _recognitionStream;
/// 监听识别事件的流
Stream<RecognitionEvent> get recognitionStream {
_recognitionStream ??= _eventChannel
.receiveBroadcastStream()
.map((event) => RecognitionEvent.fromJson(Map<String, dynamic>.from(event)));
return _recognitionStream!;
}
/// 初始化 Vosk 引擎和模型
Future<bool> initialize({
// required String modelPath,
required List<String> wakeWords,
}) async {
try {
final result = await _methodChannel.invokeMethod<bool>('initialize', {
// 'modelPath': modelPath,
'wakeWords': wakeWords,
});
return result ?? false;
} on PlatformException catch (e) {
throw voskStatusToException(e.code, e.message);
}
}
/// 开始监听
Future<bool> start() async {
try {
return await _methodChannel.invokeMethod<bool>('start') ?? false;
} on PlatformException catch (e) {
throw voskStatusToException(e.code, e.message);
}
}
/// 停止监听
Future<bool> stop() async {
try {
return await _methodChannel.invokeMethod<bool>('stop') ?? false;
} on PlatformException catch (e) {
throw voskStatusToException(e.code, e.message);
}
}
/// 释放所有原生资源
Future<bool> dispose() async {
try {
return await _methodChannel.invokeMethod<bool>('dispose') ?? false;
} on PlatformException catch (e) {
throw voskStatusToException(e.code, e.message);
}
}
}

View File

@@ -16,7 +16,6 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
pigeon: ^25.5.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec