👻
Flutter/riverpod でporcupineを使う
Picovoice、ご存知ですか?
音声認識のエッジプラットフォームで、様々な言語で利用できます。
今回はこちらの中のporcupineというWakeWord Managerをriverpodを通して使用してみます。
基本的にはこちらに掲載してある通りに行うとうまく行きます。
新規プロジェクトの立ち上げ
新しいプロジェクトを立ち上げて必要なライブラリを入れる
flutter create --org com.hoge porcupine_app_example
必要なライブラリのインストール
こちらはコピペではなく、VSCodeだとcontrol+shift+p
→ Dart:Add Dependencyを利用するとめちゃくちゃ簡単にライブラリをインストールできます。オススメ!
pubspec.yaml
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
porcupine_flutter: ^2.1.6
hooks_riverpod: ^2.0.2
json_annotation: ^4.7.0
freezed_annotation: ^2.2.0
flutter_hooks: ^0.18.5+1
flutter_dotenv: ^5.0.2
picovoice IDの作成とWakeWordファイルのダウンロード
picovoiceのページにてIDを作ります。問題なければGoogleのアカウントを利用すると簡単です。
ログインするとAccess KeyとPorcupineのWakeWord登録画面が見れると思います。
PorcupineでWakeWordの登録を行います。
LanguageはJapanese , PlatformはAndroidで好きなWakeWordを登録します。そしてTrain Wake Wordボタンを押すと訓練済みのデータをダウンロードできるようになります。
こちらのデータと、日本語をWakeWordとして登録する場合は、こちらのリポジトリをクローンして https://github.com/Picovoice/porcupine/tree/master/lib/common
よりporcupine_params_ja.pv
を両方assetsの中に登録をします。
pubspec.yaml
assets:
- assets/wakewords/yourawesomewakeword_ja_android_v2_1_0.ppn
- assets/wakewords/
- .env
パーミッションの追加
<manifest>
直下に以下のパーミッションを追加します。
AndroidManifest.xml
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
あとはコード
今回はシンプルにWakeWordを検出したら文字を変えるだけにしています。
必要に応じて色々できると思います。
porcupine_manager_provider.dart
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:porcupine_flutter/porcupine_error.dart';
import 'package:porcupine_flutter/porcupine_manager.dart';
final detectedNumberProvider = StateProvider<bool>((ref) => false);
final porcupineManagerProvider = FutureProvider<PorcupineManager>((ref) async {
await dotenv.load(fileName: '.env');
String accessKey = dotenv.get('ACCESS_KEY');
PorcupineManager _manager;
final detectedNum = ref.watch(detectedNumberProvider.notifier);
void _detectedCallback(int keywordIndex) {
detectedNum.state = true;
print('detected');
}
_manager = await PorcupineManager.fromKeywordPaths(
accessKey,
["assets/wakewords/awesomewakeword_ja_android_v2_1_0.ppn"],
_detectedCallback,
modelPath: 'assets/wakewords/porcupine_params_ja.pv',
);
_manager.start();
ref.onDispose(() async {
await _manager.delete();
});
return _manager;
});
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:picovoice_sample/porcupine_manager_provider.dart';
import 'package:porcupine_flutter/porcupine_error.dart';
import 'package:porcupine_flutter/porcupine_manager.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final _manager = ref.watch(porcupineManagerProvider);
final _detected = ref.watch(detectedNumberProvider);
print("$_detected state");
return Scaffold(
appBar: AppBar(
title: Text('app test'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_manager.when(
data: (manager) {
// manager.start();
return Text(
_detected ? 'detected!' : "waiting..",
style: Theme.of(context).textTheme.headline4,
);
},
error: (error, stackTrace) => Text(error.toString()),
loading: () => CircularProgressIndicator()),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
print('toggle');
ref.read(detectedNumberProvider.notifier).state = false;
},
tooltip: 'Toggle',
child: Icon(_detected ? Icons.toggle_off : Icons.toggle_on),
),
);
}
}
Discussion