GCP上でGoogle Cloud Speech-to-Textをpythonで動かしてみた

# はじめに
**GCP上でGoogle Cloud Speech-to-Textをpythonで動かしてみた記事**です。

以下余談
趣味で[podcast](https://fumufumu-fm.github.io/fumufumufm/)[^1]を配信しているのですが、先日友人と話していて
「どこかの回の雑談で〇〇について話していたよね」
と言われたのですが鳥あたまの私は話した記憶こそあるものの、どの回で話したか全く思い出せませんでした。
**話した内容を全文検索したい…！**
と思ったので、とりあえず音声のテキスト化から始めようと思いたち、やってみました。
文字起こしサービスは
[Amazon Transcribe](https://aws.amazon.com/jp/transcribe/)
[Google Cloud Speech-to-Text](https://cloud.google.com/speech-to-text?hl=ja)
などが有名ですが、業務ではAWSを使うことが多いので、勉強のためGoogle Cloud Speech-to-Textを使ってみようと思います。


# 手順

## GCPにプロジェクト作成
Googleのアカウントがない場合は作成し、Google Cloud コンソールを開いてください。

- 上部にあるプロジェクト名を選択する
![スクリーンショット 2022-09-27 9.01.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2777668/fff0a38d-2961-622e-71bf-409da3f70654.png)

- 新しいプロジェクトを選択
- プロジェクト名を入力して作成
## Cloud Speech-to-Text APIを利用可能にする
### Cloud Speech-to-Text APIの有効化
- ナビゲーションメニューから[APIとサービス]>[ライブラリ]>[Cloud Speech-to-Text API]の順に選択する
- 有効にする
    - 支払い情報の登録が必要なので適宜行ってください
    - 初めてGCPを利用する場合、3ヶ月間、＄300分のクレジットが付与されます。
    - 今回の作業では課金対象のリソースを使用しますが、十分無料クレジットの範疇ですので実際に課金はされないはずです。

### サービスアカウントの発行
- ナビゲーションメニューから[APIとサービス]>[認証情報]>[認証情報を作成]>[サービス アカウント]の順に選択する
- サービスアカウント名を入力して続行する
- ロールを[オーナー]として続行する
    - 今回は実験なのでオーナー権限を与えてしまいます
    - 実際に運用する再には正しい権限を設定しましょう
- ユーザーへのサービス アカウントへのアクセス許可は必要ありませんので完了を選択して作成する
- サービスアカウントの一覧が表示されるので先ほど作成したアカウントを選択する
- [キー]>[鍵を追加]>[新しい鍵を作成]を選択し、キーのタイプはJSONとして作成する
- ローカルに秘密鍵が保存されます

## Cloud ShellでCloud Speech-to-Text APIを利用する準備
### Cloud Shellの準備
- 右上のアイコンを選択し、Cloud Shellをアクティブにする
![スクリーンショット 2022-09-27 9.11.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2777668/d3257e99-49ff-969c-6204-7c0589d2874f.png)
- [︙]>[アップロード]を選択し、先程ダウンロードした秘密鍵をアップロードしてください
- 下記を実行して環境変数に入れておきます
    - 適宜`.bashrc`に書いておくと楽かもしれません
```bash
$ export GOOGLE_APPLICATION_CREDENTIALS={秘密鍵の名前}.json
```
- 今回はpythonを利用するので[python用のクライアントライブラリ](https://googleapis.dev/python/speech/latest/index.html)をインストールします
```bash
$ pip install --upgrade google-cloud-speech
```


### CloudStrageの作成と音源の準備
- ナビゲーションメニューから、[ストレージ]>[CloudStrage]>[作成]を選択
    - 設定は何でもいいですが、
        - ロケーションタイプはregion
        - ストレージクラスはStandard
    - にしておきました
- 作成したら[ファイルをアップロード]で、適当な音声ファイルをアップロードします。
    - この際、encodingに注意してください
    - オーディオコーデックはFLACやLINEAR16が推奨のようです。
    - 具体的には[こちら](https://cloud.google.com/speech-to-text/docs/reference/rest/v1p1beta1/RecognitionConfig?hl=ja#audioencoding)を参照してください

## pythonスクリプトの作成
- Cloud Shell Editorを開いて適当な名前のpythonファイルを作成する
- [公式ドキュメント](https://cloud.google.com/speech-to-text/docs/sync-recognize?hl=ja#perform_synchronous_speech_recognition_on_a_remote_file)を参考にスクリプトを作成します
- 今回は簡単のため、60秒以下のwavファイル(podcastのオープニング)をテキスト化してみました。
- 最終的なコードはこちらです
~~~python:transcribe.py
def transcribe_gcs(gcs_uri):
    """Asynchronously transcribes the audio file specified by the gcs_uri."""
    from google.cloud import speech_v1p1beta1 as speech

    # Instantiates a client
    client = speech.SpeechClient()

    audio = speech.RecognitionAudio(uri=gcs_uri)
    config = speech.RecognitionConfig(
        sample_rate_hertz=44100,
        audio_channel_count=2,
        enable_word_confidence=True,
        enable_automatic_punctuation=True,
        language_code="ja-JP",
    )

    response = client.recognize(config=config, audio=audio)

    # Each result is for a consecutive portion of the audio. Iterate through
    # them to get the transcripts for the entire audio file.
    for i, result in enumerate(response.results):
        alternative = result.alternatives[0]
        print("-" * 20)
        print("First alternative of result {}".format(i))
        print(u"Transcript: {}".format(alternative.transcript))
        print(
            u"First Word and Confidence: ({}, {})".format(
                alternative.words[0].word, alternative.words[0].confidence
            )
        )

~~~

### 補足
- 関数に渡すgcs_uriはCloud Strageの該当オブジェクトのuriです。
- speechではなく、speech_v1p1beta1を使用しました。 
    - これはβ版でspeechにはない機能を使えます。
        - 例えばMP3の変換や話者分離などです。
- config
    - sample_rate_hertzはFLACやLINEAR16の場合はoptionalです
    - audio_channel_countは私の音源はステレオだったので2を指定しています。モノラル変換した場合は指定不要です。
    - enable_word_confidenceは各単語の信頼度を表示させるためにオンにしています。文章への変換のみでしたら不要です
    - enable_automatic_punctuationは句読点入力の自動化です。日本語に対応していますが、なにを試しても「。」は出ますが「、」はでませんでした。

## 実行


返却された文章
~~~text:response
"mememe このポッドキャストはエンジニアの直人同級生の性が世の中のアレコレについてふむふむする Podcast です毎回最近興味を持って学んだことについて片方が紹介し会話形式で理解を深めていきます。"
~~~

実際の文章
> ふむふむfm
このPodcastは、エンジニアのnaoheeと大学院生のshaunが世の中のアレコレについてふむふむするPodcastです。
毎回最近興味を持って学んだことについて片方が紹介し、会話形式で理解を深めていきます。

精度はどうでしょう？
まずまずよいのではないでしょうか？
番組タイトルがmememeになっていて笑ってしまいました。滑舌悪くて申し訳ない…。
下に実際のレスポンスを貼っていますが、単語単位の信頼度も間違えているところはちゃんと低いのでその面では素晴らしいです。

<details><summary>実行時の実際のレスポンス</summary>

~~~json
[alternatives {
  transcript: "mememe このポッドキャストはエンジニアの直人同級生の性が世の中のアレコレについてふむふむする Podcast です毎回最近興味を持って学んだことについて片方が紹介し会話形式で理解を深めていきます"
  confidence: 0.856433332
  words {
    word: "mememe|エムイーエムイーエムイー"
    confidence: 0.728126049
  }
  words {
    word: "この|コノ"
    confidence: 0.961492896
  }
  words {
    word: "ポッド|ポッド"
    confidence: 0.627697647
  }
  words {
    word: "キャスト|キャスト"
    confidence: 0.627697408
  }
  words {
    word: "は|ワ"
    confidence: 0.755645514
  }
  words {
    word: "エンジニア|エンジニア"
    confidence: 0.926760137
  }
  words {
    word: "の|ノ"
    confidence: 0.926755846
  }
  words {
    word: "直人|ナオト,ナオヒト"
    confidence: 0.961492896
  }
  words {
    word: "同級|ドーキュー"
    confidence: 0.763572335
  }
  words {
    word: "生|セー"
    confidence: 0.76431787
  }
  words {
    word: "の|ノ"
    confidence: 0.779162884
  }
  words {
    word: "性|サガ,ショー,セー"
    confidence: 0.763657689
  }
  words {
    word: "が|ガ"
    confidence: 0.830286145
  }
  words {
    word: "世の中|ヨノナカ"
    confidence: 0.961492896
  }
  words {
    word: "の|ノ"
    confidence: 0.961492896
  }
  words {
    word: "アレコレ|アレコレ"
    confidence: 0.925870061
  }
  words {
    word: "に|ニ"
    confidence: 0.961492896
  }
  words {
    word: "つい|ツイ"
    confidence: 0.921401501
  }
  words {
    word: "て|テ"
    confidence: 0.933282614
  }
  words {
    word: "ふむ|フム"
    confidence: 0.956477761
  }
  words {
    word: "ふむ|フム"
    confidence: 0.956477761
  }
  words {
    word: "する|スル"
    confidence: 0.850283802
  }
  words {
    word: "podcast|ポッドキャスト"
    confidence: 0.877367198
  }
  words {
    word: "です|デス"
    confidence: 0.897724271
  }
  words {
    word: "毎回|マイカイ"
    confidence: 0.961492896
  }
  words {
    word: "最近|サイキン"
    confidence: 0.961492896
  }
  words {
    word: "興味|キョーミ"
    confidence: 0.961492896
  }
  words {
    word: "を|オ"
    confidence: 0.961492896
  }
  words {
    word: "持っ|モッ"
    confidence: 0.922351837
  }
  words {
    word: "て|テ"
    confidence: 0.961492896
  }
  words {
    word: "学ん|マナン"
    confidence: 0.961492896
  }
  words {
    word: "だ|ダ"
    confidence: 0.961492896
  }
  words {
    word: "こと|コト"
    confidence: 0.961492896
  }
  words {
    word: "に|ニ"
    confidence: 0.961492896
  }
  words {
    word: "つい|ツイ"
    confidence: 0.961492896
  }
  words {
    word: "て|テ"
    confidence: 0.961492896
  }
  words {
    word: "片方|カタエ,カタカタ,カタホー"
    confidence: 0.961492896
  }
  words {
    word: "が|ガ"
    confidence: 0.961492896
  }
  words {
    word: "紹介|ショーカイ"
    confidence: 0.961492896
  }
  words {
    word: "し|シ"
    confidence: 0.961492896
  }
  words {
    word: "会話|カイワ"
    confidence: 0.961492896
  }
  words {
    word: "形式|ケーシキ"
    confidence: 0.961492896
  }
  words {
    word: "で|デ"
    confidence: 0.961492896
  }
  words {
    word: "理解|リカイ"
    confidence: 0.961492896
  }
  words {
    word: "を|オ"
    confidence: 0.961492896
  }
  words {
    word: "深め|フカメ"
    confidence: 0.961492896
  }
  words {
    word: "て|テ"
    confidence: 0.961492896
  }
  words {
    word: "いき|イキ"
    confidence: 0.961492896
  }
  words {
    word: "ます|マス"
    confidence: 0.961492896
  }
}
result_end_time {
  seconds: 25
  nanos: 950000000
}
language_code: "ja-jp"
]
~~~
</details>

コードについての注意点を下記に記します
- 音声ファイルが60秒を超える場合は通常の音声認識ではなく、long_running_recognizeメソッドを利用する必要があります。
~~~diff_python
- response = client.recognize(config=config, audio=audio)
+ operation = client.long_running_recognize(config=config, audio=audio)
+ response = operation.result(timeout=90)
~~~

- 私は自分でpodcastをアップロードしているのでwavファイルを持っていましたが、通常podcastはMP3で配信されます。MP3の場合はconfigに下記を追加する必要があります。wavの場合は指定の必要はありません。
```diff_python
    config = speech.RecognitionConfig(
        sample_rate_hertz=44100,
        audio_channel_count=2,
        enable_word_confidence=True,
        enable_automatic_punctuation=True,
        language_code="ja-JP",
        enable_word_time_offsets=True,
+       encoding=speech.RecognitionConfig.AudioEncoding.MP3,
    )
```

- 頻出の単語などは重み付けできます。[参考](https://cloud.google.com/speech-to-text/docs/speech-adaptation?hl=ja)
    - boostの値には0~20の浮動小数点が指定できます。
    - 手元で試すと20ではその単語だらけになってしまい、10でほどよいかなと言う感じでした。
        - このあたりは調整の余地がありそうです。
```diff_python
    config = speech.RecognitionConfig(
        sample_rate_hertz=44100,
        audio_channel_count=2,
        enable_word_confidence=True,
        enable_automatic_punctuation=True,
        language_code="ja-JP",
        enable_word_time_offsets=True,
+       speech_contexts=[
            {"phrases": ["shaun", "naohee", "大学院", "ふむふむfm"], "boost": 10},
        ],
    )
```

# 後片付け
実験し終わったらGCPのプロジェクト毎消してしまうとさっぱりです！

# おわりに
公式のquick startが充実しているのでとても助かりました。
現在テキスト検索結果から番組を検索する機能を作成中です。
次回はそちらの実装を紹介できればと思います。


# 参考文献
https://cloud.google.com/speech-to-text?hl=ja
https://tech-blog.optim.co.jp/entry/2020/02/21/163000
https://qiita.com/keyhole0/items/19c39ee62cf48bb5e746
https://tomoima525.hatenablog.com/entry/2020/12/10/184534


[^1]: 番組中の発言は個人の見解であって、所属組織を代表するものではありません。