FC2ブログ

スマフォのアプリを作りたい(16):音声認識させたい④

   プログラミング [2020/03/07]
前回、react-native-google-speech-apiパッケージを使って、何やら日本語の音声認識動作ができるところまで確認しました。
ただし、続けて動作させているうちに、ハング状態になる問題があります。
それと、使用言語を日本語にはしてみましたが、パッケージ自体はあまり汎用的な作りになっていない感じです。
上記をもうちょっと改善したいなーなんてちょっと無謀かもしれませんが、やってみようかと・・・。

まずは、Exampleのコードが何をやってそうか予想するところからになります。

◆react-native-google-speech-apiのApp.jsの概要
react-native-google-speech-api\ExampleAppにあったApp.jsについてです。
さるが使っているプロジェクト内では、Speech.jsという名前のファイルにして、処理はSpeechクラスとして定義しています。
実際のコードの内容に関しては、前回の記事に丸っと載せたのでそちらをご参照ください。(根拠のない無責処理が含まれます。)

まずは、初期処理:componentDidMount()メソッドで、
・GoogleSpeechApi.setApiKey()メソッドで、Google Cloud Platformの「APIキー」を設定しています。
・EventEmitter.addListener()で、「onSpeechRecognized」と「onSpeechRecognizedError」と命名されているイベントに対してコールバック処理を定義しています。

実際の操作に関しては、render()メソッドで、
・「Start listening」と表示された(なぜか、キャプションとしてはすべて大文字で表示される。)ボタンをタップした場合、requestAudioPermission()メソッドがコールバックされるように指定しています。
・requestAudioPermission()メソッドでは、「async」※1と入っているため非同期実行なのかと思います。PermissionsAndroid.request()メソッドの実行完了を待って、自startListening()メソッドを呼び出しています。予想ですが、PermissionsAndroid.request()は、マイク入力の使用の許可申請を行うメソッドかと思います。
・startListening()メソッドでは、ボタンのキャプションを「I'm listening」に変更したのち、GoogleSpeechApi.start()メソッドを呼び出しています。予想ですが、この時点でマイク入力音声をGoogleCloud側に送り込み認識(テキスト変換)することを開始指示しているかと思います。

マイクの入力があった場合は、
・componentDidMount()メソッド内のEventEmitter.addListener()で指定した2つのfunctionが「onSpeechRecognized」と「onSpeechRecognizedError」の2種類のイベントに応じて実行されるものと思います。


いくつか分からないことがあります。

・startListening()した後で発生するイベントは、「onSpeechRecognized」と「onSpeechRecognizedError」だけ?
・もし、マイクから音が入ってこない場合は、永遠に待つ?タイムアウトはないのか?
・上記の「待ち」を強制的に終了させることはできないのか?
・同じく「待ち」状態のタイミングで、ボタンを再度タップした場合、requestAudioPermission()を再度実行してしまう。ガードしている節がない。大丈夫なのか?交通整理(待ち)とかキューインとかの能力がPermissionsAndroid.request()、あるいはGoogleSpeechApi.start()メソッド内にあるのか?

上記、辺りがハングする原因とその際のステート不明の原因ではないかと考えます。


-※1:ちょっと寄り道:asyncとawait-
いきなり、そんなこと言われても・・・JavaScriptド素人なので分かりません。
以下を読み始めました。
参考:https://qiita.com/soarflat/items/1a9613e023200bbebcb3
が、いきなり「Promise」を理解していることが前提で書いているみたいなので、(それすら知らないさるは)以下を読んでみました。
参考:https://qiita.com/toshihirock/items/e49b66f8685a8510bd76
※いずれもややこしい話をしていて、誤解があるといけないので、以下のさるな要約は信用しないで、ご自分で理解されることをお勧めします。

上記参考サイトのサンプルのコードの中にsetTimeout()という関数がよく出てきます。そこの理解からです。
「id = setTimeout(処理、実行タイミング);」と書くと、「実行タイミング」後に、「処理」を実行するという処理です。setTimeout()は、コールされた直後にid(タイマーのID)をリターンして次のステートメント(処理)を実行できるようになります。「実行タイミング」はmSec単位で指定します。例えば、「実行タイミング」が十分長く、直後に「clearTimeout(id);」とかやると、「処理」は実行されないでしょう。

