https://a.storyblok.com/f/270183/28768/433de77401/blog_translate_phone-call_1200x600.png

Blazor、SignalR、Azureで電話を翻訳する

最終更新日 November 9, 2020

所要時間:7 分

機械学習モデルによって、私たちはあらゆる種類のすてきなことができるようになる。例えば、リアルタイムの音声翻訳だ。このチュートリアルでは、Vonage番号でかかってきた電話を翻訳する方法を学ぶ。発信者の音声を翻訳し、翻訳されたテキストをすべてフロントエンドに送信します。.NET CoreでホストされたBlazor WebAssembly AppとSignalRを使用することで、このプロセスは驚くほどシームレスになります。

前提条件

  • Azure Speech Resourceが必要です。 ここで.リソース上の Keys and Endpointタブから地域とキーの値を引き出します。

  • 最新の .NET Core SDKのインストール

  • Visual StudioまたはVisual Studio Code。このデモではVisual Studio 2019を使用します。

  • 私たちのCLI。お持ちでない場合は、以下の方法でインストールできます。 npm install @vonage/cli -g

  • ngrokをテストする。必要なのは無料版だけです。

コードに直接ジャンプ

もしあなたがすべての前提条件をセットアップしているなら、このチュートリアルをスキップしてコードに直接ジャンプすることができる。アプリは GitHub

スピンアップ・ングロック

ngrok を使用して、ローカルで実行中の ASP.NET Core アプリケーションをインターネットに公開します。ngrok をインストールしたら、コンソールでコマンド ngrok http 5000を実行するだけだ。以下のようなものが表示される:

Example of Ngrok running in the terminalExample of Ngrok running in the terminal

注意 - このチュートリアルでは Kestrelを使用しています。代わりに IIS Express を使用したい場合は、私たちの 説明- を参照してください。

ここで注意しなければならないのは、転送先URLである。 http://1976e6d913a7.ngrok.io.Vonage番号に電話がかかってくると、VonageはWebHookと呼ばれるHTTP GETリクエストをアプリケーションに送り、Nexmo Call Control Object(NCCO)と呼ばれるものを要求します。アプリケーションは /webhooks/answerでリッスンするので、必要なURLはすべて http://1976e6d913a7.ngrok.io/webhooks/answer.

CLIの設定

CLIをまだセットアップしていない場合は、次のコマンドを実行してください。 vonage config:set --apiKey=API_KEY --apiSecret=API_SECRETここで、APIキーとシークレットは、あなたのアカウントの設定ページにあるAPIキーとシークレットです。 アカウントの設定ページで確認できます。

背番号の購入とアプリケーションの作成

CLIがセットアップされたので、番号を購入し、Vonageアプリケーションを作成し、そのアプリケーションに番号をリンクします。

番号を買う

番号を購入するには、次のコマンドを使用します(あなたの国IDを US)

vonage numbers:search US vonage numbers:buy [NUMBER] [COUNTRYCODE]

を入力する。 confirm購入した番号が出力されます。

アプリケーションの作成

次にアプリケーションを作成します。create applicationコマンドは2つのURLを取ります。answer URLはVonageが着信コールを送る先の番号で、event URLはVonageがあなたの番号から発生したイベントを送るURLです。忘れずに 1976e6d913a7をngrok URLのランダムハッシュに置き換えることを忘れないでください:

vonage apps:create ✔ Application Name … "DTMFInput" ✔ Select App Capabilities › Messages ✔ Create messages webhooks? … yes √ Answer Webhook - URL ... http://1976e6d913a7.ngrok.io/webhooks/answer √ Answer Webhook - Method » GET ✔ Status Webhook - URL … http://1976e6d913a7.ngrok.io/webhooks/events ✔ Status Webhook - Method › POST ✔ Allow use of data for AI training? Read data collection disclosure - https://help.nexmo.com/hc/en-us/articles/4401914566036 … yes

この操作により、アプリケーションIDと秘密鍵が返される。これら両方の値を保存してください。このチュートリアルではアプリIDのみを使用しますが、秘密鍵はアプリケーションリクエストを認証するために使用します。

アプリケーションをリンクする

