はじめに
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.前回の記事まで終わらせてください。
1.前回の記事まで終わらせてください。
2.Azureポータルを開き、Translatorを作成、エンドポイントとキー、場所(リージョン)をメモっておいてください。
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(男性)がありますので、変更してみてください。
実行
実行してみた結果がこちらの動画になります。
お疲れ様でした。