new Promise((resolve, reject) => {処理});」とやると、Promise型(どこにも、Promiseがクラスだとか、Function型だとかの説明記述は見られませんでしたが、その方が理解しやすいかと思ってそう書きます。)「処理」=非同期実行される「処理」を定義/生成することを意味します。
function asyncFunc1(param) {return new Promise((resolve, reject) => {処理});}」とやれば、asyncFunc1は非同期で「処理」を実行する関数を定義したことになります。
「resolve」と「reject」はそれぞれ処理完了時のコールバック関数を2種類指定できるようになっていて、「resolve」は正常時、「reject」は異常時という約束になっているみたいです。「resolve」「reject」をどう使うのかは、「処理」の中身:Promise関数の作り手の自由になっています。
実行では、「asyncFunc1(param1).then(resolve関数、reject関数).catch(例外処理関数);」という記述の仕方になります。resolveでもrejectでもない結果の場合には、.catch()で指定された関数が実行されるということになります。reject関数指定は、省略される場合が多いみたい。「asyncFunc1(param1).then(・・・).catch(・・・);」は終わりを待たずに次のステートメントが実行されるものと思われます。「resolve関数」、「reject関数」指定を省略した場合、Promise関数内で使っていないのであれば何もしないと思いますが、使っていてコールバック条件が成立した場合は、例外が発生するかと思います。「catch.(例外処理関数)」が省略された場合は例外が発生しても何もしないということになります。※.finally()もES9でサポートされたそうです。蛇足でした。

さて、「async function asynFunc2() {・・・}」は、「function asyncFunc1(param) {return new Promise((resolve, reject) => {処理});}」と同じ働きのようです。一方、「const result2 = await asyncFunc2();」とやった場合には、asyncFunc2()の完了を待って、結果をresult2に入れるという意味になります。非同期じゃなくなります。ただし、「await」はasync関数の中でしか使えないという制限があります。んー、非同期関数内から非同期関数を呼ぶ場合に使うということですね。※ちなみに「asyncFunc2();」とやった場合、.then()とか.catch()を指定していないので、「非同期の結果如何による処理分けはしない」という場合ですね。



さらに、react-native-google-speech-apiパッケージ内のスクリプトを斜め読みします。

◆GoogleSpeechApiModule.javaの斜め読み
斜め読みしたくて、斜めに読んでるわけではなく、知識不足で斜め読みレベルにしかならざるを得ないのです。

  • GoogleSpeechApiModuleコンポーネント(クラス)を定義しています。
    • その中で、VoiceRecorder、ServiceConnectionコンポーネントのインスタンスを生成しています。
    • ServiceConnectionのonServiceConnected、onServiceDisconnectedメソッドをオーバーライド(書き換え)しています。
      • onServiceConnectedでは、speechServiceオブジェクト?を取得して、各イベント動作に自コンポーネント内のメソッド(ハンドラ)を加えています。(よくわかんないけど、たぶんそう。)
      • onServiceDisconnectedでは、speechServiceを破棄しています。

    • GoogleSpeechApiModuleはコンストラクタですね。基底クラスのコンストラクタの動作を実行させてます。中身は不明。
    • start()、stop()、getName()メソッドを定義しています。このプロジェクトのSpeech.js(react-native-google-speech-api-master\ExampleApp\Appjsベース)で「GoogleSpeechApi.start()」として使っているのは、ここのstart()メソッドかと思います。
      • stop()メソッドは呼び出しているところはないですが、認識処理を強制的に終了できるインターフェースかもしれません。
      • また、「GoogleSpeechApi.setApiKey()」としてAPIキーを設定しているインタフェースもここにあるため、言語設定インタフェースもここに入口を追加すればいいかと思います。
      • setApiKey()で設定されたキーは、onVoiceStart()イベントハンドラで、speechService.startRecognizing()をコールする際のパラメタとして指定されている。
      • speechService.startRecognizing()の処理実体は、「react-native-google-speech-api-master\android\src\main\java\com\reactlibrary\speech_service\」のSpeechService.java内にあります。言語コードもこの関数内でクラウド側に指定しているようです。
        →じゃあ、言語コードもここで指定させればいいよね。

    • あとは、イベント関連のメソッドが定義されています。音声入力側のイベントの処理分岐をhandleVoiceEvent()でやってます。このメソッドは、onServiceConnected()で、「voiceRecorder.getVoiceEventObservable().subscribe(・・・)」されてます。(これ以上どう言っていいのかわかんない。)音声入力側のイベント処理になっているかと思います。
    • handleVoiceEvent()からの分岐先で、onVoiceStart()、onVoice()、onVoiceEnd()があります。それぞれ、speechServiceのメソッドをコールしています。
      →先々、認識動作と録音を同時にさせたいんだけど、ここに手を入れてなんとかならんかなぁ。
    • handleSpeechEvent()は、「speechService.getSpeechEventObservable().subscribe(・・・)」されているので、音声認識(クラウドサービス)側のイベント処理になっているんだと思います。中身を見ると音声認識(テキスト変換)完了の処理のように見えます。ここで、Speech.js内で指定したコールバック関数(イベント名:"onSpeechRecognized")に制御を渡す(sendJSEvent()コール)処理をしています。
    • handleErrorEvent()は、音声入力、あるいは音声認識処理でエラーが発生した場合のイベント処理で、sendJSErrorEvent()→sendJSEvent()で、Speech.js内で指定したコールバック関数(イベント名:"onSpeechRecognizedError")に制御を渡しています。
      →「"onSpeechRecognized"、"onSpeechRecognizedError"以外のイベントはないのか!」って思ってたけど、ここでフィルタされてたんですね。

  • 各イベント処理の中で、「Log.i(・・・)」というのをコールしてます。
    →このログ出力は、どうやったら見られるんだろう。※2
  • クラウドによる音声認識の開始/終了は、音声入力側にコントロールされているよう。
    →発話を開始してから、言い淀んだりすると意図していないところで、切られて認識されるんだけど、その対策はどこでできるんだろう。