次に、新しく購入した番号をアプリケーションにリンクさせる必要があります。番号をリンクすることで、Vonageはその番号で受信したすべてのコールをアプリケーションのWebhook URLに送信するようになります。これを行うには、アプリの作成リクエストで受け取ったアプリケーションIDが必要です。 e7a25242-77a1-42cd-a32e-09febcb375f4そして購入したばかりの電話番号で、次のようなコマンドを実行します:

vonage apps:link --number=VONAGE_NUMBER APP_ID

アプリを作る

これでセットアップと設定は完了したので、次はアプリをビルドする。ターミナルで典型的なソース・ディレクトリに行き、以下のコマンドを実行する:

dotnet new blazorwasm -ho --no-https -n VonageDotnetTranslator

このコマンドは Blazor WebAssembly アプリケーションを scaffold してくれます。SSLを設定するつもりはありません。

つのcsprojファイルが作成される、

  1. VonageDotnetTranslator.Client - ここでWebAssemblyが定義され、アプリケーションのフロントエンドとなります。

  2. VonageDotnetTranslator.Server - これは私たちのアプリケーションのための.NETコアホストサーバになります。このプロジェクトは、私たちが行う必要があることの大部分を行う場所です。

  3. VonageDotnetTranslator.Shared - これはクライアントとサーバー間の共有データです。

NuGetパッケージの追加

このサンプルでは以下のNuGetパッケージを使用します:

  1. ボネージ

  2. Microsoft.aspnetcore.signalr.core

  3. Microsoft.CognitiveServices.Speech

  4. Microsoft.aspnetcore.signalr.クライアント

これらをインストールするには、まずターミナルで VonageDotnetTranslator\Serverに移動し、以下のコマンドを実行する:

dotnet add package Vonage dotnet add package Microsoft.aspnetcore.signalr.core dotnet add package Microsoft.CognitiveServices.Speech

次に、ターミナルで VonageDotnetTranslator\Clientに移動して実行する:

dotnet add package Microsoft.AspNetcore.SignalR.client

このコマンドを実行すると、必要なパッケージがすべてインストールされます。次にVisual StudioでVonageDotnetTranslator.slnファイルを開きます。

モデルを追加する

クライアントとサーバーの間で共有されるデータのモデルを作成します。このために、1つのクラスを使用します。 Translation.このクラスには、翻訳するコールの一意な識別子、翻訳イベントのテキスト、話された言語、翻訳された言語が格納されます。共有プロジェクトの Translation.csを作成し、そこに以下を追加します:

public class Translation
 {
     public string UUID { get; set; }
     public string Text { get; set; }
     public string LanguageSpoken { get; set; }
     public string LanguageTranslated { get; set; }
 }

翻訳ハブの追加

SignalR接続を使って、フロントエンドで翻訳イベントを受信します。バックエンドからフロントエンドへの通信には、Hub接続を使用します。これを使うには、サーバーにHubを定義する必要がある。サーバ・プロジェクトにHubsフォルダを追加し、それから TranslationHubというクラスを追加し、それをパブリック・クラスにして Microsoft.AspNetCore.SignalR.Hubs.このクラスには他のロジックは必要ありません。

翻訳者の構築

このプロジェクトで最も複雑なクラスはトランスレーターになります。ここでは TranslationEngine.まず TranslationEngine.csファイルを VonageDotnetTranslator.Serverプロジェクトにファイルを作りましょう。この TranslationEngineは、いくつかの基本的なシステム・リソースを扱うことになる。その結果 TranslationEngineIDisposableインターフェイスを実装します。クラス定義は以下のようになる:

public class TranslationEngine : IDisposable

定数の定義

まず、このクラスにいくつかの定数を追加します。これらの定数は、これから扱うオーディオ・ストリームに関するさまざまなメタデータになります。これから SAMPLES_PER_SECONDを追加します。 BITS_PER_SAMPLEを16に、a NUMBER_OF_CHANNELS1、そして BUFFER_SIZEを640(または320 * 2)に追加します。

const int SAMPLES_PER_SECOND = 16000;
const int BITS_PER_SAMPLE = 16;
const int NUMBER_OF_CHANNELS = 1;
const int BUFFER_SIZE = 320 * 2;

