👻

Flutter/riverpod でporcupineを使う

2022/10/15に公開

Picovoice、ご存知ですか?
音声認識のエッジプラットフォームで、様々な言語で利用できます。

https://picovoice.ai/

今回はこちらの中の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