-※2:ちょっと寄り道:Log.i()-
Logコンポーネント?は「import android.util.Log;」でインポートされてます。AndroidのUtilのライブラリのようです。ぐぐります。
参考:https://qiita.com/kaleidot725/items/cbf2de28dcd5bc848a35
Log.X(tag,message)の形式で指定します。Xは、メッセージの種類を示す1文字です。verbose/debug/info/warn/error/なし:assertが準備されているようです。
AndroidStudio配下のLogcatというメニュー?で見られるんだそうです。
がーん。これまで全くAndroidStudioをほぼ使ってきてません。変に動かして、今のReact Native CLIベースの環境に悪影響ないかなぁとか思いながら、恐る恐るAndroidStudioを起動してみましたが、何等かのプロジェクトを作成してからでないと本丸にたどり着きそうになかったので、その先へは進みませんでした。もうちょっと探ったら、コマンドラインでも見れるとありました。
参考:https://developer.android.com/studio/command-line/logcat?hl=ja
「adb logcat」と実行すれば、ガーとメッセージが出て、ログのお尻で止まります。順次新しいログが出力されると表示されます。
フィルタ条件として、ログ出力時のtagが指定できます。
----コマンドプロンプト-----------------------------
C:\WINDOWS\system32>cd /d (Android Sdkインストール先)\platform-tools
D:\AppMake\Android\Sdk\platform-tools>adb logcat (ログ出力時tag):X *:S
---------------------------------------------------

フィルタ条件は、「出したいtag:X」を指定するのかとおもいきや、指定したtagのメッセージのminレベルX(VDIWEFSの順で高い)を指定するものだそうです。



◆[START LISTENING]ボタンの2度押し対策
ボタンを押して音声認識を開始してテキスト化している内にハング状態になる現象は、どうもボタンの2度押しに原因の一端があるようです。
(1)ボタンの2度押しをガードします。
(2)ボタンのキャプションを日本語化します。
(3)ボタンと変換後テキストの上下位置を交換し、スクロールできるようにします。
(*)機能しない追加コード↓の削除
※前回記事に掲載したSpeech.jsには、さるがあてずっぽうで入れた、onSpeechEnd/onSpeechError/onSpeechResultのイベント名でEventEmitter.addListener()をコールしている処理がありましたが、機能するわけもないので、今回は削除しています。

以下の赤字の部分が変更箇所です。
----Speech.js--------------------------------------
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow
*/

import {
NativeModules,
Platform,
NativeEventEmitter,
DeviceEventEmitter,
SafeAreaView,
ScrollView,

Text,
View,
Button,
Alert,
PermissionsAndroid,
} from 'react-native';

import React, { Component } from 'react';

const { GoogleSpeechApi } = NativeModules;

const EventEmitter = Platform.select({
android: DeviceEventEmitter,
ios: new NativeEventEmitter(GoogleSpeechApi),
});

const Btn_cap = ["認識開始", "認識中"];

export default class Speech extends Component {

constructor(props) {
super(props);
this.state = {
currentText: "",
previousTexts: "",
button: "",
btn_state: 0,

};
this.state.button = Btn_cap[this.state.btn_state];
}

componentDidMount(){
GoogleSpeechApi.setApiKey("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
EventEmitter.addListener('onSpeechRecognized', (event) => {
var previousTexts = this.state.previousTexts;
var currentText = event['text'];
this.state.btn_state = 1;
if (event['isFinal']){
currentText = "";
previousTexts = event['text'] + "\n" + previousTexts;
this.state.btn_state = 0;
}

this.setState({
currentText: currentText,
previousTexts: previousTexts,
button: Btn_cap[this.state.btn_state]
});
});

EventEmitter.addListener('onSpeechRecognizedError', (error) => {
this.state.btn_state = 0;
this.setState({
button: Btn_cap[this.state.btn_state]
});
Alert.alert(
"Error occured",
error['message']
);
});

}


startListening = () => {
this.setState({
button: Btn_cap[this.state.btn_state]
});
GoogleSpeechApi.start();
}

requestAudioPermission = async () => {
if (this.state.btn_state) {
console.log('double clicked button');
return;
}
this.state.btn_state = 1;


try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
{
title: 'Record Audio Permission',
message:
'App needs access to your microphone ' +
'so you can convert speech to text.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
},
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
console.log('permission granted');
this.startListening();
} else {
console.log('permission denied');
}
} catch (err) {
console.warn(err);
this.state.btn_state = 0;
}
}