プライベート・フィールドの追加

TranslationEngineクラスには、かなり多くの可動部分があります。多くのプライベートフィールドを定義する必要があります。これらのフィールドのほとんどは、翻訳者と音声合成器の設定を処理します。翻訳エンジンのメタデータを扱うフィールドもいくつかあります。また、通話に書き戻す音声をキューに入れるコンカレント・キューもあります。いずれにせよ、フィールドはこのようになる:

private ConcurrentQueue<byte[]> _audioToWrite = new ConcurrentQueue<byte[]>(); // queue to managed synthesized audio
private readonly IConfiguration _config; //Where Azure Subscription Keys will be stored
private readonly IHubContext<TranslationHub> _hub; // Hub connection we'll use to talk to frontend
private string _uuid; // Unique ID of the call being translated
private string _languageSpoken; // The language being spoken on the call
private string _languageTranslated; // The language being translated to

private SpeechTranslationConfig _translationConfig; // the configuration for the speech translator
private SpeechConfig _speechConfig; // configuration for the speech synthesizer
private PushAudioInputStream _inputStream = AudioInputStream.CreatePushStream(AudioStreamFormat.GetWaveFormatPCM(SAMPLES_PER_SECOND, BITS_PER_SAMPLE, NUMBER_OF_CHANNELS)); //Stream for handling audio input to the translator
private AudioConfig _audioInput; //configuration for the translation audio
private TranslationRecognizer _recognizer; // The translator
private SpeechSynthesizer _synthesizer; // The syntheziser, which will turn translated text into audio
private AudioOutputStream _audioOutputStream; // Output stream from the synthezier
private AudioConfig _outputConfig; // output configuration for the speech syntheizer

このクラスで使っているインポートは以下の通り:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using Microsoft.CognitiveServices.Speech;
using Microsoft.CognitiveServices.Speech.Audio;
using Microsoft.CognitiveServices.Speech.Translation;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using VonageDotnetTranslator.Server.Hubs;
using VonageDotnetTranslator.Shared;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;

コンストラクタの追加

このクラスにはコンストラクタを1つ用意する。このコンストラクタは、ミドルウェアから2つの依存性注入タイプのアイテムを取得することになります。オブジェクトです。 IConfigurationオブジェクト、これは Cognitive Services リソースからの Azure 認証情報を格納する場所です。 IHubContextオブジェクトで、フロントエンドとの通信に使用します。これらを適切なクラス・フィールドに割り当て、オーディオ用の設定とストリームも構築します。

public TranslationEngine(IConfiguration config, IHubContext<TranslationHub> hub)
{
   _hub = hub;
   _config = config;
   _translationConfig = SpeechTranslationConfig.FromSubscription(
       _config["SUBSCRIPTION_KEY"], _config["REGION"]);
   _speechConfig = SpeechTranslationConfig.FromSubscription(
       _config["SUBSCRIPTION_KEY"], _config["REGION"]);
   _audioInput = AudioConfig.FromStreamInput(_inputStream);
   _audioOutputStream = AudioOutputStream.CreatePullStream();
   _outputConfig = AudioConfig.FromStreamOutput(_audioOutputStream);
}

ハンドル翻訳認識

次に、翻訳認識イベントを処理するイベントを追加する必要があります。翻訳者が品詞を翻訳するたびに、このイベントが発生します。認識イベントから翻訳を取り出します。そして、その翻訳を SpeechSynthesizerに送られ、通話中にユーザーに再生される音声が取り出されます。次に、翻訳されたテキストから Translationオブジェクトを作成し、ハブを聞いているすべてのクライアントに送信します。最後に、合成された音声を先ほど作成したキューにエンキューします。

private void RecognizerRecognized(object sender, TranslationRecognitionEventArgs e)
{
   var translationLanguage = _languageTranslated.Split("-")[0];
   var translation = e.Result.Translations[translationLanguage].ToString();
   Trace.WriteLine("Recognized: " + translation);
   var ttsAudio = _synthesizer.SpeakTextAsync(translation).Result.AudioData;
   var translationResult = new Translation
   {
       LanguageSpoken = _languageSpoken,
       LanguageTranslated = _languageTranslated,
       Text = translation,
       UUID = _uuid
   };
   _hub.Clients.All.SendAsync("receiveTranslation", translationResult);
   _audioToWrite.Enqueue(ttsAudio);
}

