kako.dev

開発、自作アプリのこと

Androidで手書きの文字を解析して音声再生する

プロローグ

個人開発しているアプリで、キャンバスに書かれた文字を解析して音声再生する機能をリリースしました。 実装した記録をブログとしてまとめます。

構成

全体の構成はこんな感じ。 「ひつだん」は個人開発したアプリの名称です。 FirebaseとGoogleCloudVisionを利用します。

サーバーサイドの処理にはFirebase Functionsを利用します。 f:id:h3-birth:20210828115531j:plain

実装ステップ

  1. CanvasをBitmapにする
  2. Google Cloud Visionで文字認識する
  3. TextToSpeechで音声再生する

1. CanvasをBitmapにする

「ひつだん」は画面全体をCanvasにしているのでスクリーンサイズを取得してまるごとBitmapを生成します。

// >= Build.VERSION_CODES.R

 val metrics= DisplayMetrics()
activity.display?.getRealMetrics(metrics) 

val bitmap = Bitmap.createBitmap(metrics.widthPixels, metrics.heightPixels, Bitmap.Config.ARGB_8888)

このままだと最近の端末はサイズが大きいので、圧縮します。

val maxDimension = 1280

val originalWidth = bitmap.width                                                            
val originalHeight = bitmap.height                                                          
var resizedWidth = maxDimension                                                             
var resizedHeight = maxDimension                                                            
if (originalHeight > originalWidth) {                                                       
    resizedHeight = maxDimension                                                            
    resizedWidth =                                                                          
            (resizedHeight * originalWidth.toFloat() / originalHeight.toFloat()).toInt()    
} else if (originalWidth > originalHeight) {                                                
    resizedWidth = maxDimension                                                             
    resizedHeight =                                                                         
            (resizedWidth * originalHeight.toFloat() / originalWidth.toFloat()).toInt()     
} else if (originalHeight == originalWidth) {                                               
    resizedHeight = maxDimension                                                            
    resizedWidth = maxDimension                                                             
}                                                                                           

val resizeBitmap = Bitmap.createScaledBitmap(bitmap, resizedWidth, resizedHeight, false)                

Cloud Visionにわたすためにbase64化します

val byteArrayOutputStream = ByteArrayOutputStream()                       
resizeBitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)    
val imageBytes: ByteArray = byteArrayOutputStream.toByteArray()           
val base64 = Base64.getEncoder().encodeToString(imageBytes)                     

2. Google Cloud Visionで文字認識する

サーバーサイドの処理を書きます。 今回はFirebase Functionsを利用したので、以下のように実装してます。

import * as functions from "firebase-functions";
import vision from "@google-cloud/vision";

const client = new vision.ImageAnnotatorClient();

export const annotateImage = functions.region(YOUR_REGION).https.onCall(async (data, context) => {
  try {
    return await client.annotateImage(JSON.parse(data));
  } catch (e) {
    throw new functions.https.HttpsError("internal", e.message, e.details);
  }
});

Vision クライアント ライブラリを利用したので簡単です。 ただしベータ版なので規約には注意が必要です。

https://cloud.google.com/vision/docs/libraries?hl=ja#client-libraries-install-nodejs

3. TextToSpeechで音声再生する

GoogleCloudVisionから返された結果をTextToSpeechで再生します。

val tts = TextToSpeech(context, this /* TextToSpeech.OnInitListener */)

tts.speak(message, TextToSpeech.QUEUE_FLUSH, null, "messageID")

tts.shutdown()

これだけでも音声再生されるのですが、実用にはもっとケアが必要です。

OnInitListenerで初期化を検知

TextToSpeech.OnInitListenerでTextToSpeechの初期化に成功しているかチェックします。 失敗した場合はstatusに入るのでエラー処理します

 override fun onInit(status: Int) {
        if(status == TextToSpeech.SUCCESS) {
            // 初期化成功
        } else if (status == TextToSpeech.ERROR_SERVICE) {
            // この端末ではTextToSpeechは提供されてません。
        } else {
            // その他のエラー処理
        }
    }

日本語で再生されるように設定

初期化に成功したら言語の設定をします。端末依存するので最低限の処理として書いてあげます。 今回は日本語利用前提なので日本語に設定しますが、設定するまえに isLanguageAvailable で利用可能かチェックします。

if(status == TextToSpeech.SUCCESS) {
    val locale = Locale.JAPAN
    if (tts.isLanguageAvailable(locale) > TextToSpeech.LANG_AVAILABLE) {
        tts.language = locale
    }
}

UtteranceProgressListenerで再生ステータスを検知

utteranceId にはspeakに渡した識別子が入ります。

tts.setOnUtteranceProgressListener(object : UtteranceProgressListener(){
                override fun onDone(utteranceId: String?) {
                    Timber.d("onDone id=$utteranceId")
                }

                override fun onError(utteranceId: String?) {
                    Timber.d("onError id=$utteranceId")
                }

                override fun onStart(utteranceId: String?) {
                    Timber.d("onStart id=$utteranceId")
                }

            })

実際はもっと細かくいろいろしてますが、とりあえず必要なのはこの辺でしょうか。

実際の挙動

Google Cloud Visionの精度は結構高いと感じました。 ただし、日本語での縦書きとなるとだいぶくるしい解析結果なので、横書きでの利用がおすすめです。

アプリこちら

今回この機能を実装したアプリはこちら。 ま、この機能は有料なんですけどね。 play.google.com