render() {
return (
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic">
<View style={{ margin: 20 }}>
<Button
title={this.state.button}
onPress={Platform.OS === 'ios' ? this.startListening : this.requestAudioPermission}/>
<Text>{this.state.currentText}</Text>
<Text>{this.state.previousTexts}</Text>
</View>
</ScrollView>
</SafeAreaView>

);
}
}
---------------------------------------------------

this.state中にbuttonとbtn_stateの2種類があるとか、btn_stateの変更箇所があっちこっちにあるとかちょっと整理が不足していますが、細かいことは言いっこなしとしてください。

割とあっさり、ハング状態になる件は、この対策で軽減されました。軽減というのは、完全に消えたわけではないという意味です。
どこが原因かまだ分かってませんが、たまにテキスト化が遅いときがあります。使っているポケットWifiのせいなのかもしれません。


◆「使用言語」指定インタフェースの追加
「◆GoogleSpeechApiModule.javaの斜め読み」でちょっと話を出しましたが、使用言語を固定値として設定している個所は、「react-native-google-speech-api-master\android\src\main\java\com\reactlibrary\speech_service\」の「SpeechService.java」内にあります。当然、お国が変われば変更したいでしょうに。

使用言語(LanguageCode)を変更できるようにします。
----SpeechService.java-----------------------------
【変更前】

private static final String LANGUAGE_CODE = "en-US";
private static final int TERMINATION_TIMEOUT_SECONDS = 5;

private final SpeechBinder binder = new SpeechBinder();


public void startRecognizing(int sampleRate, String apiKey) {
final ManagedChannel channel = new OkHttpChannelProvider()
.builderForAddress(HOSTNAME, PORT)
.nameResolverFactory(new DnsNameResolverProvider())
.intercept(new GoogleCredentialsInterceptor(apiKey))
.build();

speechStub = SpeechGrpc.newStub(channel);
requestObserver = speechStub.streamingRecognize(responseObserver);
requestObserver.onNext(StreamingRecognizeRequest.newBuilder()
.setStreamingConfig(StreamingRecognitionConfig.newBuilder()
.setConfig(RecognitionConfig.newBuilder()
.setLanguageCode(LANGUAGE_CODE)
.setEncoding(RecognitionConfig.AudioEncoding.LINEAR16)
.setSampleRateHertz(sampleRate)
.build())
.setInterimResults(true)
.setSingleUtterance(true)
.build())
.build());
}

---------------------------------------------------
【変更後】

private static final String LANGUAGE_CODE = "en-US";
private static final int TERMINATION_TIMEOUT_SECONDS = 5;
private String LanguageCode = LANGUAGE_CODE;

private final SpeechBinder binder = new SpeechBinder();


public void startRecognizing(int sampleRate, String apiKey, String langCode) {
final ManagedChannel channel = new OkHttpChannelProvider()
.builderForAddress(HOSTNAME, PORT)
.nameResolverFactory(new DnsNameResolverProvider())
.intercept(new GoogleCredentialsInterceptor(apiKey))
.build();

if (langCode != null) {
LanguageCode = langCode;
}


speechStub = SpeechGrpc.newStub(channel);
requestObserver = speechStub.streamingRecognize(responseObserver);
requestObserver.onNext(StreamingRecognizeRequest.newBuilder()
.setStreamingConfig(StreamingRecognitionConfig.newBuilder()
.setConfig(RecognitionConfig.newBuilder()
.setLanguageCode(LanguageCode)
.setEncoding(RecognitionConfig.AudioEncoding.LINEAR16)
.setSampleRateHertz(sampleRate)
.build())
.setInterimResults(true)
.setSingleUtterance(true)
.build())
.build());
}

---------------------------------------------------

----GoogleSpeechApiModule.java---------------------
【変更前】

private CompositeDisposable compositeDisposable;
private String apiKey;


private void onVoiceStart() {
Log.i(TAG, "onVoiceStart");
if (speechService != null) {
speechService.startRecognizing(voiceRecorder.getSampleRate(), apiKey);
}
}

---------------------------------------------------
【変更後】

private CompositeDisposable compositeDisposable;
private String apiKey;
private String langCode;

@ReactMethod
public void setLangCode(String langCode) {
Log.i(TAG, "setLangCode");
this.langCode = langCode;
}


private void onVoiceStart() {
Log.i(TAG, "onVoiceStart");
if (speechService != null) {
speechService.startRecognizing(voiceRecorder.getSampleRate(), apiKey, langCode);
}
}

---------------------------------------------------

----Speech.js--------------------------------------
【変更前】

