🔍

Flutter_Pluginのspeech_to_textを弄り倒した時の知見について

に公開

この記事を書くに至ったとあるトラブル

Flutterのプラグインspeech_to_textを使用しているコードでとあるバグが発生しました。そのコードでは発話が途切れたことを検知して取得した音声のデータを違和感のない内容に直すREST APIを呼び出すという内容で、その考えは至って正常のように思われたのですが、iOSでごく稀に途切れの検知ができないことがありました。そこで、プロジェクト用にプラグインを改良した際にFlutterとSwiftの両方のコードなどを見て得た知見を共有しようと思います。

「発話が途切れたこと」は、発話中に喋らず数秒が空いている状態やその開始時点という意味で使用します。一応、吃音はこれに該当しないと思われますが試していないので無視をします。なので、今回はあくまで発話の一時的な中断と思ってください。

そのコードの問題点

バグを発生させていた問題点は「発話が途切れたことを検知」するロジックが間違っていたと言うのが結論でした。発話を文字起こしする関数listen()の定義は以下のとおりです。不要なコメントや定数は省いています。

関数`listen()`の定義と関係する型の定義
speech_to_text.dart
 Future listen(
      {SpeechResultListener? onResult,
      Duration? listenFor,
      Duration? pauseFor,
      String? localeId,
      SpeechSoundLevelChange? onSoundLevelChange,
      ('Use SpeechListenOptions.cancelOnError instead')
      cancelOnError = false,
      ('Use SpeechListenOptions.partialResults instead')
      partialResults = true,
      ('Use SpeechListenOptions.onDevice instead') onDevice = false,
      ('Use SpeechListenOptions.listenMode instead')
      ListenMode listenMode = ListenMode.confirmation,
      ('Use SpeechListenOptions.sampleRate instead') sampleRate = 0,
      SpeechListenOptions? listenOptions})
speech_to_text.dart
typedef SpeechResultListener = void Function(SpeechRecognitionResult result);
speech_recognition_result.dart
(explicitToJson: true)
class SpeechRecognitionResult {
  List<SpeechRecognitionWords> alternates;

  final bool finalResult;
}
speech_recognition_result.dart
()
class SpeechRecognitionWords {
  final String recognizedWords;

  final double confidence;
}

「発話が途切れたことを検知」するロジックはlisten()onResultに渡した処理で引数のresultconfidenceを参照して途切れの判定をしていました。お気づきの方もいらっしゃると思いますが、このconfidenceは文字起こしの信頼度を参照するのであって「発話が途切れたことを検知」する値ではなかったというだけの話です。ですが、確かに「発話が途切れたこと」を検知すること自体はある意味・ある程度は可能なわけでSwiftのライブラリSpeechSpeechRecognizerから渡されるconfidenceをどうFlutterのプラグインに受け渡すか、Flutterプラグインでどのように受け取り利用するのかという点でバグやエラーが発生した思っていた次第です。

そもそもconfidenceってどこからきたのか?

言われてみれば、confidenceってどのように算出され、どのように得られるのかということを忘れていました。

be_worried

そこでコードを読み解くと、onResultに渡した処理は色々と受け渡しされ、加工されてSwiftのhandleResultという関数でFlutterの呼び出しをされることで呼び出されてました。つまり、これを呼び出している処理こそがconfidenceを設定する犯人というわけです。

これの正体は以下のSwiftのSFSpeechRecognitionTaskDelegateに設定された関数

  • speechRecognitionTask(_, didHypothesizeTranscription)
  • speechRecognitionTask(_, didFinishRecognition)

でした。

Delegateについては後日、記事を書けたら書こうと思うのですが、とりあえず「フレーム側に設定されたある条件を満たしたときに発火する処理」(関数やメソッド)を渡す手法の一つだと思ってください。ここで2つのDelegateの関数の機能を見てみましょう。

speechRecognitionTask(_, didHypothesizeTranscription)とは

公式Docはこちらです。

これは、公式Docで

Tells the delegate that a hypothesized transcription is available.

と書かれています。日本語訳(Google)すると

仮説的な文字起こしが利用可能であるとDelegateに伝える

仮説的な文字起こしが利用可能である?わかりづらいですね、これ。Discussionという項目でいつ呼ばれるかを見つけました。

このメソッドは、部分的な認識も含め、すべての認識に対して呼び出されます。

つまり、認識している間、文字が増えたときとかに呼ばれるわけですね。このことから、これは発話中の全ての状況でほぼ連続的に呼び出され続けるということがわかりました。もう一つはどうでしょう?

speechRecognitionTask(_, didFinishRecognition)とは

公式Docはこちらです。

見た感じ、didFinishRecognitionとかかれているので最後に呼ばれるって感じですけど、Discussionではどうでしょうか?

メソッドが呼ばれてからは、Delegateは発話に関する情報の取得が期待できない

どういうことでしょうか?Docsの本文は??

最後の発話が認識されたときにデリゲートに通知します。

わかりやすい、これ。

つまり、、、

発話中と発話の最後で呼び出されるってことですね。発話の最後、これってもしかするとですよね、、、

finding

予測より計測です。やってみましょう。

実験

iOS側で2つのDelegateの関数でFlutter側に渡ってきている明確な差分はbool finalResultだけです。この値を見ればわかるかもしれません。なのでonResultに渡していた処理の最初にこれを追加しました。

print('isFinal: ${result.finalResult}');

これで監視できますね。

手順は以下のとおりです。また、実験に関係ない余計なログは見せないことにします。
また、実験の際には「発話」ブロックの中で実際に何回か発話を途切れさせるため、アナウンサーでお馴染みの「アメンボ赤いな、あいうえお」を50音の各行ごとに2〜3秒ほど間隔をあけて言い、結果が自明になった時点で終了します。(あれ、サ行とかラ行はきついので)

実験結果

(40)flutter: isFinal: false
flutter: isFinal: true

望ましい結果だった場合、isFinal: trueisFinal: falseの結果が交互に見えるわけですが、、、

これ発話の最後というより認識機能を終えたタイミング、つまりSpeechToText.stop()を呼び出したタイミングがspeechRecognitionTask(_, didFinishRecognition)の呼ばれるタイミングというわけですね。

発話が途切れたタイミングってどうしよう、、、

結論

まず、発話が途切れたことを認識する機能自体がspeech_to_textにないというのが結論のようです。なので、発話が途切れたことを認識するFlutterプラグインを自分で作った方が早いかもしれないです。途切れの認識する条件は難しいかもしれないですが、こっちの方が実現性がありそうだと判断しました。まぁ、提案も実装もする予定はないのですが。

ちょっと気になったので、、、

Androidではどうなのか気になったので見てみました。

speechResult.put("finalResult", isFinal)
val confidence = speechBundle.getFloatArray(SpeechRecognizer.CONFIDENCE_SCORES)

このことから、どっちもconfidenceの計算はフレームワーク頼りってことですし、あくまで信頼度でしかないってわけですね。ちなみに、Androidでは発話の最後でfinalResulttrueになりますが、そもそもRecognizerが発話が途切れた時点で起動していたRecognizerが閉じるので利用するデバイスのOSで仕様の変わるプラグインのようです。

mutex Tech Blog

Discussion