トランスレーターとシンセサイザーを起動する

電話を受けたら、トランスレーターとシンセサイザーを起動する。トランスレータに RecognizerRecognizedイベントをトランスレータに登録し、すべてを継続的に起動させます。入力ストリームを使って音声をトランスレータに送るので、特定の時間が経過するか、トランスレータが音声の途切れを検出すると、翻訳イベントが継続的にプッシュされます。

private async Task StartSpeechTranslationEngine(string recognitionLanguage, string targetLanguage)
{
   _translationConfig.SpeechRecognitionLanguage = recognitionLanguage;
   _translationConfig.AddTargetLanguage(targetLanguage);
   _speechConfig.SpeechRecognitionLanguage = targetLanguage;
   _speechConfig.SpeechSynthesisLanguage = targetLanguage;
   _synthesizer = new SpeechSynthesizer(_speechConfig, _outputConfig);
   _recognizer = new TranslationRecognizer(_translationConfig, _audioInput);
   _recognizer.Recognized += RecognizerRecognized;
   await _recognizer.StartContinuousRecognitionAsync();
}

翻訳停止

翻訳エンジンを停止するメソッドが必要です。このメソッドは RecognizerRecognizedイベントを _recognizerを呼び出します。 StopContinuousRecognitionAsyncを呼び出します。このメソッドが終了するまでに数秒かかることがあるので、非同期で行います。

private async Task StopTranscriptionEngine()
{
   if (_recognizer != null)
   {
       _recognizer.Recognized -= RecognizerRecognized;
       await _recognizer.StopContinuousRecognitionAsync();
   }
}

メイン処理ループ

私たちはWebSocketで通話から音声を受信します。つまり、WebSocketから音声を継続的に読み取ることになります。つまり、WebSocketから音声を継続的に読み込むことになります。 SpeechSynthesizerから合成された音声を受け取り、それをWebSocketに書き戻すことで、発信者は翻訳を聞くことができます。この処理は1つのメインループで行われ、クローズステータスが表示されるまでWebSocketからメッセージを読み続けます。

特に、WebSocketから受け取る最初のメッセージは、UTF-8でエンコードされたJSONになります。 Headersに対応するUTF-8エンコードされたJSONになります。このJSONは、後でVonage Voice APIにWebSocketの作成を依頼する際に渡されます。このメッセージには Headersには、共有プロジェクトで定義した Translationオブジェクトにデシリアライズします。 Translationオブジェクトにデシリアライズし、そのメタデータを使用してTranslationEngineを起動します。

public async Task ReceiveAudioOnWebSocket(HttpContext context, WebSocket webSocket)
{
   var buffer = new byte[BUFFER_SIZE];

   try
   {
       WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
       var config = JsonConvert.DeserializeObject<Translation>(System.Text.Encoding.Default.GetString(buffer));
       _uuid = config.UUID;
       await StartSpeechTranslationEngine(config.LanguageSpoken,
           config.LanguageTranslated);
       _languageSpoken = config.LanguageSpoken;
       _languageTranslated = config.LanguageTranslated;
       while (!result.CloseStatus.HasValue)
       {

           byte[] audio;
           while (_audioToWrite.TryDequeue(out audio))
           {
               const int bufferSize = 640;
               for (var i = 0; i + bufferSize < audio.Length; i += bufferSize)
               {
                   var audioToSend = audio[i..(i + bufferSize)];
                   var endOfMessage = audio.Length > (bufferSize + i);
                   await webSocket.SendAsync(new ArraySegment<byte>(audioToSend, 0, bufferSize), WebSocketMessageType.Binary, endOfMessage, CancellationToken.None);
               }
           }

           result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);

           _inputStream.Write(buffer);
       }
       await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
   }
   catch (Exception e)
   {
       Trace.WriteLine(e.ToString());
   }
   finally
   {
       await StopTranscriptionEngine();
   }
}