componentDidMount(){
GoogleSpeechApi.setApiKey("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
EventEmitter.addListener('onSpeechRecognized', (event) => {
var previousTexts = this.state.previousTexts;

---------------------------------------------------
【変更後】

componentDidMount(){
GoogleSpeechApi.setLangCode("ja_JP");
GoogleSpeechApi.setApiKey("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
EventEmitter.addListener('onSpeechRecognized', (event) => {
var previousTexts = this.state.previousTexts;

---------------------------------------------------



◆「言い淀み」対策
「つっかえてるだけ(言い淀み)なのか、発話の終わりなのかをどこで判定しているのか?」をソースを眺めたり、ログ出力を追加したりして調べまてみました。

「react-native-google-speech-api\android\src\main\java\com\reactlibrary\voice_recorder」フォルダに「VoiceRecorder.java」というスクリプトがあって、その中でVoiceRecorderコンポーネントが定義されています。そこに、「SPEECH_TIMEOUT_MILLIS」という定数が定義されていて、値は2000mSecになっています。

当初、この値で「発話の区切り」を仕切っているのかと思ったのですが、そうではありませんでした。

「SPEECH_TIMEOUT_MILLIS」によるタイムアウト判定は、認識処理に音声の「送り込み」を一時停止する処理で、再び声を出すと「送り込み」は再開されます。

「認識完了」を判断しているのは、クラウド側からのレスポンスのようです。認識結果で「isFinal」になると認識処理を終了させています。(「isFinal」をtrueに設定している個所はさるでは見つけられませんでした。)
クラウドでは、発話中もその時点の変換結果をレスポンスしてきます。発話が終了してから1秒くらいで「isFinal」付きでレスポンスして来ます。この「1秒くらい」を動作パラメタ設定として、クラウド側に指定できないのか探しましたが、見つけられませんでした。(知ってる方いたら教えてください。)なお、ずーっと発話し続けた状態では「isFinal」が発生しないのかと思いきや、適当なところで切られます。判定条件は不明です。

オリジナルのVoiceRecorderコンポーネントの処理では、音声認識を開始してずっと無音状態だと処理を抜ける術がありません。また、雑音のみが入っていてクラウド側が「isFinal」になる条件にならない場合、「音声認識」は完了しません。
これって、問題ありますよね。

では、どう判定して処理を終了させるか、まずは仕様を考えます。

・無音が1秒を超えたら、音声の送り込みを一時停止する。(現状仕様-ただし2秒→1秒)
・無音がn秒を超えたら、RecignizedErrorとしてその回の認識処理を停止する。
・「isFinal」を受けても、RecignizedErrorにならない限り、認識動作を繰り返して、テキストは連結して表示する。
・ただし、雑音が入り続ける環境を考慮して、有音(認識開始)から30秒を認識の最大時間とする。(現状仕様)


スクリプトに加えた変更は以下の感じです。
----VoiceEvent.java--------------------------------
【変更前】

public static VoiceEvent voice(byte[] data, int size) {
return new VoiceEvent(State.VOICE, data, size);
}

public static VoiceEvent end() {
return new VoiceEvent(State.END, null, 0);
}

private VoiceEvent(State state, byte[] data, int size) {


public int getSize() {
return size;
}

@Override
public String toString() {

---------------------------------------------------
【変更後】

public static VoiceEvent voice(byte[] data, int size) {
return new VoiceEvent(State.VOICE, data, size);
}

public static VoiceEvent end(int reason) {
//reason=0:requested stop, 1:for error handling
return new VoiceEvent(State.END, null, reason);
}

private VoiceEvent(State state, byte[] data, int size) {


public int getSize() {
return size;
}

public int getReason() {
//When event type is 'end', the 'size' is the reason code.
return size;
}


@Override

---------------------------------------------------


----VoiceRecorder.java-----------------------------
【変更前】
package com.reactlibrary.voice_recorder;

import android.media.AudioFormat;

private static final int[] SAMPLE_RATE_CANDIDATES = new int[]{16000, 11025, 22050, 44100};
private static final int SPEECH_TIMEOUT_MILLIS = 2000;
private static final int MAX_SPEECH_LENGTH_MILLIS = 30 * 1000;
private static final int DEFAULT_SAMPLE_RATE = 16000;

private long voiceStartedMillis;

public void start() {
// Stop recording if it is currently ongoing.
stop();
// Try to create a new recording session.
audioRecord = createAudioRecord();

public void dismiss() {
if (lastVoiceHeardMillis != Long.MAX_VALUE) {
lastVoiceHeardMillis = Long.MAX_VALUE;
voiceEventPublishSubject.onNext(VoiceEvent.end());
}
}


private Observable createVoiceEventObservable() {
return Observable.create(emitter -> {
while (!emitter.isDisposed()) {
final int size = audioRecord.read(buffer, 0, buffer.length);
final long now = System.currentTimeMillis();
if (isHearingVoice(buffer, size)) {
if (lastVoiceHeardMillis == Long.MAX_VALUE) {
voiceStartedMillis = now;
emitter.onNext(VoiceEvent.start());
}
emitter.onNext(VoiceEvent.voice(buffer, size));
lastVoiceHeardMillis = now;
if (now - voiceStartedMillis > MAX_SPEECH_LENGTH_MILLIS) {
lastVoiceHeardMillis = Long.MAX_VALUE;
emitter.onNext(VoiceEvent.end());
}
} else if (lastVoiceHeardMillis != Long.MAX_VALUE) {
emitter.onNext(VoiceEvent.voice(buffer, size));
if (now - lastVoiceHeardMillis > SPEECH_TIMEOUT_MILLIS) {
lastVoiceHeardMillis = Long.MAX_VALUE;
emitter.onNext(VoiceEvent.end());
}
}

}
});
}
:
---------------------------------------------------
【変更後】
package com.reactlibrary.voice_recorder;

import android.util.Log;
import android.media.AudioFormat;

private static final String TAG = "VoiceRecorder";
private static final int[] SAMPLE_RATE_CANDIDATES = new int[]{16000, 11025, 22050, 44100};
private static final int SPEECH_TIMEOUT_MILLIS = 1000;
private static final int MAX_SPEECH_LENGTH_MILLIS = 30 * 1000;
private static final int DEFAULT_SAMPLE_RATE = 16000;
private int maxVoiceBlank = SPEECH_TIMEOUT_MILLIS;
private boolean fVoiceless = false;

private long voiceStartedMillis = Long.MAX_VALUE;

public void start(int maxVoiceBlank) {
// Stop recording if it is currently ongoing.
stop();
if (maxVoiceBlank > SPEECH_TIMEOUT_MILLIS) {
this.maxVoiceBlank = maxVoiceBlank;
}

// Try to create a new recording session.
audioRecord = createAudioRecord();

public void dismiss() {
if (lastVoiceHeardMillis != Long.MAX_VALUE) {
voiceStartedMillis = Long.MAX_VALUE;
lastVoiceHeardMillis = Long.MAX_VALUE;
voiceEventPublishSubject.onNext(VoiceEvent.end(0));
}
}


private Observable createVoiceEventObservable() {
return Observable.create(emitter -> {
while (!emitter.isDisposed()) {
final int size = audioRecord.read(buffer, 0, buffer.length);
final long now = System.currentTimeMillis();
if (isHearingVoice(buffer, size)) {
if (voiceStartedMillis == Long.MAX_VALUE) {
voiceStartedMillis = now;
emitter.onNext(VoiceEvent.start());
}
Log.i(TAG, "input voice("+size+").");
emitter.onNext(VoiceEvent.voice(buffer, size));
lastVoiceHeardMillis = now;
fVoiceless = false;
if (now - voiceStartedMillis > MAX_SPEECH_LENGTH_MILLIS) {
Log.i(TAG, "reached the limit of long speech("+MAX_SPEECH_LENGTH_MILLIS+")!!!");
voiceStartedMillis = Long.MAX_VALUE;

lastVoiceHeardMillis = Long.MAX_VALUE;
emitter.onNext(VoiceEvent.end(1));
}
} else if (lastVoiceHeardMillis == Long.MAX_VALUE) {
lastVoiceHeardMillis = now;
} else {
if (voiceStartedMillis != Long.MAX_VALUE && !fVoiceless) {
emitter.onNext(VoiceEvent.voice(buffer, size));
}
if (now - lastVoiceHeardMillis > maxVoiceBlank) {
Log.i(TAG, "max voice blank("+maxVoiceBlank+")!!!");
voiceStartedMillis = Long.MAX_VALUE;
lastVoiceHeardMillis = Long.MAX_VALUE;
fVoiceless = false;
emitter.onNext(VoiceEvent.end(1));
}
else if (now - lastVoiceHeardMillis > SPEECH_TIMEOUT_MILLIS && !fVoiceless) {
Log.i(TAG, "found silence("+SPEECH_TIMEOUT_MILLIS+")!!!");
fVoiceless = true;
emitter.onNext(VoiceEvent.end(0));
}
}

}
});
}

---------------------------------------------------

----GoogleSpeechApiModule.java---------------------
【変更前】

private static final String KEY_IS_FINAL = "isFinal";
private static final String KEY_MESSAGE = "message";

private CompositeDisposable compositeDisposable;
private String apiKey;
private String langCode;

private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {


compositeDisposable.add(
voiceRecorder.getVoiceErrorEventObservable()
.doAfterNext(event -> stop())
.subscribe(GoogleSpeechApiModule.this::handleErrorEvent)
);

voiceRecorder.start();
}

};


@ReactMethod
public void start() {
Log.i(TAG, "start");
if (apiKey == null) {
sendJSErrorEvent("call setApiKey() with valid access token before calling start()");
}

} else {
sendJSErrorEvent("Another instance of SpeechService is already running");
}
}

@ReactMethod
public void setLangCode(String langCode) {
Log.i(TAG, "setLangCode");
this.langCode = langCode;
}



private void handleVoiceEvent(VoiceEvent event) {
switch (event.getState()) {

case END:
onVoiceEnd();
break;
}
}


private void onVoiceEnd() {
Log.i(TAG, "onVoiceEnd");
if (speechService != null) {
speechService.finishRecognizing();
}
}

private void handleSpeechEvent(SpeechEvent speechEvent) {

sendJSEvent(getReactApplicationContext(), ON_SPEECH_RECOGNIZED, params);
if (speechEvent.isFinal()) {
stop();
}

}

private void handleErrorEvent(Throwable throwable) {
sendJSErrorEvent(throwable.getMessage());
}

private void sendJSErrorEvent(String message){
WritableMap params = Arguments.createMap();
params.putString(KEY_MESSAGE, message);
sendJSEvent(getReactApplicationContext(), ON_SPEECH_RECOGNIZED_ERROR, params);
}

---------------------------------------------------
【変更後】

private static final String KEY_IS_FINAL = "isFinal";
private static final String KEY_REASON_CODE = "reasonCode";
private static final String KEY_MESSAGE = "message";

private CompositeDisposable compositeDisposable;
private String apiKey;
private String langCode;
private int maxVoiceBlank;

private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {


compositeDisposable.add(
voiceRecorder.getVoiceErrorEventObservable()
.doAfterNext(event -> stop())
.subscribe(GoogleSpeechApiModule.this::handleErrorEvent)
);

voiceRecorder.start(maxVoiceBlank);
}

};


@ReactMethod
public void start() {
Log.i(TAG, "start");
if (apiKey == null) {
sendJSErrorEvent("call setApiKey() with valid access token before calling start()", 0);
}

} else {
sendJSErrorEvent("Another instance of SpeechService is already running", 0);
}
}

@ReactMethod
public void setLangCode(String langCode) {
Log.i(TAG, "setLangCode");
this.langCode = langCode;
}


@ReactMethod
public void setSpeechMaxBlank(int speechMaxBlank) {
Log.i(TAG, "speechMaxBlank");
this.speechMaxBlank = speechMaxBlank;
}


private void handleVoiceEvent(VoiceEvent event) {
switch (event.getState()) {

case END:
onVoiceEnd(event.getReason());
break;
}
}


private void onVoiceEnd(int reason) {
Log.i(TAG, "onVoiceEnd("+reason+")");
if (speechService != null) {
speechService.finishRecognizing();
if (reason == 1) {
stop();
sendJSErrorEvent("Did not input the voice for limited term.", 1);
}

}
}

private void handleSpeechEvent(SpeechEvent speechEvent) {

//To have possibilities to call start() in a place that perform the ON_SPEECH_RECOGNIZED event,
//I changed the sequence of throw it.
//But, I think that it is proper not to carry out a stop-and-restart.
if (speechEvent.isFinal()) {
stop();
}
sendJSEvent(getReactApplicationContext(), ON_SPEECH_RECOGNIZED, params);

}

//In serviceConnection, be set to execute stop().
private void handleErrorEvent(Throwable throwable) {
sendJSErrorEvent(throwable.getMessage(), 0);
}

private void sendJSErrorEvent(String message, int reasonCode){
WritableMap params = Arguments.createMap();
params.putString(KEY_MESSAGE, message);
params.putInt(KEY_REASON_CODE, reasonCode);
sendJSEvent(getReactApplicationContext(), ON_SPEECH_RECOGNIZED_ERROR, params);
}

---------------------------------------------------

----Speech.js--------------------------------------
【変更前】

constructor(props) {
super(props);
this.state = {
currentText: "",
previousTexts: "",
button: "",
btn_state: 0,

};
this.state.button = Btn_cap[this.state.btn_state];
}

componentDidMount(){
GoogleSpeechApi.setLangCode("ja_JP");
GoogleSpeechApi.setApiKey("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
EventEmitter.addListener('onSpeechRecognized', (event) => {
var previousTexts = this.state.previousTexts;
var currentText = event['text'];
this.state.btn_state = 1;
if (event['isFinal']){
currentText = "";
previousTexts = event['text'] + "\n" + previousTexts;
this.state.btn_state = 0;

}

this.setState({
currentText: currentText,
previousTexts: previousTexts,
button: Btn_cap[this.state.btn_state]
});
});

EventEmitter.addListener('onSpeechRecognizedError', (error) => {
this.state.btn_state = 0;
this.setState({
button: Btn_cap[this.state.btn_state]
});
Alert.alert(
"Error occured",
error['message']
);

});


render() {

<View style={{ margin: 20 }}>
<Button
title={this.state.button}
onPress={Platform.OS === 'ios' ? this.startListening : this.requestAudioPermission}/>
<Text>{this.state.currentText}</Text>
<Text>{this.state.previousTexts}</Text>
</View>

}

---------------------------------------------------
【変更後】

constructor(props) {
super(props);
this.state = {
currentText: "",
currentSentence: "",
previousTexts: "",
button: "",
btn_state: 0,

};
this.state.button = Btn_cap[this.state.btn_state];
}

componentDidMount(){
GoogleSpeechApi.setMaxVoiceBlank(7000);
GoogleSpeechApi.setLangCode("ja_JP");
GoogleSpeechApi.setApiKey("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
EventEmitter.addListener('onSpeechRecognized', (event) => {
var previousTexts = this.state.previousTexts;
var currentSentence = this.state.currentSentence;
var currentText = event['text'];
if (event['isFinal']){
currentSentence = currentSentence + currentText + ". ";
currentText = "";
if (this.state.btn_state != 0) {
GoogleSpeechApi.start();
}
else {
previousTexts = currentSentence + "\n" + previousTexts;
currentSentence = "";
}

}

this.setState({
currentText: currentText,
currentSentence: currentSentence,
previousTexts: previousTexts,
button: Btn_cap[this.state.btn_state]
});
});

EventEmitter.addListener('onSpeechRecognizedError', (error) => {
var previousTexts = this.state.previousTexts;
var currentSentence = this.state.currentSentence;
if (error['reasonCode'] != 1) {
Alert.alert("Error occured", error['message']);
}
else {
previousTexts = currentSentence + "\n" + previousTexts;
currentSentence = "";
}

this.state.btn_state = 0;
this.setState({
currentSentence: currentSentence,
previousTexts: previousTexts,

button: Btn_cap[this.state.btn_state]
});


render() {

<View style={{ margin: 20 }}>
<Button
title={this.state.button}
onPress={Platform.OS === 'ios' ? this.startListening : this.requestAudioPermission}/>
<Text>{this.state.currentSentence+this.state.currentText}</Text>
<Text>{this.state.previousTexts}</Text>
</View>

}

---------------------------------------------------



結果、こんな見た目になりました。
20200307_1.jpg


◆残・問題/現象
これらのスクリプト変更と動作確認をやっている間に発生したいくつかの制限/問題について以下に備忘しておきます。


(1)連続で話しかけてもGoogleクラウドがテキストを返して来なくなる
原因不明です。マイクから入る音質:感度/音量の問題ではないかと。

(2)react-native-google-speech-api配下のスクリプトにデバッガで入り込めない?
さるの環境では、プロジェクトフォルダ直下のSpeech.js内にブレークポイントを設定した場合には、ブレークできます。
でもreact-native-google-speech-apiの下のソースでは、止まりません。
よって、ログを仕込んで動きを観察するしかありませんでした。
Log.i(・・・)のメッセージを見るためには、
-コマンドプロンプトを起動し、
-カレントをAndroidのSDKフォルダ下のplatform-toolsに変更して、
-「adb logcat GoogleSpeechApi:i VoiceRecorder:i SpeechService:i *:s」と打ちます。

(3)パッケージのスクリプトに変更を加えた場合ReloadではBuildし直しされない?
通常、プロジェクト直下のソースに変更を加えた場合、エミュレータ上のReloadを行うと、その変更は反映されてリロードされます。
でも、パッケージ(react-native-google-speech-api配下)のソースに変更を加えた場合、一旦デバッグを終了させて、再度「react-native run-android」しないとダメそうでした。

(4)テザリング接続では、Google Cloudへの接続が許可されない?
Wifi環境でタブレット上で動作させてテストしてたのですが、あるときスマフォ経由でテザリングしてGoogleクラウドに接続しようとしたのですが、APIが「UNAVAILABLE」とされてテストできませんでした。Googleクラウド側の設定を眺めてみたのですが、やっぱりわけが分かりませんでした。

あそうそう、もう一つ、AndroidエミュレータのMicrophone設定は、エミュレータを立ち上げ直したら毎回やんないといけないのよ。


次回は(1)の認識(テキスト変換)自体の精度が何とかならないのか? もんもんした話をするかと思います。


では、この辺で。ごきげんよう。
(m__)m





スポンサーサイト





コメントの投稿

非公開コメント

カレンダー
01 | 2024/02 | 03
- - - - 1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 - -
プロフィール

さるもすなる

Author:さるもすなる
さるです。別HPサイト「さるもすなる」から侵食してきました。 山菜/きのこ、それとタイトルにしたPPバンド籠のことをメインに徒然に・・・・暇を持て余したさるの手仕事:男手芸のブログってことで。

最新記事
最新コメント
月別アーカイブ
カテゴリ
天気予報

-天気予報コム- -FC2-
本家のHPのトップ
山菜や茸の話です
PPバンドの籠作品と作り方です
投稿をお待ちしております



PVアクセスランキング にほんブログ村 にほんブログ村 ハンドメイドブログへ



マニュアルのお申し込み



検索フォーム
リンク
RSSリンクの表示
ブロとも申請フォーム

この人とブロともになる

QRコード
QR