SPAJAM 2022 決勝戦で `Unity (C#)` で `Speech-to-Text API` と `DeepL API` を使ったので紹介するよ


この記事は **フリュー Advent Calendar 2022** の3日目の記事となります。


# 紹介の前に
2022/11/19 〜 20 に、SPAJAM 2022 の本選が開催されました。
その本選にて、めでたく **優秀賞** を受賞しました！！🎉

残念ながら最優秀賞は逃しましたが、初めての本選参加はとても楽しめました♨️

今回は、その開発の中で使った技術周りの紹介をしたいと思います。
（Unity 初心者なので、専門用語とか間違ってても許してください）


https://www.spajam.jp/

[2022年 本選結果](https://www.spajam.jp/result/)

## アプリの紹介
- アプリ名: PiCCOM (読み方: ピッコム)
- 使用技術:
  - Unity (C#): 2021.3.2f1 (SILICON)
  - Google Cloud Speech-to-Text API
  - Speech Recognition using Google Cloud Pro
  - DeepL API


# Google Cloud Speech-to-Text API
## 概要
Google が提供するAPIで、「音声データをテキストデータに変換する」ことができます。

https://cloud.google.com/speech-to-text/

https://cloud.google.com/speech-to-text/docs/apis

## 準備
Google Cloud Console で、 `Speech-to-Text API` の利用できるように、サービスの有効化を行います。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172730/41f67f99-2f41-54e2-0438-f8ec025cf9a4.png">

次に、APIを利用するための認証情報を登録します。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172730/b0bd5561-1767-6f3d-05f1-f15f94cc27e5.png">
（誤字ってますね、今更なので気にしない）


## 使い方
今回は、ハッカソンという時間が限られた状況、且つまだまだ実装しなければならいものがあったので、時間をお金で買う決断をしました。
というのも、本来であれば、諸々を時前実装すべきなのですが、ライブラリ周りを探していたところ **Speech Recognition using Google Cloud Pro** を見つけました。

なので、今回は実装方法については `Speech Recognition using Google Cloud Pro` を使う方法での説明を次項でやりたいと思います。
とはいえ、今回のアプリを使う中で、調べた情報は追記しておこうと思います。

### APIを駆使（？）する方法
#### 多言語化対応
多言語化対応というと少し大袈裟ですが「テキストへ変換したい音声データの言語の候補（代替言語）を複数指定する」ことができます。
ただし **最大3つまで** という制限はありますが「本来の変換対象の言語」の指定と合わせれば **最大4つ** までの指定が可能にはなりそうです。

指定する方法は [RecognitionConfig](https://cloud.google.com/speech-to-text/docs/reference/rest/v1/RecognitionConfig) を使い、必要な項目を設定します。

| Field                     |                         | 説明 |
|:--------------------------|:------------------------|:------------|
| languageCode              | String (required)       | 変換対象とする音声データの言語コード。 (例: `"en-US"`) |
| alternativeLanguageCodes  | String の配列 (optional) | 変換対象とする音声データの代替言語コード。この項目が設定されている場合は、認識結果には `languageCode` を含め、検出された最も可能性が高い言語での認識が含まれ、認識結果には検出された言語タグが含まれます。 |

**言語コード** は、共に「[BCP-47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt) に対応した言語タグである」必要があります。
Speech-to-Text API としての [サポート状況](https://cloud.google.com/speech-to-text/docs/languages) は確認して利用してください。


# Speech Recognition using Google Cloud Pro
## 概要
Google Cloud サービスで提供される Speech-to-Text API を利用して、音声をテキストに変換する機能を提供する。

https://assetstore.unity.com/packages/add-ons/machinelearning/speech-recognition-using-google-cloud-vr-ar-mobile-desktop-pro-72625


## 準備
`Speech Recognition using Google Cloud Pro` を上記、Unity　Asset Store から購入し、該当プロジェクトにインポートします。
インポートしたあとは `Speech-to-Text API` を利用するために取得した、認証コードを `Api Key` に設定します。

この時の注意点として「`GC Recognitionnition` を使いたいスクリプトなどに `Component` として追加」すると、1回目の実行はうまくいうものの、2度目以降の実行がうまくいかなくなります。

:::note warn
アプリ起動かビルドタイミングでエラーが発生します。
発生する内容は、メモってません！！
エラーの内容的には「`GCSpeechRecognition` の初期化に失敗しました」のようなエラーが出ます。
:::

回避策としては `GCSpeechRecognition` の Prefab を `Scene` に直接配置して、共通参照できる状態にしてあげることです。
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172730/63f1fee6-cb1e-dd8b-025b-06d570ef868f.png">


## 使い方
とりあえず、下のコードを参考にしてもらえばよいかなと思います。
（`C#` 特有？の改行はしていません！！！）

音声データの録音から、 `Speech-to-Text API` へのデータ送信など全てをやってくれるので、かなり実装が楽にできるようになります。


```c#
using FrostweepGames.Plugins.GoogleCloud.SpeechRecognition;


public class SpeechToTextText: MonoBehaviour {

	[SerializeField]
	private Button recordButton = null;
    
    // MEMO: `GCSpeechRecognition` を `Scene` に追加するときは、Prefabを配置すること
    private GCSpeechRecognition _speechRecognition;

    private void Awake() {
        _speechRecognition = GCSpeechRecognition.Instance;

        _speechRecognition.FinishedRecordEvent += OnFinishedRecordEvent;
        _speechRecognition.RecognizeSuccessEvent += OnRecognizeSuccessEvent;

        if (_speechRecognition.HasConnectedMicrophoneDevices()) {
            _speechRecognition.SetMicrophoneDevice(_speechRecognition.GetMicrophoneDevices()[0]);
        }

        if (recordButton != null) {
            // ボタンを押している最中は録音する
            recordButton.OnPointerDownAsObservable()
                .Subscribe(_ => StartRecord())
                .AddTo(this);

            // ボタンを離した段階で録音を終了する
            recordButton.OnPointerUpAsObservable()
                .Subscribe(_ => StopRecord())
                .AddTo(this);
        }
    }

    private void OnDestroy() {
        _speechRecognition.FinishedRecordEvent -= OnFinishedRecordEvent;
        _speechRecognition.RecognizeSuccessEvent -= OnRecognizeSuccessEvent;
    }

    private void StartRecord() {
        _speechRecognition.StartRecord(false);
    }

    private void StopRecord() {
        _speechRecognition.StopRecord();
    }

    private void OnFinishedRecordEvent(AudioClip clip, float[] raw) {
        if (clip == null) return;

        RecognitionConfig config = RecognitionConfig.GetDefault();
        // メインの言語コードを設定
        config.languageCode = Enumerators.LanguageCode.en_US.Parse();
        // メインの言語コード以外の、代替言語コードを設定
        List<string> autoLangCodes = new List<string>() {
             Enumerators.LanguageCode.ja_JP.Parse(),
             Enumerators.LanguageCode.en_GB.Parse(),
             "zh",  // `Enumerators.LanguageCode` に定義が無いのでも、 `Speech-to-Text API` 側がサポートしていれば指定は可能
         };
        config.alternativeLanguageCodes = autoLangCodes.ToArray();

        config.audioChannelCount = clip.channels;

        GeneralRecognitionRequest recognitionRequest = new GeneralRecognitionRequest() {
            audio = new RecognitionAudioContent() {
                content = raw.ToBase64()
            },
            config = config
        };

        _speechRecognition.Recognize(recognitionRequest);
    }

    private async void OnRecognizeSuccessEvent(RecognitionResponse recognitionResponse) {
        string transcript = "";

        foreach (var result in recognitionResponse.results) {
            foreach (var alternative in result.alternatives) {
                if (recognitionResponse.results[0].alternatives[0] != alternative) {
                    transcript = alternative.transcript;
                }
            }
        }
    }
}
```

こんな感じです。

実装などについては、以下の記事を参考にさせていただきました。

https://qiita.com/OKsaiyowa/items/8da381e0c247dc5594fa


# DeepL API
## 概要
DeepL SE が提供するAPIで、「テキストデータの翻訳する」ことができます。

ちなみに、読み方は「ディープエル」です。
（実は、ディープルって呼んでたんですよねー・・・）


https://www.deepl.com/

https://www.deepl.com/docs-api


## 準備
DeepL API を使うにはアカウント登録が必要です。
現時点では[プラン(開発者向け)](https://www.deepl.com/ja/pro#developer)は現時点では、2つあります。
- DEEPL API FREE
- DEEPL API Pro

フリープランでも「**1か月に500,000文字まで翻訳** (2022/12/03 時点)」可能なので、お試しするなら、フリープランで十分です。

登録が完了したら、マイページを確認します。
マイページの「アカウント」タブを開き、下の方にスクロールすると **DeepL APIで使用する認証キー** があるので、それをコピーしてください。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172730/b9088d2b-7d17-38a2-558d-9ddf50adfd3a.png">



## 注意点
DeepL のアカウントでログインしていると、Web版の **DeepL 翻訳** は使えません。
ログアウトしてから使うようにしましょう！！
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172730/86cc1afd-d5ce-b0fd-7a53-7d5ea5d3925c.png">

## 使い方
こちらも、 下のコードを参考にしてもらえばよいかなと思います。
（`C#` 特有？の改行はしていません！！！）

```c#
public class DeepL {
    static string APIKey = "{DeepL APIで使用する認証キー}";
    static string BaseURL = "https://api-free.deepl.com";
    static string TranslateAPI = BaseURL + "/v2/translate";

    public async UniTask<TranslateModel> TranslateAPIRequest(string text, string targetLang) {
        try {
            HttpClient client = new HttpClient();

            // リクエストヘッダーに認証キーを設定
            client.DefaultRequestHeaders.Add("Authorization", $"DeepL-Auth-Key {APIKey}");

            // DeepL API を利用するテキストデータは `UTF-8` にエンコードする必要がある
            byte[] bytesData = System.Text.Encoding.UTF8.GetBytes(text);
            Encoding utf8Enc = Encoding.GetEncoding("UTF-8");
            string utf8EncodedText = utf8Enc.GetString(bytesData);

            // `source_lang` を指定しなければ、自動認識による翻訳をしてくれる
            Dictionary<string, string> requestParameters = new Dictionary<string, string>() {
                    { "text", utf8EncodedText },
                    { "target_lang", targetLang }
            };

            HttpResponseMessage response;

            using (FormUrlEncodedContent content = new FormUrlEncodedContent(requestParameters)) {
                content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
                response = await client.PostAsync(TranslateAPI, content);
            }

            string contentString = await response.Content.ReadAsStringAsync();
            TranslateResult result = JsonConvert.DeserializeObject<TranslateResult>(contentString);

            var dic = JsonConvert.DeserializeObject<Dictionary<string, List<Dictionary<string, string>>>>(contentString);
            List<Dictionary<string, string>> translations = dic["translations"];

            TranslateModel model = result.translations[0];
            Debug.Log($"ほんやくこんにゃく({targetLang}): {model.language}, {model.text}");
            return model;

        } catch (Exception e) {
            Debug.Log("Error: \n" + e.Message);

            return null;
        }
    }
}

public class TranslateResult {
    [JsonProperty("translations")]
    public List<TranslateModel> translations { get; set; }
}

public class TranslateModel {
    /// 変換元のテキストデータの言語コード
    [JsonProperty("detected_source_language")]
    public string language { get; set; }
    /// 変換後のテキストデータ
    [JsonProperty("text")]
    public string text { get; set; }
}
```

あとは、呼び出すだけですね。
細かいところは省いています。

```c#
private DeepL deepL = new DeepL();

// テキストデータを、指定した言語コードに翻訳する
// 言語コードは `DeepL API` 側で対応されているもに限る
// See: https://www.deepl.com/ja/docs-api/translate-text/translate-text/
TranslateModel translated = await deepL.TranslateAPIRequest(transcript, "JA");

translated.language
translated.text
```

`DeepL API` のレスポンスには「翻訳前の言語コード」が含まれているのがとても助かります。

アプリ利用開始タイミングでテキスト入力をしてもらい、その結果をもとに、自動でロケールの変更やUIの変更などにもつかえるかもですねー・・・


# さいごに
- `Speech-to-Text API` を使ってみた感想としては、日本語メインで使いましたが「音として」は認識していますが「意図する単語にならない」ことは多々ありましたが、意味は通じそうだなという感じでした
  - ただし、それを「翻訳」する場合は意味が変わってくるので使い方次第かもしれないです
- 端末にはよりますが、なかなか認識してくれない時もありました（滑舌の問題かもしれませんけど）
  - ゆっくりはっきり発音すれば、拾ってくれたので、やっぱり滑舌のせいですね・・・
- `DeepL API` をあえて使ってみましたが、翻訳精度は高かったですね
  - ちなみに、同じテキストをAPI版とWeb版で試しましたが、同じ結果返ってきました！！（当たり前ですけど、一回は検証しますよね、エンジニアなら）
- 今回のアプリとしては本来は翻訳後のテキストデータを `Google Cloud Natural Language API` を通して、テキスト分析も行いたかったんですが、これについてはまたいつか試したいと思います
- あと `Unity(2021.3.2f1)` と古いバージョンで、しかも `SILICON` 版を使ったのですが、`Unity Hub` を通して使う裏技的なものも見つけた（？）ので、別の記事で書こうと思います
  - Unity 初心者ですので、今回はUI部分はノータッチだったので、勉強していってもよさそうだなとは思いました


# 参考文献まとめ

**Google Cloud Speech-to-Text API**

https://cloud.google.com/speech-to-text/

https://cloud.google.com/speech-to-text/docs/apis


**Speech Recognition using Google Cloud [VR\AR\Mobile\Desktop] Pro**

https://assetstore.unity.com/packages/add-ons/machinelearning/speech-recognition-using-google-cloud-vr-ar-mobile-desktop-pro-72625


**【Unity(C#)】Google Cloud Speech-to-TextをUnityで使用できるGoogle Cloud Speech Recognitionの使い方**

https://qiita.com/OKsaiyowa/items/8da381e0c247dc5594fa


**DeepL API**

https://www.deepl.com/docs-api


**Google Cloud Natural Language API**

https://cloud.google.com/natural-language/