[Android] 015. 音声認識(GCP Speech-to-Text v1)

独自(マイクとか音声ファイルとか)で音声データを渡して音声認識をしたい場合を想定してGCPで試してみました
課金したくないのでv1で試しましたが、v1使うくらいならAndroid SDKを使った方がいいかなって感じです

Android向けのサンプルプロジェクトはメンテされていなくて古いのでこちらを参考にした方がよいです
https://cloud.google.com/speech-to-text/docs/streaming-recognize?hl=ja

録音の実装は013. AudioRecordで作ったものを使用しています

GCPの設定

GCPでプロジェクトを追加してCloud Speech-to-Text APIを有効にします

・APIキーの取得

APIとサービスで認証情報の認証情報の作成でAPIキーを作ります

・サービスアカウントキー取得

軽く試したいだけなのでサンプル通りサービスアカウントキーをjsonで取得します
IAMと管理のサービスアカウントでキーの鍵の追加を選択してjsonをダウンロードします
取得したjsonはcredential.jsonにリネームして”key” : “(APIキー)”を追加して
対象プロジェクトのres/rawに置きます

実装

とりあえず試すだけの最低限の実装です
発話検出とかないので手動で録音止めてください
止めないと無限に音声データ送信しつづけるので課金が怖いです

認識結果のisFinalはsend中はtrueにならないのでこのフラグを見て録音止めることもできない
課金したくないからv2は試してないけどv2ならいい感じにできるのかも

com.google.cloud:google-cloud-speech 4.43.0
io.grpc:grpc-okhttp 1.66.0
<manifest
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.INTERNET" />
android {
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
            excludes += "/META-INF/LICENSE"
            excludes += "/META-INF/INDEX.LIST"
            excludes += "/META-INF/DEPENDENCIES"
        }
    }
import android.Manifest
import com.google.cloud.speech.v1.RecognitionConfig
import com.google.cloud.speech.v1.SpeechClient
import com.google.cloud.speech.v1.SpeechSettings
import com.google.cloud.speech.v1.StreamingRecognitionConfig
import com.google.cloud.speech.v1.StreamingRecognizeRequest
import com.google.cloud.speech.v1.StreamingRecognizeResponse

class MainActivity : ComponentActivity() {  
    val locationPermissionRequest by lazy {
        registerForActivityResult(
            ActivityResultContracts.RequestMultiplePermissions()
        ) { permissions ->
            when {
                permissions.getOrDefault(Manifest.permission.RECORD_AUDIO, false) -> {}
                else -> {}
            }
        }
    }

    val recorder = AudioRecorder()  
    val speechClient by lazy {
        applicationContext.resources.openRawResource(R.raw.credential).use {
            SpeechClient.create(SpeechSettings.newBuilder()
                .setCredentialsProvider { GoogleCredentials.fromStream(it) }
                .build())
        }
    }
    var requestStream: ClientStream<StreamingRecognizeRequest?>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        locationPermissionRequest.launch(
            arrayOf(Manifest.permission.RECORD_AUDIO)
        )

        setContent {
            MaterialTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Screen(Modifier.padding(innerPadding))
                }
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        recorder.stop()
        requestStream?.closeSend()
        speechClient.shutdown()
    }

    @Composable
    fun Screen(modifier: Modifier = Modifier) {
        val context = LocalContext.current
        val isRecording by recorder.isRecording.collectAsState()
        val isFirstRequest = AtomicBoolean(true)

        Row(modifier, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
            Button(onClick = {
                if (!recorder.isRecording.value) {
                    // closeSendした後作り直さないとIllegalStateException call was half-closedになる?
                    requestStream = speechClient.streamingRecognizeCallable()?.splitCall(object : ResponseObserver<StreamingRecognizeResponse> {
                        override fun onStart(controller: StreamController?) {}
                        override fun onResponse(response: StreamingRecognizeResponse?) {
                            response?.let {
                                Log.d("SpeechClient", "onResponse: ${it.toString()}")
                                val result = it.resultsList.first()
                                val alternative = result?.alternativesList?.first()
                                Log.d("SpeechClient", "onResponse: ${alternative?.transcript}")
                            }
                        }
                        override fun onError(t: Throwable?) {}
                        override fun onComplete() {
                            Log.d("SpeechClient", "onComplete")
                        }
                    })
                    isFirstRequest.set(true)

                    recorder.start(context) { buffer, size, index ->
                        val byteString = ByteString.copyFrom(buffer, 0, size)
                        val builder = StreamingRecognizeRequest.newBuilder()
                            .setAudioContent(byteString)

                        if (isFirstRequest.getAndSet(false)) {
                            builder.streamingConfig = StreamingRecognitionConfig.newBuilder()
                                .setConfig(RecognitionConfig.newBuilder()
                                    .setLanguageCode("ja-JP")
                                    .setEncoding(RecognitionConfig.AudioEncoding.LINEAR16)
                                    .setSampleRateHertz(16000)
                                    .setAudioChannelCount(1)
                                    .build())
                                // 途中結果を受け取るかどうか
                                .setInterimResults(false)
                                // 一発話で解析を終了するかどうか
                                .setSingleUtterance(false)
                                .build()
                        }

                        requestStream?.send(builder.build())
                    }
                }
                else {
                    recorder.stop()
                    requestStream?.closeSend()
                    isFirstRequest.set(false)
                }
            }) {
                Text(if (isRecording) "stop" else "start")
            }
        }
    }
}

認識結果は以下
v1では大した内容受け取れないみたいですね

results {
  alternatives {
    transcript: "432675317314314541607317311315"
    confidence: 0.95148057
  }
  is_final: true
  result_end_time {
    seconds: 2
    nanos: 160000000
  }
  language_code: "ja-jp"
}
total_billed_time {
  seconds: 3
}
speech_event_time {
}
request_id: 5722919676389996625

今日はいい天気ですね

Android Studio Koala 2024.1.1 Patch 1 built on July 11, 2024