すべてを処分する

最後に、良きメモリー市民として、このオブジェクトが破棄されるときに、翻訳中にアクセスしたすべての管理されていないリソースを破棄する。

public void Dispose()
{
   _inputStream.Dispose();
   _audioInput.Dispose();
   _recognizer.Dispose();
   _synthesizer.Dispose();
   _audioOutputStream.Dispose();
}

ボイスコントローラーを追加する

ここからは下り坂だ。とりあえず、空のAPIコントローラ VoiceControllerという空の API コントローラを Controllersフォルダに追加します。ここに Answer.エンドポイントは /webhooks/answer.このメソッドは、Vonage API番号がコールを受信したときに呼び出されるGETリクエストになります。このメソッドは Nexmo Call Control Object (NCCO) を生成し、Vonage にサーバへの WebSocket を構築するように指示する。先に述べたように Translationオブジェクトをこのオブジェクトのヘッダーに渡す。このNCCOを受け取り、VonageにJSONを返し、リクエストの処理方法を伝えます。

[Route("/webhooks/answer")]
[HttpGet]
public ActionResult Answer()
{
   var host = Request.Host.ToString();
   var webSocketAction = new ConnectAction()
   {
       Endpoint = new[]
       {
           new WebsocketEndpoint()
           {
               Uri = $"ws://{host}/ws",
               ContentType="audio/l16;rate=16000",
               Headers = new Translation
               {
                   UUID = Request.Query["uuid"].ToString(),
                   LanguageSpoken = "en-US",
                   LanguageTranslated = "es-MX"
               }
           }
       }
   };
   var ncco = new Ncco(webSocketAction);
   return Ok(ncco.ToString());
}

ミドルウェアの設定

コントローラができたので、アプリのサーバ部分で最後に行うことは、アプリのミドルウェアを設定することです。開く Startup.cs.

サービス設定

メソッド内に ConfigureServicesメソッドの中にSignalRを追加するコールを追加する:

services.AddSignalR();

app.UseEndpoints

次は app.UseEndpointsメソッドの Configureメソッドの呼び出しに大きな変更を加える必要がある。まず、クライアントが使用できるハブへのルートを /TranslationHubへのルートを定義します:

endpoints.MapHub<Hubs.TranslationHub>("/TranslationHub");

次に、使用している16khzのリニアPCMエンコーディングに基づいて、Vonageからの適切なサイズのメッセージを処理するためのWebSocketオプションを設定します:

var webSocketOptions = new WebSocketOptions()
{
  KeepAliveInterval = TimeSpan.FromSeconds(120),
  ReceiveBufferSize = 640
};

app.UseWebSockets(webSocketOptions);

最後に、Webソケットへの直接のルートを app.UseEndpointsデリゲートに定義します。このルートは、WebSocket用のHubContextを取得し、WebSocketをアップグレードします。 TranslationHubのHubContextを取得し、WebSocketをアップグレードし、Translationエンジンを起動するために必要なハブ、設定、WebSocket、httpContextを取得します。

endpoints.Map("/ws", async (context) => {
if (context.WebSockets.IsWebSocketRequest)
{
   var hub = (IHubContext<TranslationHub>)app.ApplicationServices.GetService(typeof(IHubContext<TranslationHub>));
   WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
   using (var engine = new TranslationEngine(Configuration, hub))
   {
       await engine.ReceiveAudioOnWebSocket(context, webSocket);
   }
}
else
{
   context.Response.StatusCode = 400;
}
});

フロントエンドの構築

最後に必要なのは、フロントエンドの構築だ。皮肉なことに、これはこの練習の簡単な部分です。 VonageDotnetTranslator.Clientプロジェクトの Pagesフォルダに追加します。 TranslationComponent.razor.このファイルでフロントエンドのロジックを定義します。

依存関係を取り込む

