【Unity(C#)】Microsoft Translatorの使い方

## はじめに

VRヘビーユーザーからすれば当たり前のことかもしれませんが、
[OculusQuest](https://www.oculus.com/quest-2/)にマイクが搭載されているのはご存じでしたでしょうか。

ボイスチャットに利用されることがほとんどで、
その他の用途で使われている事例をあまり見たことがありませんでした。
(たぶん世の中にはたくさんある)

しかし、最近目にした記事にボイスチャット以外の用途でマイクを使った事例がありました。
【参考リンク】：[Synamon、ロゼッタと「リアルタイム多言語翻訳システム装備のVRオフィス」を共同開発](https://prtimes.jp/main/html/rd/p/000000053.000025330.html)

一言で説明すると、翻訳VRアプリです。
マイクを音声認識の受け口として利用しています。

そこで、私も勉強がてら"OculusQuestのマイクを利用したVRアプリ作りに挑戦してみたい"と思い、実際に作りました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">勉強がてら作成していた翻訳VRできました！😎<br><br>OculusQuestのマイクで拾った音声を音声認識APIに渡して、認識結果を翻訳APIに渡す、、、というやり方です💪<br><br>次はマルチ対応していきます😆<a href="https://twitter.com/hashtag/OculusQuest?src=hash&amp;ref_src=twsrc%5Etfw">#OculusQuest</a> <a href="https://twitter.com/hashtag/Unity?src=hash&amp;ref_src=twsrc%5Etfw">#Unity</a> <a href="https://t.co/k95D73gEnh">pic.twitter.com/k95D73gEnh</a></p>&mdash; KENTO⚽️XRエンジニア😎Zenn100記事マラソン挑戦中29/100 (@okprogramming) <a href="https://twitter.com/okprogramming/status/1378167158694277120?ref_src=twsrc%5Etfw">April 3, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## Microsoft Translator
先ほどのアプリで利用した翻訳の部分はMicrosoft Translatorを利用しています。

登録の手順を覚えている範囲でメモします。
この手順に関してですが、**2021/04/03時点の情報**となります。
私も使い方がわからなかったため、過去に執筆された記事等手掛かりに進めてましたが、UIや手順に変更があり、そこそこ苦労しました。

この記事もそうなる可能性が高いのでご注意ください。


まずは下記からMicrosoft AzureのHome画面を開きます。

https://portal.azure.com/#home

ログインしたらHome画面上部からTranslatorを検索し、
Marketplaceの欄のTranslatorを選び、登録に進みます。
![MST登録画面2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/5b73381d-45e4-d58c-08c1-e5b7e0197b53.png)

登録画面で必要な項目を選択します。ここでPricing tierの欄をfreeにしておけば無料で使えるはずです。(たぶん)
![MST登録画面1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/40e1f9d3-6343-e011-9748-0091ef72588e.png)

設定完了したらAPIを利用する際に必要な値を下記画面から確認できます。
![MST登録画面3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/d5b25aee-3d04-6ac4-02d7-0cf68f295d24.png)

## 翻訳デモ
翻訳VRアプリ内のコードは他のAPIに置換できるよう、モジュール化しているためわかりにくいかなと思い、シンプルな翻訳デモを作りました。
![MSTranslatorSample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/8e12205e-ca38-6d61-6be5-4a7c6687e415.gif)

英語→日本語、日本語→英語が翻訳可能です。
もちろんMicrosoft Translatorが対応している言語であれば他の言語でも翻訳可能です。
【参考リンク】：[Language and region support for text and speech translation](https://docs.microsoft.com/ja-jp/azure/cognitive-services/translator/language-support)

##バージョン
念のため使ったライブラリ等のバージョンも書いときます。
>Unity 2019.4.8f1
UniTask.2.2.4
UniRx 7.1.0

## コード

翻訳デモのコードです。

```C#
using System;
using System.Linq;
using System.Text;
using System.Threading;
using Cysharp.Threading.Tasks;
using UniRx;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

/// <summary>
/// MSTranslatorの最小構成サンプル
/// </summary>
public class SimpleTranslation : MonoBehaviour
{
    [SerializeField] private Dropdown fromLanguageDd;
    [SerializeField] private Dropdown toLanguageDd;
    [SerializeField] private Button translateButton;
    [SerializeField] private InputField inputField;
    [SerializeField] private Text translationText;

    /// <summary>
    /// レスポンスを格納する構造体
    /// </summary>
    [Serializable]
    public struct TranslateData
    {
        public Translations[] translations;

        [Serializable]
        public struct Translations
        {
            public string text;
            public string to;
        }
    }

    /// <summary>
    /// リクエストを格納する構造体
    /// </summary>
    [Serializable]
    public struct SpeechData
    {
        public string Text;
    }

    private const string SUBSCRIPTION_KEY = "登録キー";
    private const string ENDPOINT = "https://api.cognitive.microsofttranslator.com/";
    private const string LOCATION = "登録時に設定したLocation";
    
    /// <summary>
    /// 設定言語
    /// </summary>
    private enum Language
    {
        ja,
        en
    }

    private Language fromLanguage = Language.ja;
    private Language toLanguage = Language.en;
    
    private void Start()
    {
        var token = this.GetCancellationTokenOnDestroy();

        //ドロップダウンメニュー作成
        var languages = Enum.GetNames(typeof(Language));
        fromLanguageDd.ClearOptions();
        fromLanguageDd.AddOptions(languages.ToList());
        toLanguageDd.ClearOptions();
        toLanguageDd.AddOptions(languages.ToList());
        fromLanguageDd.value = (int) fromLanguage;
        toLanguageDd.value = (int) toLanguage;
        
        //翻訳元言語
        fromLanguageDd.OnValueChangedAsObservable()
            .Subscribe(value => { fromLanguage = (Language) value; })
            .AddTo(this);

        //翻訳後言語
        toLanguageDd.OnValueChangedAsObservable()
            .Subscribe(value => { toLanguage = (Language) value; })
            .AddTo(this);
        
        //翻訳ボタン押下
        translateButton.OnClickAsObservable()
            .Subscribe(async _ =>
            {
                //結果が送られてくるまで待ってから表示
                var result = GetTranslation(fromLanguage, toLanguage, inputField.text, token);
                translationText.text = await result;
            })
            .AddTo(this);
    }

    /// <summary>
    /// 翻訳結果を返す
    /// </summary>
    /// <param name="from">翻訳前の言語設定</param>
    /// <param name="to">翻訳語の言語設定</param>
    /// <param name="speechText">翻訳したい文字列</param>
    /// <param name="ct">CancellationToken</param>
    /// <returns>翻訳結果</returns>
    private async UniTask<string> GetTranslation(Language from,Language to,string speechText, CancellationToken ct)
    {
        //POSTメソッドのリクエストを作成
        var requestInfo = "translate?api-version=3.0";
        requestInfo += $"&from={from}&to={to}";
        var request = UnityWebRequest.Post(ENDPOINT + requestInfo, "Post");

        //リクエストに使用するJSON作成
        var speechData = new SpeechData {Text = speechText};
        var jsonData = "[" + JsonUtility.ToJson(speechData) + "]";
        var bodyRaw = Encoding.UTF8.GetBytes(jsonData);
        request.uploadHandler = new UploadHandlerRaw(bodyRaw);
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "application/json");
        //ヘッダーに必要な情報を追加
        request.SetRequestHeader("Ocp-Apim-Subscription-Region", LOCATION);
        request.SetRequestHeader("Ocp-Apim-Subscription-Key", SUBSCRIPTION_KEY);

        //結果受け取り
        var second = TimeSpan.FromSeconds(3);
        var result = await request.SendWebRequest().ToUniTask(cancellationToken: ct).Timeout(second);
        var rawJson = result.downloadHandler.text;
        var json = rawJson.Substring(1, rawJson.Length - 2);
        var data = JsonUtility.FromJson<TranslateData>(json);
        return data.translations[0].text;
    }
}

```

APIのリクエスト用のJson、APIのレスポンス用のJsonのそれぞれをシリアライズ、デシリアライズするための構造体を用意する必要があります。

リクエスト用のJsonはルートが配列でないとAPIの都合上だめだったのでごり押ししています。
`var jsonData = "[" + JsonUtility.ToJson(speechData) + "]";`

【参考リンク】：[【Unity(C#)】WebAPIで返ってきたJSONデータの扱いでつまったところ](https://qiita.com/OKsaiyowa/items/39318efd20524ec1d4d9)


## EnumをDropDownに反映
全然関係ない内容ですが、知らなかったのでメモします。(下記参考リンクのまんまですが...)

【参考リンク】：[UnityでDropDownのOptionリストに、enumの定義値をラベルとしてスクリプトからセットする](https://alice3.net/unity/post-4058)

```C#
/// <summary>
/// 設定言語
/// </summary>
private enum Language
{
    ja,
    en
}

private Language fromLanguage = Language.ja;
private Language toLanguage = Language.en;

private void Start()
{
    //ドロップダウンメニュー作成
    var languages = Enum.GetNames(typeof(Language));
    fromLanguageDd.ClearOptions();
    fromLanguageDd.AddOptions(languages.ToList());
    toLanguageDd.ClearOptions();
    toLanguageDd.AddOptions(languages.ToList());
    fromLanguageDd.value = (int) fromLanguage;
    toLanguageDd.value = (int) toLanguage;
    
    //翻訳元言語
    fromLanguageDd.OnValueChangedAsObservable()
        .Subscribe(value => { fromLanguage = (Language) value; })
        .AddTo(this);

    //翻訳後言語
    toLanguageDd.OnValueChangedAsObservable()
        .Subscribe(value => { toLanguage = (Language) value; })
        .AddTo(this);
}
```

Enumの値を`var languages = Enum.GetNames(typeof(Language));で配列化して`DropDownの値に追加します。

あとはDropDownの値変更を監視して、変更時にenumに値を設定してあげればOKです。

## おわりに
記事内にも書いた通り、翻訳APIの箇所はモジュール化しているので他の無料で使えるAPIと比較して精度など試してみようと思います。

次は音声認識機能について書きます。

## 参考リンク
[Microsoft Translator テキスト API で、日本語を英語に翻訳するサンプル](https://qiita.com/TakeshiNickOsanai/items/a8039ba8d558f7c8a05e)
[UniTaskの使い方2020 / UniTask2020](https://speakerdeck.com/torisoup/unitask2020?slide=53)
[Quickstart: Get started with Translator](https://docs.microsoft.com/en-us/azure/cognitive-services/translator/quickstart-translator?tabs=csharp)