HoloLens2 × Azure Cognitive Services (Translator APIで英語から日本語に翻訳し音声合成)

はじめに

HoloLensアドベントカレンダー2020 の8日目の記事です。

開発環境

  • Azure
  • Computer Vision API (画像分析 API)
  • Translator API
  • Speech SDK 1.14.0
  • Unity 2019.4.1f1
  • MRTK 2.5.1
  • Windows 10 PC
  • HoloLens2

導入

1.前回の記事まで終わらせてください。

image.png

1.前回の記事まで終わらせてください。

2.Azureポータルを開き、Translatorを作成、エンドポイントとキー、場所(リージョン)をメモっておいてください。

image.png

3.Translatorのチュートリアル( C# ,  Python ,  Translate.py )を参考にTapToCaptureAnalyzeAPI.csを編集します。画像分析APIで得られた画像説明文(英語)をTranslator APIに投げ、翻訳した日本語のテキストをSSMLを用いて音声合成します。

1TapToCaptureAnalyzeAPI.cs
2using System.Collections;
3using System.Collections.Generic;
4using System.Linq;
5using System;
6using UnityEngine;
7using Microsoft.MixedReality.Toolkit.Utilities;
8using System.Threading.Tasks;
9using OpenCVForUnity.CoreModule;
10using OpenCVForUnity.UnityUtils;
11using OpenCVForUnity.ImgprocModule;
12
13// SpeechSDK 追加分ここから
14using System.IO;
15using System.Text;
16using Microsoft.CognitiveServices.Speech;
17using Microsoft.CognitiveServices.Speech.Audio;
18// SpeechSDK 追加分ここまで
19
20public class TapToCaptureAnalyzeAPI : MonoBehaviour
21{
22
23    // Translator 追加分ここから
24    private string translator_endpoint = "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=ja";
25    private string translator_subscription_key = "";
26
27    [System.Serializable]
28    public class Translator
29    {
30        public DetectedLanguage detectedLanguage;
31        public Translations[] translations;
32    }
33
34    [System.Serializable]
35    public class DetectedLanguage
36    {
37        public string language;
38        public float score;
39    }
40
41    [System.Serializable]
42    public class Translations
43    {
44        public string text;
45        public string to;
46    }
47    // Translator 追加分ここまで
48
49    // SpeechSDK 追加分ここから
50    public AudioSource audioSource;
51
52    async Task SynthesizeAudioAsync(string text) 
53    {
54        var config = SpeechConfig.FromSubscription("YourSubscriptionKey", "YourServiceRegion");
55        var synthesizer = new SpeechSynthesizer(config, null); // nullを省略するとPCのスピーカーから出力されるが、HoloLensでは出力されない。
56
57        // SSMLを使用して日本語で音声合成
58        // https://docs.microsoft.com/ja-jp/azure/cognitive-services/speech-service/get-started-text-to-speech?fbclid=IwAR2zk6s9XcOGxXzGlOb9tfrg8hw7jTozXuGEnbk-M3ZORrJFbLPZE6Y2d1k&tabs=script%2Cwindowsinstall&pivots=programming-language-csharp
59        string ssml = " " + text + " ";
60
61        // https://github.com/Azure-Samples/cognitive-services-speech-sdk/blob/master/quickstart/csharp/unity/text-to-speech/Assets/Scripts/HelloWorld.cs
62        // Starts speech synthesis, and returns after a single utterance is synthesized.
63        // using (var result = synthesizer.SpeakTextAsync(text).Result)
64        using (var result = synthesizer.SpeakSsmlAsync(ssml).Result)
65        {
66            // Checks result.
67            if (result.Reason == ResultReason.SynthesizingAudioCompleted)
68            {
69                // Native playback is not supported on Unity yet (currently only supported on Windows/Linux Desktop).
70                // Use the Unity API to play audio here as a short term solution.
71                // Native playback support will be added in the future release.
72                var sampleCount = result.AudioData.Length / 2;
73                var audioData = new float[sampleCount];
74                for (var i = 0; i < sampleCount; ++i)
75                {
76                    audioData[i] = (short)(result.AudioData[i * 2 + 1] << 8 | result.AudioData[i * 2]) / 32768.0F;
77                }
78
79                // The output audio format is 16K 16bit mono
80                var audioClip = AudioClip.Create("SynthesizedAudio", sampleCount, 1, 16000, false);
81                audioClip.SetData(audioData, 0);
82                audioSource.clip = audioClip;
83                audioSource.Play();
84
85                // newMessage = "Speech synthesis succeeded!";
86            }
87            else if (result.Reason == ResultReason.Canceled)
88            {
89                var cancellation = SpeechSynthesisCancellationDetails.FromResult(result);
90                // newMessage = $"CANCELED:\nReason=[{cancellation.Reason}]\nErrorDetails=[{cancellation.ErrorDetails}]\nDid you update the subscription info?";
91            }
92        }
93    }
94    // SpeechSDK 追加分ここまで
95
96    public GameObject quad;
97
98    [System.Serializable]
99    public class Analyze
100    {
101        public Categories[] categories;
102        public Color color;
103        public Description description;
104        public string requestId;
105        public Metadata metadata;
106    }
107
108    [System.Serializable]
109    public class Categories
110    {
111        public string name;
112        public float score;
113    }
114
115    [System.Serializable]
116    public class Color
117    {
118        public string dominantColorForeground;
119        public string dominantColorBackground;
120        public string[] dominantColors;
121        public string accentColor;
122        public bool isBwImg;
123        public bool isBWImg;
124    }
125
126    [System.Serializable]
127    public class Description
128    {
129        public string[] tags;
130        public Captions[] captions;
131    }
132
133    [System.Serializable]
134    public class Captions
135    {
136        public string text;
137        public float confidence;
138    }
139
140    [System.Serializable]
141    public class Metadata
142    {
143        public int height;
144        public int width;
145        public string format;
146    }
147
148    UnityEngine.Windows.WebCam.PhotoCapture photoCaptureObject = null;
149    Texture2D targetTexture = null;
150
151    private string endpoint = "https:///vision/v3.1/analyze";
152    private string subscription_key = "";
153    private bool waitingForCapture;
154
155    void Start(){
156        waitingForCapture = false;
157    }
158
159    public void AirTap()
160    {
161        if (waitingForCapture) return;
162        waitingForCapture = true;
163
164        Resolution cameraResolution = UnityEngine.Windows.WebCam.PhotoCapture.SupportedResolutions.OrderByDescending((res) => res.width * res.height).First();
165        targetTexture = new Texture2D(cameraResolution.width, cameraResolution.height);
166
167        // PhotoCapture オブジェクトを作成します
168        UnityEngine.Windows.WebCam.PhotoCapture.CreateAsync(false, delegate (UnityEngine.Windows.WebCam.PhotoCapture captureObject) {
169            photoCaptureObject = captureObject;
170            UnityEngine.Windows.WebCam.CameraParameters cameraParameters = new UnityEngine.Windows.WebCam.CameraParameters();
171            cameraParameters.hologramOpacity = 0.0f;
172            cameraParameters.cameraResolutionWidth = cameraResolution.width;
173            cameraParameters.cameraResolutionHeight = cameraResolution.height;
174            cameraParameters.pixelFormat = UnityEngine.Windows.WebCam.CapturePixelFormat.BGRA32;
175
176            // カメラをアクティベートします
177            photoCaptureObject.StartPhotoModeAsync(cameraParameters, delegate (UnityEngine.Windows.WebCam.PhotoCapture.PhotoCaptureResult result) {
178                // 写真を撮ります
179                photoCaptureObject.TakePhotoAsync(OnCapturedPhotoToMemoryAsync);
180            });
181        });
182    }
183
184    async void OnCapturedPhotoToMemoryAsync(UnityEngine.Windows.WebCam.PhotoCapture.PhotoCaptureResult result, UnityEngine.Windows.WebCam.PhotoCaptureFrame photoCaptureFrame)
185    {
186        // ターゲットテクスチャに RAW 画像データをコピーします
187        photoCaptureFrame.UploadImageDataToTexture(targetTexture);
188        byte[] bodyData = targetTexture.EncodeToJPG();
189
190        Response response = new Response();
191
192        try
193        {
194            string query = endpoint + "?visualFeatures=Categories,Description,Color";
195            var headers = new Dictionary<string, string>();
196            headers.Add("Ocp-Apim-Subscription-Key", subscription_key);
197            response = await Rest.PostAsync(query, bodyData, headers, -1, true);
198        }
199        catch (Exception e)
200        {
201            photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
202            return;
203        }
204
205        if (!response.Successful)
206        {
207            photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
208            return;
209        }
210
211        Debug.Log(response.ResponseCode);
212        Debug.Log(response.ResponseBody);
213        Analyze analyze = JsonUtility.FromJson(response.ResponseBody);
214        Debug.Log(analyze.description.captions[0].text);
215
216        // Translator 追加分ここから
217        try
218        {
219            var headers = new Dictionary<string, string>();
220            headers.Add("Ocp-Apim-Subscription-Key", translator_subscription_key);
221            headers.Add("Ocp-Apim-Subscription-Region", "japaneast");
222            // headers.Add("Content-type", "application/json");
223            string jsonData = "[{" + "'text'" + ":'" + analyze.description.captions[0].text + "'}]";
224            // headers.Add("X-ClientTraceId", "uuid4");
225            response = await Rest.PostAsync(translator_endpoint, jsonData, headers, -1, true);
226        }
227        catch (Exception e)
228        {
229            photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
230            return;
231        }
232
233        if (!response.Successful)
234        {
235            photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
236            return;
237        }
238
239        Debug.Log(response.ResponseCode);
240        Debug.Log(response.ResponseBody);
241        string newResponseBody = "{ \"results\": " + response.ResponseBody + "}";
242        Translator[] translator = JsonHelper.FromJson(newResponseBody);
243        Debug.Log(translator[0].translations[0].text);
244        // Translator 追加分ここまで
245
246        // SpeechSDK 追加分ここから
247        // 生成された画像説明文をSynthesizeAudioAsyncに投げる
248        // await SynthesizeAudioAsync(analyze.description.captions[0].text); // en
249        await SynthesizeAudioAsync(translator[0].translations[0].text); // jp 
250        // SpeechSDK 追加分ここまで
251
252        // OpenCVを用いて結果をて画像に書き込み
253        Mat imgMat = new Mat(targetTexture.height, targetTexture.width, CvType.CV_8UC4);
254        Utils.texture2DToMat(targetTexture, imgMat);
255        Debug.Log("imgMat.ToString() " + imgMat.ToString());
256        Imgproc.putText(imgMat, analyze.description.captions[0].text, new Point(10, 100), Imgproc.FONT_HERSHEY_SIMPLEX, 4.0, new Scalar(255, 255, 0, 255), 4, Imgproc.LINE_AA, false);
257        Texture2D texture = new Texture2D(imgMat.cols(), imgMat.rows(), TextureFormat.RGBA32, false);
258        Utils.matToTexture2D(imgMat, texture);
259        Renderer quadRenderer = quad.GetComponent() as Renderer;
260        quadRenderer.material.SetTexture("_MainTex", texture);
261
262        // カメラを非アクティブにします
263        photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
264    }
265
266    void OnStoppedPhotoMode(UnityEngine.Windows.WebCam.PhotoCapture.PhotoCaptureResult result)
267    {
268        // photo capture のリソースをシャットダウンします
269        photoCaptureObject.Dispose();
270        photoCaptureObject = null;
271        waitingForCapture = false;
272    }
273}

4.ソースコードの重要なところ説明していきますね。まずは、translatorsubscriptionkeyにメモしたキーをコピぺしてください。エンドポイントはそのままでOKです。クエリパラメータにapi-version=3.0と日本語への翻訳(to=ja)を指定しています。

5.MRTKのRestを用いて、前回の画像説明文の生成結果がanalyze.description.captions[0].textに格納されているので、json形式にして、Translator APIにPOSTします。ヘッダーはOcp-Apim-Subscription-Keyにtranslatorsubscriptionkey、Ocp-Apim-Subscription-Regionにメモした場所(japaneast)を入力します。

6.POSTが成功したら、次のようなレスポンスボディが返ってくるので、仕様に合わせてTranslatorクラス、DetectedLanguageクラス、Translationsクラスを作ります。

1[{"detectedLanguage":{"language":"en","score":1.0},"translations":[{"text":"部屋の人","to":"ja"}]}]

7.今回は、リストのjsonになっているので、 Face API のときみたいに、JsonHelperを用いてパースします。

1string newResponseBody = "{ \"results\": " + response.ResponseBody + "}";
2Translator[] translator = JsonHelper.FromJson(newResponseBody);

8.あとはtranslator[0].translations[0].textに日本語に翻訳されたテキストがあるので、SynthesizeAudioAsyncに投げます。ただし、前回のままだと「ArgumentException: Length of created clip must be larger than 0」エラーが発生し、言語が対応してないことがわかります。

1await SynthesizeAudioAsync(translator[0].translations[0].text);

9.そこで、 SSML を使用して音声の特徴をカスタマイズする を参考に日本語で音声合成します。SSMLを作成し、日本語、男性、標準音声のja-JP-Ichiroを指定します。サポートしている言語の一覧は こちら です。

1string ssml = " " + text + " ";

10.synthesizer.SpeakTextAsync(text)をsynthesizer.SpeakSsmlAsync(ssml)に変更することで、ssmlを読み込み、指定した音声で合成することができます。

11.今回、標準音声を用いていますが、 ニューラル音声 の方がより人間らしく合成できるみたいです。使用したい場合は、Azureの音声リソースの場所を”米国東部”、”東南アジア”、”西ヨーロッパ”のいずれかにしておく必要があるようです。

12.日本語の標準音声は、ja-JP-Ayumi(女性)、ja-JP-HarukaRUS(女性)、ja-JP-Ichiro(男性)、ニューラル音声は、ja-JP-NanamiNeural(女性)、ja-JP-KeitaNeural(男性)がありますので、変更してみてください。

実行

実行してみた結果がこちらの動画になります。

お疲れ様でした。

この記事を書いた人

azure-recipe-user