この翻訳コンポーネントで動作するために必要な依存関係を取り込みます。これらの依存関係には、SignalRクライアント、これまで使ってきた共有プロジェクト、ルーティングを助けるための NavigationManager(を注入し、最後にIDisposableを実装します:

@using Microsoft.AspNetCore.SignalR.Client
@using VonageDotnetTranslator.Shared
@inject NavigationManager NavigationManager
@implements IDisposable

翻訳を更新するコードを追加する

次に @codeブロックの中で、サーバーから取得したすべての翻訳を格納する辞書を定義します。 HubConnection.を定義します。 OnInitializedAsyncを構築します。 HubConnectionミドルウェアで定義した /TranslationHubルートを構築します。そして receiveTranslationイベントが発生するたびに(これは翻訳が発生したときに送信されるイベントです)、翻訳辞書を更新します。もし UUIDがすでに辞書に登録されている場合、その翻訳テキストを現在の翻訳テキストに連結します。そうでない場合は、新しい翻訳オブジェクトを追加します。その後、Hub Connectionを起動し、接続を管理するためのメソッドをいくつか追加して、接続が完了したらクリーンアップします。

@code {
    private Dictionary<string, Translation> _translations = new Dictionary<string, Translation>();
    private HubConnection _hubConnection;
    protected override async Task OnInitializedAsync()
    {
        _hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/TranslationHub"))
            .Build();
        _hubConnection.On<Translation>("receiveTranslation", (translation) =>
        {
            if (_translations.ContainsKey(translation.UUID))
            {
                _translations[translation.UUID].Text += translation.Text;
            }
            else
            {
                _translations.Add(translation.UUID, translation);
            }
            StateHasChanged();
        });
        await _hubConnection.StartAsync();
    }
    public bool IsConnected => _hubConnection.State == HubConnectionState.Connected;

    public void Dispose()
    {
        _ = _hubConnection.DisposeAsync();
    }
}

見解を加える

次に、すべての翻訳を格納するテーブルを追加します。このテーブルは Translationオブジェクトのプロパティに対応するヘッダを持ちます。 _translationsコレクションから直接入力します。

<h3>Translation</h3>
<table class="table">
    <thead>
        <tr>
            <th>Uuid</th>
            <th>Language Spoken</th>
            <th>Language Translated To</th>
            <th>Text</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var translation in _translations.Values)
        {
            <tr>
                <td>@translation.UUID</td>
                <td>@translation.LanguageSpoken</td>
                <td>@translation.LanguageTranslated</td>
                <td>@translation.Text</td>
            </tr>
        }
    </tbody>
</table>

インデックスに追加

最後にすることは TranslationComponentindex.razorファイルに追加することだ。そのためには Index.razorを開き @pageディレクティブ以外の内容を削除し、追加する:

アプリの設定

最後にすることは、2つのAzure Configuration項目を VonageDotnetTranslation.Server/appsettings.jsonファイルに追加する。ベースオブジェクトに2つのフィールドを追加します、 SUBSCRIPTION_KEYREGIONという2つのフィールドを追加し、サブスクリプションキーとリージョンに設定する。

テスト

音声翻訳機を作るために必要なことは以上です!あとはテストするだけです。 VonageDotnetTranslation/Serverディレクトリから dotnet runまたはf5か再生ボタンを使ってください。

注意 - IIS Express を使用している場合は、以下のガイドを参照してください。 ngrok を IIS Express で使用するためのガイドを参照してください。

私たちの VoiceControllerでは、翻訳言語をスペイン語に、話し言葉をアメリカ英語に設定しています。 対応言語ドキュメントを参照してください。

さらに突き進む

WebSocketは、Azure Cognitive Servicesと組み合わせることで、あらゆる種類の強力なユースケースを可能にする。翻訳だけでなく、通常の書き起こしやセンチメント分析も可能だ!VonageのAPIは、PSTNやVoIPとの柔軟性が高く、比較的簡単に統合を構築することができます。

リソース

  • このデモのソースは GitHub

シェア:

https://a.storyblok.com/f/270183/384x384/73d57fd8eb/stevelorello.png
Steve Lorelloヴォネージの卒業生

スティーブは、かつてVonageチームの一員でした。彼はVonageで.NET開発者アドボケートを務め、フルスタックかつ多言語対応のソフトウェアエンジニアであり、AI/MLの専門家でもありました。