
Compartilhar:
Steve é um ex-membro da equipe da Vonage. Ele atuou como Developer Advocate .NET na Vonage, engenheiro de software full-stack poliglota, especializado em IA/ML
Traduza uma ligação telefônica com Blazor, SignalR e Azure
Os modelos de aprendizado de máquina nos permitem fazer todo tipo de coisas interessantes. Por exemplo, tradução de fala em tempo real. Neste tutorial, aprenderemos como traduzir uma chamada recebida em um número da Vonage. Em seguida, traduziremos a fala do chamador e enviaremos todo o texto traduzido para nosso front-end. O uso de um aplicativo Blazor WebAssembly hospedado no .NET Core, juntamente com o SignalR, tornará esse processo incrivelmente simples.
Pré-requisitos
Você precisará de um recurso do Azure Speech — é possível criar um seguindo as etapas aqui. Obtenha a região e o valor da chave na
Keys and Endpointaba do seu recurso.A versão mais recente SDK do .NET Core instalado
Visual Studio ou Visual Studio Code. Vou usar o Visual Studio 2019 para esta demonstração
Nossa CLI. Se você ainda não a tiver, pode instalá-la com
npm install @vonage/cli -gngrok para testes. Você só precisa do plano gratuito.
Ir direto para o código
Se você já tiver todos os pré-requisitos configurados, pode pular este tutorial e ir direto para o código. O aplicativo está no GitHub
Iniciar o Ngrok
Vamos usar o ngrok para expor à internet nossa aplicação ASP.NET Core em execução local. Depois de instalar o ngrok, basta executar o comando ngrok http 5000 no seu console. Isso gerará algo parecido com isto:
Example of Ngrok running in the terminal
NOTA — este tutorial utiliza o Kestrel para depuração local. Se você preferir usar o IIS Express, consulte nosso guia explicativo sobre como usar o ngrok com o IIS Express.
O ponto fundamental a se observar aqui é a URL de redirecionamento — no meu caso, essa URL é http://1976e6d913a7.ngrok.io. Quando você receber uma chamada no seu número da Vonage, a Vonage enviará ao seu aplicativo o que é chamado de WebHook, que é apenas uma solicitação HTTP GET, solicitando algo chamado Nexmo Call Control Object (NCCO). Nosso aplicativo ficará à espera em /webhooks/answer , portanto, a URL completa de que vou precisar será http://1976e6d913a7.ngrok.io/webhooks/answer.
Configuração da CLI
Se você ainda não configurou nossa CLI, faça isso executando o comando vonage config:set --apiKey=API_KEY --apiSecret=API_SECRET onde “API Key” e “Secret” são a chave e o segredo da API encontrados na página de configurações da sua conta
Compre um número e crie um aplicativo
Agora que sua CLI está configurada, vamos adquirir um número, criar uma Application da Vonage e vincular o número a essa Application, o que fará com que a Vonage encaminhe as chamadas para o seu aplicativo.
Compre um número
Para comprar um número, use o seguinte comando (substituindo o ID do seu país por US)
Digite confirm para concluir a operação; será exibido o número que você comprou.
Criar um aplicativo
A seguir, vamos criar um aplicativo. O comando “create application” aceita duas URLs: a URL de resposta — que será o número para o qual a Vonage encaminhará as chamadas recebidas — e a URL de evento, que será a URL para a qual a Vonage enviará os eventos gerados por um dos seus números. Lembre-se de substituir 1976e6d913a7 pelo hash aleatório da sua URL do ngrok:
Essa operação retornará um ID de aplicativo e uma chave privada. Salve esses dois valores. Neste tutorial, usaremos apenas o ID do aplicativo, mas você utilizará a chave privada para autorizar as solicitações do seu aplicativo.
Vincular o aplicativo
Em seguida, precisamos vincular nosso número recém-adquirido ao nosso aplicativo. Ao vincular nosso número, a Vonage será instruída a encaminhar todas as chamadas recebidas nesse número para a URL do webhook do nosso aplicativo. Para isso, precisaremos do ID do aplicativo que acabamos de receber da solicitação de criação do aplicativo — que terá o formato e7a25242-77a1-42cd-a32e-09febcb375f4, e o número de telefone que acabamos de adquirir, e executaremos um comando semelhante a este:
Crie nosso aplicativo
Agora que já estamos instalados e configurados, precisamos compilar nosso aplicativo. Acesse o diretório de código-fonte no terminal e execute o seguinte comando:
Este comando irá gerar uma estrutura inicial para um aplicativo Blazor WebAssembly. Não vamos configurar o SSL, o que facilitará o trabalho com o ngrok.
Isso vai criar três arquivos csproj,
VonageDotnetTranslator.Client — é aqui que o WebAssembly é definido — será a interface do usuário do nosso aplicativo.
VonageDotnetTranslator.Server — Este será o servidor hospedado em .NET Core para nosso aplicativo. É neste projeto que ocorrerá a maior parte do trabalho que precisamos realizar.
VonageDotnetTranslator.Shared — Estes são os dados compartilhados entre o cliente e o servidor.
Adicionar pacotes NuGet
Vamos utilizar os seguintes pacotes NuGet para este exemplo:
Vonage
Microsoft.aspnetcore.signalr.core
Microsoft.CognitiveServices.Speech
Microsoft.aspnetcore.signalr.client
Para instalá-los, primeiro acesse VonageDotnetTranslator\Server no seu terminal e execute os seguintes comandos:
Em seguida, acesse VonageDotnetTranslator\Client no seu terminal e execute:
Este comando instalará todos os pacotes de que você vai precisar. Agora, abra o arquivo VonageDotnetTranslator.sln no Visual Studio.
Adicionar um modelo
Vamos criar um modelo de dados compartilhados entre nosso Cliente e nosso Servidor. Para isso, vamos usar apenas uma única classe, que chamaremos de Translation. Essa classe conterá um identificador único para a chamada que estamos traduzindo, o texto de um determinado evento de tradução, o idioma falado e o idioma para o qual a tradução foi feita. Crie um arquivo Translation.cs no projeto compartilhado e adicione o seguinte a ele:
public class Translation
{
public string UUID { get; set; }
public string Text { get; set; }
public string LanguageSpoken { get; set; }
public string LanguageTranslated { get; set; }
} Adicionar um Centro de Tradução
Receberemos eventos de tradução em nosso front-end por meio de uma conexão SignalR. Para nos comunicarmos com o front-end a partir do back-end, usaremos uma conexão Hub. Para isso, precisamos definir um Hub em nosso servidor. Basta adicionar uma pasta “Hubs” ao projeto do servidor, em seguida, criar uma classe chamada TranslationHub, torne-a uma classe pública e faça com que ela herde de Microsoft.AspNetCore.SignalR.Hubs. Não precisamos de nenhuma outra lógica para essa classe.
Crie nosso Tradutor
A classe mais complexa deste projeto será o nosso tradutor. Ao qual vamos chamar de TranslationEngine. Vamos começar criando um arquivo TranslationEngine.cs em nosso VonageDotnetTranslator.Server projeto. O TranslationEngine vai lidar com alguns recursos do sistema subjacente. Consequentemente, a TranslationEngine vai implementar a IDisposable interface para nos permitir limpá-lo facilmente após a conclusão. A definição da classe deve ficar assim:
public class TranslationEngine : IDisposable Definir constantes
Vamos adicionar várias constantes a essa classe para darmos início ao trabalho. Essas constantes serão diversos metadados sobre o fluxo de áudio com o qual vamos trabalhar. Vamos adicionar uma SAMPLES_PER_SECOND a 16.000, uma BITS_PER_SAMPLE de 16, um NUMBER_OF_CHANNELS em 1 e um BUFFER_SIZE de 640 (ou 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; Adicionar campos privados
A classe TranslationEngine possui um número considerável de componentes. Precisaremos definir vários campos privados. A maioria desses campos lida com a configuração do tradutor e do sintetizador de voz. Temos alguns que lidarão com os metadados do mecanismo de tradução. Também teremos uma fila simultânea, na qual enfileiraremos o áudio para reenviá-lo à nossa chamada. De qualquer forma, os campos terão a seguinte aparência:
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
Isso requer várias importações; aqui estão as que estou usando nesta classe:
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; Adicionar construtor
Teremos um único construtor para essa classe. Ele receberá dois itens do tipo injeção de dependência do middleware. Um IConfiguration objeto, onde armazenaremos as credenciais do Azure do nosso recurso do Cognitive Services, e um IHubContext objeto, que usaremos para nos comunicarmos com nosso front-end. Vamos atribuí-los aos campos apropriados da classe e, em seguida, também criaremos algumas configurações e fluxos para nosso áudio.
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);
}
Tratamento do reconhecimento de tradução
Agora precisaremos adicionar um evento para lidar com eventos de reconhecimento de tradução. Sempre que nosso tradutor traduzir uma classe gramatical, esse evento será acionado. Extrairemos a tradução do evento de reconhecimento. Em seguida, ela será encaminhada ao nosso SpeechSynthesizer para extrair o áudio a ser reproduzido para o usuário durante a chamada. Em seguida, criaremos um Translation objeto a partir do texto traduzido e enviá-lo a todos os nossos clientes que estão conectados ao hub. Por fim, colocaremos o áudio sintetizado na fila que criamos anteriormente.
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);
} Inicie o Tradutor e o Sintetizador
Quando recebermos uma chamada, vamos iniciar nosso tradutor e nosso sintetizador. Vamos registrar nosso RecognizerRecognized evento no tradutor e deixaremos tudo rodando continuamente. Como estamos usando um fluxo de entrada para alimentar o áudio ao nosso tradutor, isso irá gerar eventos de tradução continuamente após um determinado tempo ter decorrido ou quando o tradutor detectar uma pausa na fala.
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();
} Interromper tradução
Precisaremos de um método para interromper o mecanismo de tradução. Esse método cancelará o registro do nosso RecognizerRecognized evento do _recognizer e chamar o StopContinuousRecognitionAsync no reconhecedor para encerrá-lo. Esse método pode levar alguns segundos para ser concluído, e é por isso que o executamos de forma assíncrona.
private async Task StopTranscriptionEngine()
{
if (_recognizer != null)
{
_recognizer.Recognized -= RecognizerRecognized;
await _recognizer.StopContinuousRecognitionAsync();
}
} Ciclo principal de processamento
Receberemos o áudio das nossas chamadas por meio de um WebSocket, o que significa que estaremos lendo continuamente o áudio desse WebSocket. Então, sempre que recebermos traduções, pegaremos o áudio sintetizado do nosso SpeechSynthesizer e o reenviaremos pelo WebSocket para que a pessoa que está ligando possa ouvir a tradução. Esse processo ocorrerá em um loop principal que continuará lendo mensagens do WebSocket até que seja detectado um status de encerramento.
Vale destacar que a primeira mensagem que receberemos do WebSocket será um JSON codificado em UTF-8, correspondente ao Headers que, posteriormente, reenviaremos à Voice API da Vonage quando solicitarmos que ela estabeleça o WebSocket para nós. Esse Headers conterá um Translation objeto que definimos em nosso projeto compartilhado; portanto, vamos deserializar o JSON em um Translation objeto e usaremos os metadados dele para iniciar nosso 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();
}
}
Jogue tudo fora
Por fim, seremos “cidadãos da memória” responsáveis e liberaremos todos os recursos não gerenciados aos quais tivemos acesso durante nossa tradução quando este objeto for destruído.
public void Dispose()
{
_inputStream.Dispose();
_audioInput.Dispose();
_recognizer.Dispose();
_synthesizer.Dispose();
_audioOutputStream.Dispose();
} Adicionar um controlador de voz
Daqui em diante é só descer. Por enquanto, vamos adicionar um controlador de API vazio chamado VoiceController à nossa Controllers pasta. Aqui, vamos adicionar uma única rota chamada Answer. O endpoint será /webhooks/answer. Esse método será uma solicitação GET chamada quando seu número da API da Vonage receber uma chamada. Ele criará um Nexmo Call Control Object (NCCO), instruindo a Vonage a estabelecer um WebSocket com nosso servidor. Como mencionamos anteriormente, passaremos um Translation objeto pelos cabeçalhos desse objeto, o que informará ao outro lado como lidar com a tradução. Pegaremos esse NCCO e retornaremos seu JSON à Vonage para indicar como lidar com a solicitação.
[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());
} Configurar o middleware
Agora que já temos um controlador, a última coisa a fazer na parte do servidor do nosso aplicativo é configurar o middleware do aplicativo. Abra Startup.cs.
Configurar serviços
Dentro do ConfigureServices método, adicione uma chamada para incluir o SignalR:
services.AddSignalR(); app.UseEndpoints
Em seguida, teremos que fazer algumas modificações significativas na app.UseEndpoints chamada no Configure método. Primeiro, definiremos uma rota para o nosso hub em /TranslationHub que nossos clientes poderão usar:
endpoints.MapHub<Hubs.TranslationHub>("/TranslationHub");
A seguir, vamos configurar as opções do WebSocket para processar as mensagens da Vonage com o tamanho adequado, com base na codificação PCM linear de 16 kHz que estamos usando:
var webSocketOptions = new WebSocketOptions()
{
KeepAliveInterval = TimeSpan.FromSeconds(120),
ReceiveBufferSize = 640
};
app.UseWebSockets(webSocketOptions);Por fim, definiremos uma rota direta para nossos WebSockets no app.UseEndpoints delegado. Essa rota irá buscar o HubContext para o nosso TranslationHub, atualizará o WebSocket e iniciará nosso mecanismo de tradução com o hub, a configuração, o WebSocket e o httpContext necessários para que ele comece a funcionar.
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;
}
});
Desenvolva nosso front-end
A última coisa que precisaremos fazer é desenvolver nosso front-end. Curiosamente, essa é a parte mais fácil deste exercício no VonageDotnetTranslator.Client projeto: adicione um novo componente Razor à Pages pasta chamada TranslationComponent.razor. É nesse arquivo que vamos definir nossa lógica de front-end.
Incorporar dependências
Vamos incluir as dependências necessárias para trabalhar com esse componente de tradução. Essas dependências incluirão o SignalR Client, o Projeto Compartilhado que temos usado, além da injeção de um NavigationManager (para auxiliar no roteamento) e, por fim, a implementação de IDisposable:
@using Microsoft.AspNetCore.SignalR.Client
@using VonageDotnetTranslator.Shared
@inject NavigationManager NavigationManager
@implements IDisposable Adicionar código para atualizar as traduções
Em seguida, no @code bloco, vamos definir um dicionário para armazenar todas as traduções que receberemos do nosso servidor e teremos um HubConnection. Em OnInitializedAsync, vamos criar nosso HubConnection, direcionando-a de volta para nossa /TranslationHub rota definida no middleware. Assim, sempre que o receiveTranslation evento for disparado (esse é o evento que enviamos quando ocorre uma tradução), atualizaremos nosso dicionário de traduções. Se o UUID da chamada já estiver no dicionário, concatenaremos o texto da tradução ao texto da tradução atual. Caso contrário, adicionaremos um novo objeto de tradução. Em seguida, iniciaremos a conexão com o Hub e adicionaremos alguns métodos extras para gerenciar a conexão e limpar tudo quando terminar.
@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();
}
}
Adicione nossa opinião
A seguir, vamos adicionar uma tabela que conterá todas as nossas traduções. Essa tabela terá cabeçalhos correspondentes às Translation propriedades do objeto, e vamos preenchê-la diretamente a partir da nossa _translations coleção.
<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>
Adicionar ao Índice
A última coisa a fazer é adicionar o TranslationComponent ao index.razor arquivo. Para isso, abra Index.razor, remova todo o conteúdo, exceto a diretiva @page , e adicione:
Configurar o aplicativo
A última coisa a fazer é adicionar os dois itens de configuração do Azure ao VonageDotnetTranslation.Server/appsettings.json arquivo. Adicione dois campos ao objeto base, SUBSCRIPTION_KEY e REGION, e defina-os com sua chave de assinatura e região.
Teste
É só isso que você precisa fazer para criar seu tradutor de fala! Agora só falta testá-lo: execute o aplicativo a partir do VonageDotnetTranslation/Server diretório com dotnet run ou usar a tecla F5 ou o botão “Reproduzir”.
Observação — Se você estiver usando o IIS Express, será necessário consultar nosso guia sobre como usar o o ngrok com o IIS Express
Em nosso VoiceController definimos o idioma de tradução como espanhol e o idioma falado como inglês dos EUA — você pode alterar isso facilmente — consulte os documentação de idiomas compatíveis para saber quais idiomas o Azure oferece suporte.
Indo além
Os WebSockets permitem todo tipo de casos de uso avançados quando combinados com o Azure Cognitive Services. Além de traduções, você também pode fazer transcrições comuns ou até mesmo análises de sentimento! As APIs da Vonage permitem todo tipo de integrações altamente flexíveis e relativamente fáceis de implementar com PSTN e VoIP.
Recursos
O código-fonte desta demonstração está em GitHub