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