https://d226lax1qjow5r.cloudfront.net/blog/blogposts/build-a-speech-translation-app-on-deno-with-azure-and-vonage/Blog_Speach-Translation_Deno-Azure_1200x600-1.png

Crear una aplicación de traducción de voz en Deno con Azure y Vonage

Publicado el May 4, 2021

Tiempo de lectura: 13 minutos

Vonage lanzó recientemente reconocimiento automático de voz (ASR) como una nueva función de Voice API, ¡lo cual es una excelente razón para crear una nueva y entretenida aplicación de voz que aproveche esta nueva capacidad!

En este tutorial, crearemos una aplicación de voz que se ejecuta en Deno que:

  1. Recibir una llamada telefónica

  2. Aceptar el discurso dicho por el interlocutor en el aviso

  3. Convierte esa voz en texto con Vonage ASR

  4. Tradúzcalo a un idioma elegido al azar utilizando Microsoft Azure

  5. Reproduzca tanto el texto original en inglés como el texto recién traducido

  6. Si el texto recién traducido tiene Vonage Voice disponiblese utilizará esa voz

Estamos construyendo usando Deno como nuestro entorno de ejecución porque Deno nos permite construir una aplicación del lado del servidor en TypeScript con dependencias ligeras. Nos permite integrar sólo el código externo que realmente necesitamos y darle sólo los permisos de ejecución que deseamos que tenga.

Hay varios proveedores posibles con los que podemos integrarnos para proporcionar traducción de texto. En este tutorial vamos a construir utilizando la API de traducción de voz de Microsoft Azure.

Empecemos.

tl;dr Si quieres saltarte el proceso y simplemente ejecutar la aplicación, puedes encontrar una versión completamente funcional en GitHub.

Requisitos previos

Para crear esta aplicación, necesitarás varios elementos antes de empezar a implementarla:

Una vez solucionado todo esto, podemos empezar a implantar nuestras aplicaciones.

Cuenta API de Vonage

Para completar este tutorial, necesitarás una cuenta API de Vonage. Si aún no tienes una, puedes inscribirte hoy y comenzar a acumular crédito gratis. Una vez que tengas una Account, puedes encontrar tu clave y secreto de API en la parte superior del Panel de API de Vonage.

Este tutorial también utiliza un número de teléfono virtual. Para adquirir uno, vaya a Numbers > Comprar Numbers y busque uno que se ajuste a sus necesidades.

Start building with Vonage

Creación de la estructura de carpetas

El primer paso es crear la estructura de carpetas para nuestra aplicación. Al final tendrá este aspecto:

.
+-- data/
|   +-- languages.ts
|   +-- voices.ts
+-- services/
|   +-- auth/
|     +-- token.ts
|   +-- translate.ts
|   +-- language_picker.ts
|   +-- voice_picker.ts
+-- server.ts
+-- .env

Para este tutorial, llamaremos a la carpeta raíz de la aplicación, speech-translation-apppero puedes llamarla como quieras. Una vez que hayas creado la carpeta raíz, cambia de directorio en ella y crea la carpeta data, servicesy services/auth subcarpetas.

Dentro de la carpeta raíz cree server.ts y .env ejecutando touch server.ts .env desde dentro del directorio raíz.

Realice una acción similar dentro de data, servicesy services/auth ejecutando touch para crear los archivos que se muestran en el árbol de directorios anterior.

Creación del servidor Deno

En su editor de código preferido, abra el archivo server.ts del directorio raíz que creó en el último paso.

Dentro de este archivo, instanciaremos un servidor HTTP, le proporcionaremos sus rutas y controlaremos el flujo de la aplicación.

Vamos a utilizar Opine como nuestro framework web para el servidor. Opine es un framework minimalista hecho para Deno portado desde ExpressJS. Si estás familiarizado con ExpressJS, entonces las construcciones en Opine te resultarán familiares.

Para utilizar Opine, tenemos que importarlo en la parte superior de nuestro archivo. Deno, a diferencia de NodeJS, no utiliza node_modules u otro sistema similar de gestión de paquetes. Como resultado, cada paquete traído a su aplicación se importa directamente de su fuente:

import { opine } from "https://deno.land/x/opine@master/mod.ts";

Una vez que hemos hecho opine disponible para su uso, podemos instanciar una instancia de la misma y crear la estructura de esqueleto para nuestras rutas:

const app = opine();
 app.get("/webhooks/answer", async function (req, res) {
  // Do something on a GET request to /webhooks/answer
});
 app.get("/webhooks/asr", async function (req, res) {
  // Do something on a GET request to /webhooks/asr
});
 app.get("/webhooks/event", async function (req, res) {
  // Do something on a GET request to /webhooks/event
  res.status = 204
})
 app.listen({ port: 8000 });
 console.log("Server is running on port 8000");

Las tres GET solicitudes enumeradas en el servidor corresponden a tres webhooks únicos de la Voice API de Vonage. La primera es donde la API envía una llamada entrante. La segunda es donde la API enviará el habla convertida a texto usando la función de reconocimiento automático de voz de Vonage. Por último, la tercera ruta es a donde se envían todos los datos de eventos del ciclo de vida de la llamada.

Necesitamos proporcionar lógica para cada una de estas tres rutas que controlarán el funcionamiento de nuestra aplicación: Esta conversación fue marcada como resuelta por NJalal7

  • La ruta de llamadas entrantes se encargará de tomar la entrada de voz de la persona que llama y enviarla a la API de Vonage para la conversión de texto.

  • La segunda ruta recibirá el texto y lo enviará a la API de traducción de voz de Azure para traducirlo a un segundo idioma. También reproducirá para la persona que llama los mensajes originales y traducidos.

  • La ruta final recibirá todos los datos de eventos del ciclo de vida de la llamada y acusará recibo de los datos.

Definición de las rutas

Construyamos la lógica para la llamada entrante /webhooks/answer ruta.

Dentro de la ruta necesitamos asignar el identificador de llamada (UUID) a una variable para poder utilizarla más tarde. La dirección UUID es un componente necesario de la petición ASR:

const uuid = req.query.uuid

Luego, debemos responder con un código de estado HTTP de 200 y enviar como respuesta un objeto de control de llamadas Nexmo (NCCO), que es un objeto JSON que contiene el conjunto de instrucciones que deseamos que realice la API de Vonage:

res.json([
  {
    action: 'talk',
    text: 'Welcome to the Vonage Universal Translator Randomizer brought to you by Vonage Automatic Speech Recognition run on Deno. Please say something.',
    bargeIn: true
  },
  {
    eventUrl: [asrWebhook],
    eventMethod: 'GET',
    action: 'input',
    speech: {
      uuid: [uuid],
      language: 'en-us'
    }
  }
]);

Como puede ver, la OCNC se compone de dos acciones: talk y inputrespectivamente.

La acción talk da la bienvenida a la persona que llama y le pide que diga algo. También establece un parámetro bargeIn igual a trueque permite a la persona que llama empezar a hablar antes de que el mensaje haya terminado.

La acción input es donde aceptamos la voz de la persona que llama. En esta acción definimos algunos parámetros únicos:

  • eventUrl: A dónde enviar la conversión de voz a texto. En la acción definimos la URL como una variable llamada asrWebhook. La crearemos más tarde.

  • eventMethod: Qué verbo HTTP utilizar para enviar el discurso completado a texto con. En este caso, utilizamos GET.

  • action: El parámetro base para todas las acciones NCCO. Su valor es igual a la acción que se desea realizar, en este caso input.

  • speech: Un parámetro cuyo valor es igual a un objeto que contiene el UUID de la persona que llama y el language del discurso que se está convirtiendo en texto.

En conjunto, esta primera GET ruta tiene el siguiente aspecto:

app.get("/webhooks/answer", async function (req, res) {
  const uuid = req.query.uuid
  res.status = 200
  res.json([
    {
      action: 'talk',
      text: 'Welcome to the Vonage Universal Translator Randomizer brought to you by Vonage Automatic Speech Recognition run on Deno. Please say something.',
      bargeIn: true
    },
    {
      eventUrl: [asrWebhook],
      eventMethod: 'GET',
      action: 'input',
      speech: {
        uuid: [uuid],
        language: 'en-us'
      }
    }
  ]);
});

La segunda ruta que debemos definir es la ruta /webhooks/asr que recibirá la conversión de voz a texto de la API de Vonage y actuará en consecuencia.

Hay algunos valores que queremos asignar a las variables a utilizar. El primero son los resultados de la conversión ASR, que nos llegan en forma de matriz de objetos. Los objetos están en orden descendente de probabilidad de precisión. La segunda variable contendrá el texto del objeto con la mayor exactitud de probabilidad.

Instanciamos la segunda variable como una variable vacía y asignamos su valor según la condición de si el ASR de Vonage pudo captar el habla ofrecida por la persona que llama. Si se reconoció el habla, se utiliza ese valor. Sin embargo, si no se reconoció el habla, se proporciona un valor predeterminado y se muestra un mensaje en la consola que explica el motivo.

En las dos últimas variables que creamos asignamos el valor de la elección aleatoria del idioma al que se traducirá el discurso, y elegimos la voz con la que se realizará la traducción. A continuación, compartimos la información sobre el idioma y la voz en la consola:

const data = await JSON.parse(req.query.speech)
var mostConfidentResultsText;
if (!data.results) {
  console.log("Vonage ASR did not pick up what you tried to say");
  mostConfidentResultsText = 'Vonage ASR did not pick up your speech. Please call back and try again.';
} else {
  mostConfidentResultsText = data.results[0].text;
};
const languageChoice = languagePicker(languageList);
const voiceChoice = voicePicker(voicesList, languageChoice);
console.log(`Language to translate into: ${languageChoice.name} and Vonage language voice being used: ${voiceChoice}`);

A continuación, establecemos el código de estado HTTP de respuesta en 200 como en la primera ruta, y respondemos con otro objeto NCCO JSON:

res.status = 200
res.json([
  {
    action: 'talk',
    text: `This is what you said in English: ${mostConfidentResultsText}`
  },
  {
    action: 'talk',
    text: `This is your text translated into ${languageChoice.name}`
  },
  {
    action: 'talk',
    text: `${await translateText(languageChoice.code.split('-')[0], mostConfidentResultsText)}`,
    voiceName: voiceChoice
  }
])

Este objeto NCCO contiene tres talk acciones, cada una de ellas con variables y funciones que necesitan ser creadas. Lo haremos cuando hayamos terminado de definir las rutas.

La primera talk acción devuelve a la persona que llama su mensaje original en inglés, tal y como se entendió durante la conversión automática de reconocimiento de voz.

La segunda talk indica a la persona que llama a qué idioma se ha traducido su mensaje.

La tercera talk acción dice a la persona que llama su mensaje recién traducido. También aprovecha el parámetro voiceName para decir el mensaje traducido en la voz designada del idioma, si hay una disponible para ese idioma.

La última ruta que necesitamos definir será una corta. Esta es la que recibirá el resto de los datos del evento webhook para la llamada. En este tutorial no vamos a hacer nada con esos datos aparte de acusar recibo. Lo confirmamos enviando de vuelta un código de estado 204 código de estado HTTP, que equivale a decir que el mensaje se ha recibido correctamente y que no hay contenido con el que responder:

app.get("/webhooks/event", async function (req, res) {
  res.status = 204
})

Con el servidor definido, estamos listos para construir las funciones helper que invocamos en las rutas del servidor.

Creación de servicios y datos

Naveguemos hasta la parte superior del archivo server.ts y añadamos unas cuantas sentencias import para las funciones y datos que definiremos:

import { languageList } from "./data/languages.ts";
import { voicesList } from "./data/voices.ts";
import { translateText } from "./services/translate.ts";
import { voicePicker } from "./services/voice_picker.ts";
import { languagePicker } from "./services/language_picker.ts";

Como indica el fragmento anterior, tenemos que crear los cinco elementos siguientes:

  • languageList: Un array de posibles idiomas a los que traducir el mensaje

  • voicesList: Una serie de posibles voces para pronunciar el mensaje traducido.

  • translateText: La función para traducir el texto a la segunda lengua

  • voicePicker: Función que permite elegir la voz con la que se pronunciará el texto traducido.

  • languagePicker: La función para elegir un idioma para traducir el texto en

Ahora construiremos cada uno de ellos.

Definición de los datos

En primer lugar, vamos a añadir algunos datos a nuestra aplicación.

Tenemos que añadir dos datos: una lista de idiomas y una lista de voces que hablen esos idiomas.

La lista de idiomas admitidos procede de la Guía de ASR de Vonage. La lista de nombres de voz también proviene de la Guía de Voice API de Vonage.

Abra el archivo data/languages.ts y le añadiremos un array de objetos:

export const languageList = [
  { "name": "Afrikaans (South Africa)", "code": "af-ZA" },
  { "name": "Albanian (Albania)", "code": "sq-AL" },
  { "name": "Amharic (Ethiopia)", "code": "am-ET" },
  { "name": "Arabic (Algeria)", "code": "ar-DZ" },
  { "name": "Arabic (Bahrain)", "code": "ar-BH" },
  { "name": "Arabic (Egypt)", "code": "ar-EG" },
  { "name": "Arabic (Iraq)", "code": "ar-IQ" },
  { "name": "Arabic (Israel)", "code": "ar-IL" },
  { "name": "Arabic (Jordan)", "code": "ar-JO" },
  { "name": "Arabic (Kuwait)", "code": "ar-KW" },
  { "name": "Arabic (Lebanon)", "code": "ar-LB" },
  { "name": "Arabic (Morocco)", "code": "ar-MA" },
  { "name": "Arabic (Oman)", "code": "ar-OM" },
  { "name": "Arabic (Qatar)", "code": "ar-QA" },
  { "name": "Arabic (Saudi Arabia)", "code": "ar-SA" },
  { "name": "Arabic (State of Palestine)", "code": "ar-PS" },
  { "name": "Arabic (Tunisia)", "code": "ar-TN" },
  { "name": "Arabic (United Arab Emirates)", "code": "ar-AE" },
  { "name": "Armenian (Armenia)", "code": "hy-AM" },
  { "name": "Azerbaijani (Azerbaijan)", "code": "az-AZ" },
  { "name": "Basque (Spain)", "code": "eu-ES" },
  { "name": "Bengali (Bangladesh)", "code": "bn-BD" },
  { "name": "Bengali (India)", "code": "bn-IN" },
  { "name": "Bulgarian (Bulgaria)", "code": "bg-BG" },
  { "name": "Catalan (Spain)", "code": "ca-ES" },
  { "name": "Chinese, Mandarin (Simplified, China)", "code": "zh" },
  { "name": "Croatian (Croatia)", "code": "hr-HR" },
  { "name": "Czech (Czech Republic)", "code": "cs-CZ" },
  { "name": "Danish (Denmark)", "code": "da-DK" },
  { "name": "Dutch (Netherlands)", "code": "nl-NL" },
  { "name": "English (Australia)", "code": "en-AU" },
  { "name": "English (Canada)", "code": "en-CA" },
  { "name": "English (Ghana)", "code": "en-GH" },
  { "name": "English (India)", "code": "en-IN" },
  { "name": "English (Ireland)", "code": "en-IE" },
  { "name": "English (Kenya)", "code": "en-KE" },
  { "name": "English (New Zealand)", "code": "en-NZ" },
  { "name": "English (Nigeria)", "code": "en-NG" },
  { "name": "English (Philippines)", "code": "en-PH" },
  { "name": "English (South Africa)", "code": "en-ZA" },
  { "name": "English (Tanzania)", "code": "en-TZ" },
  { "name": "English (United Kingdom)", "code": "en-GB" },
  { "name": "English (United States)", "code": "en-US" },
  { "name": "Finnish (Finland)", "code": "fi-FI" },
  { "name": "French (Canada)", "code": "fr-CA" },
  { "name": "French (France)", "code": "fr-FR" },
  { "name": "Galician (Spain)", "code": "gl-ES" },
  { "name": "Georgian (Georgia)", "code": "ka-GE" },
  { "name": "German (Germany)", "code": "de-DE" },
  { "name": "Greek (Greece)", "code": "el-GR" },
  { "name": "Gujarati (India)", "code": "gu-IN" },
  { "name": "Hebrew (Israel)", "code": "he-IL" },
  { "name": "Hindi (India)", "code": "hi-IN" },
  { "name": "Hungarian (Hungary)", "code": "hu-HU" },
  { "name": "Icelandic (Iceland)", "code": "is-IS" },
  { "name": "Indonesian (Indonesia)", "code": "id-ID" },
  { "name": "Italian (Italy)", "code": "it-IT" },
  { "name": "Japanese (Japan)", "code": "ja-JP" },
  { "name": "Javanese (Indonesia)", "code": "jv-ID" },
  { "name": "Kannada (India)", "code": "kn-IN" },
  { "name": "Khmer (Cambodia)", "code": "km-KH" },
  { "name": "Korean (South Korea)", "code": "ko-KR" },
  { "name": "Lao (Laos)", "code": "lo-LA" },
  { "name": "Latvian (Latvia)", "code": "lv-LV" },
  { "name": "Lithuanian (Lithuania)", "code": "lt-LT" },
  { "name": "Malay (Malaysia)", "code":  "ms-MY" },
  { "name": "Malayalam (India)", "code": "ml-IN" }, 
  { "name": "Marathi (India)", "code": "mr-IN" },
  { "name": "Nepali (Nepal)", "code":  "ne-NP"},
  { "name": "Norwegian Bokmål (Norway)",  "code": "nb-NO"},
  { "name": "Persian (Iran)", "code":  "fa-IR"},
  { "name": "Polish (Poland)", "code":  "pl-PL"},
  { "name": "Portuguese (Brazil)", "code": "pt-BR"},
  { "name": "Portuguese (Portugal)", "code": "pt-PT"},
  { "name": "Romanian (Romania)", "code": "ro-RO"} ,
  { "name": "Russian (Russia)", "code": "ru-RU" },
  { "name": "Serbian (Serbia)", "code": "sr-RS" },
  { "name": "Sinhala (Sri Lanka)", "code": "si-LK" },
  { "name": "Slovak (Slovakia)", "code": "sk-SK" },
  { "name": "Slovenian (Slovenia)", "code": "sl-SI" },
  { "name": "Spanish (Argentina)", "code": "es-AR" },
  { "name": "Spanish (Bolivia)", "code": "es-BO" },
  { "name": "Spanish (Chile)", "code": "es-CL" },
  { "name": "Spanish (Colombia)", "code": "es-CO" },
  { "name": "Spanish (Costa Rica)", "code":  "es-CR" },
  { "name": "Spanish (Dominican Republic)", "code": "es-DO" },
  { "name": "Spanish (Ecuador)", "code": "es-EC" },
  { "name": "Spanish (El Salvador)", "code": "es-SV" },
  { "name": "Spanish (Guatemala)", "code": "es-GT" },
  { "name": "Spanish (Honduras)", "code": "es-HN" },
  { "name": "Spanish (Mexico)", "code": "es-MX" },
  { "name": "Spanish (Nicaragua)", "code": "es-NI" },
  { "name": "Spanish (Panama)", "code": "es-PA" },
  { "name": "Spanish (Paraguay)", "code": "es-PY" },
  { "name": "Spanish (Peru)", "code": "es-PE" },
  { "name": "Spanish (Puerto Rico)", "code": "es-PR" },
  { "name": "Spanish (Spain)", "code": "es-ES" },
  { "name": "Spanish (United States)", "code": "es-US" },
  { "name": "Spanish (Uruguay)", "code": "es-UY" },
  { "name": "Spanish (Venezuela)", "code": "es-VE" },
  { "name": "Sundanese (Indonesia)", "code": "su-ID" },
  { "name": "Swahili (Kenya)", "code": "sw-KE" },
  { "name": "Swahili (Tanzania)", "code": "sw-TZ" },
  { "name": "Swedish (Sweden)", "code": "sv-SE" },
  { "name": "Tamil (India)", "code": "ta-IN" },
  { "name": "Tamil (Malaysia)", "code": "ta-MY" },
  { "name": "Tamil (Singapore)", "code": "ta-SG" },
  { "name": "Tamil (Sri Lanka)", "code": "ta-LK" },
  { "name": "Telugu (India)", "code": "te-IN" },
  { "name": "Thai (Thailand)", "code": "th-TH" },
  { "name": "Turkish (Turkey)", "code": "tr-TR" },
  { "name": "Ukrainian (Ukraine)", "code": "uk-UA" },
  { "name": "Urdu (India)", "code": "ur-IN" },
  { "name": "Urdu (Pakistan)", "code": "ur-PK" },
  { "name": "Vietnamese (Vietnam)", "code": "vi-VN" },
  { "name": "Zulu (South Africa)", "code": "zu-ZA" }
]

Esta es la lista de idiomas admitidos en el momento de la publicación de este tutorial. La lista está sujeta a cambios, por lo que debe consultarse la guía del sitio web para obtener la información más actualizada.

A continuación, abra el archivo data/voices.ts y añádele también una matriz de objetos. Al igual que la lista de idiomas, aquí los datos representan la lista de nombres de voces en el momento de la publicación. A menudo hay más de una Voz por idioma. Por el bien de este tutorial, hemos eliminado las voces de idiomas duplicados para mantenerlo a una voz por idioma:

export const voicesList = [
  { "name": "Salli", "code": "en-US" },
  { "name": "Marlene", "code": "de-DE" },
  { "name": "Nicole", "code": "en-AU" },
  { "name": "Gwyneth", "code": "en-GB" },
  { "name": "Geraint", "code": "cy-GB" },
  { "name": "Raveena", "code": "en-IN" },
  { "name": "Conchita", "code": "es-ES" },
  { "name": "Penelope", "code": "es-US" },
  { "name": "Chantal", "code": "fr-CA" },
  { "name": "Mathieu", "code": "fr-FR" },
  { "name": "Aditi", "code": "hi-IN" },
  { "name": "Dora", "code": "is-IS" },
  { "name": "Carla", "code": "it-IT" },
  { "name": "Liv", "code": "nb-NO" },
  { "name": "Lotte", "code": "nl-NL" },
  { "name": "Jacek", "code": "pl-PL" },
  { "name": "Vitoria", "code": "pt-BR" },
  { "name": "Ines", "code": "pt-PT" },
  { "name": "Carmen", "code": "ro-RO" },
  { "name": "Tatyana", "code": "ru-RU" },
  { "name": "Astrid", "code": "sv-SE" },
  { "name": "Filiz", "code": "tr-TR" },
  { "name": "Mizuki", "code": "ja-JP" },
  { "name": "Seoyeon", "code": "ko-KR" },
  { "name": "Laila", "code": "ara-XWW" },
  { "name": "Damayanti", "code": "ind-IDN" },
  { "name": "Miren", "code": "baq-ESP" },
  { "name": "Sin-Ji", "code": "yue-CHN" },
  { "name": "Jordi", "code": "cat-ESP" },
  { "name": "Montserrat", "code": "cat-ESP" },
  { "name": "Iveta", "code": "ces-CZE" },
  { "name": "Tessa", "code": "eng-ZAF" },
  { "name": "Satu", "code": "fin-FIN" },
  { "name": "Melina", "code": "ell-GRC" },
  { "name": "Carmit", "code": "heb-ISR" },
  { "name": "Lekha", "code": "hin-IND" },
  { "name": "Mariska", "code": "hun-HUN" },
  { "name": "Sora", "code": "kor-KOR" },
  { "name": "Tian-Tian", "code": "cmn-CHN" },
  { "name": "Mei-Jia", "code": "cmn-TWN" },
  { "name": "Nora", "code": "nor-NOR" },
  { "name": "Henrik", "code": "nor-NOR" },
  { "name": "Felipe", "code": "por-BRA" },
  { "name": "Joana", "code": "por-PRT" },
  { "name": "Ioana", "code": "ron-ROU" },
  { "name": "Laura", "code": "slk-SVK" },
  { "name": "Alva", "code": "swe-SWE" },
  { "name": "Kanya", "code": "tha-THA" },
  { "name": "Yelda", "code": "tur-TUR" },
  { "name": "Empar", "code": "spa-ESP" }
]

Definición de las funciones de servicio

Nuestra aplicación utiliza varias funciones definidas en services/ que proporcionan la funcionalidad básica. Vamos a construir cada uno de ellos en este punto.

Para utilizar la API de traducción de voz de Microsoft Azure, debemos autenticarnos en la API mediante un proceso de dos pasos. El primer paso consiste en obtener un token web JSON (JWT) desde el punto final de la API de creación de tokens que luego utilizaremos en el segundo paso cuando realicemos una llamada HTTP al punto final de la API de traducción.

Abrimos el fichero services/auth/token.ts y en él crearemos la funcionalidad para obtener un JWT de Azure. Ten en cuenta que esto depende de que hayas creado correctamente una Account en Microsoft Azure y hayas recibido tu API key. La función lee la clave API desde una variable de entorno en nuestro archivo .env que definiremos más adelante en este tutorial:

import "https://deno.land/x/dotenv/load.ts";
const azureEndpoint: any = Deno.env.get("AZURE_ENDPOINT");
var data;
 export const getToken = async (key: string | undefined) => {
  if (!key) {
    console.log("You are missing your Azure Subscription Key. You must add it as an environment variable.");
    return;
  };
  if (!azureEndpoint) {
    console.log("You are missing your Azure endpoint definition. You must add it as an environment variable.");
  };
  data = await fetch(`${azureEndpoint.toString()}sts/v1.0/issuetoken`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Content-length': '0',
      'Ocp-Apim-Subscription-Key':key.toString()
    }
  })
  var text = await data.text();
  return text; 
};

La función getToken() acepta un parámetro key parámetro, y junto con el punto final de la URL de Microsoft Azure definido en su archivo dotenv, realiza una petición fetch() solicitud enviando su clave de API. El valor que se devuelve es el JWT que se devuelve explícitamente como valor de la función. En la parte superior del archivo, importamos un módulo cargador dotenv de Deno que nos permite leer los valores del archivo .env archivo.

Si el key es undefined o si no hay ningún valor para azureEndpoint, la función regresará antes de tiempo y proporcionará una explicación en la consola de lo que faltaba.

Una vez que tenemos el token de getToken()podemos utilizarlo para crear una función de ayuda que llame a la API de traducción y obtenga el texto traducido.

Abrimos el archivo services/translate.ts y en ese archivo crearemos una translateText() función:

import { getToken } from './auth/token.ts';
import "https://deno.land/x/dotenv/load.ts";
const azureSubscriptionKey: string | undefined = Deno.env.get("AZURE_SUBSCRIPTION_KEY");
 export const translateText = async (languageCode: string, text: string) => {
  const token =  await getToken(azureSubscriptionKey);
  const response = await fetch(`https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&from=en&to=${languageCode}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`
    },
    body: JSON.stringify([{"text": text}])
  });
  var translation = await response.json();
  return translation[0][<any>"translations"][0][<any>"text"]
};

Esta función, como la anterior, lee de nuestro archivo .env para obtener la clave de la API de Azure que hemos definido. Toma dos argumentos: el código de idioma de dos letras y la conversión de voz a texto.

A continuación, la función crea dos variables token y response. La primera invoca la función getToken() pasando la clave de la API de Azure como argumento. La segunda invoca una fetch() POST al punto final de la API de traducción de voz de Azure utilizando el código de idioma de dos letras como parte de los parámetros de consulta. El JWT generado por la función getToken() se pasa al encabezado Authorization encabezado. La dirección body de la POST solicitud se convierte en una cadena JSON.

La respuesta de la solicitud se guarda en la variable translation y el texto traducido es devuelto por la función translation[0]["translations][0]["text].

Nos quedan dos funciones por crear antes de pasar a definir nuestras .env variables de entorno.

La primera de las dos funciones restantes que construiremos elegirá aleatoriamente un idioma de la lista de idiomas para traducir el texto.

Abra services/language_picker.ts y añade el siguiente código:

export const languagePicker = (languages: any) => {
 const language = languages[Math.floor(Math.random() * languages.length)];
 return language;
}

La función utiliza un poco de matemáticas para elegir al azar un índice de la lista de idiomas y devolver el valor del objeto en ese índice.

La última función que crearemos elegirá una voz de Vonage en la que hablar el idioma traducido, si existe una para ese idioma. Si no existe, devolverá la voz Salli Voice, que representa el inglés americano. También garantizamos que si el idioma elegido es uno de los dialectos regionales del árabe, la voz elegida será una de las voces árabes de Vonage.

Abre services/voice_picker.ts y añade lo siguiente:

var voiceChoice: any = { "name": "Salli", "code": "en-US" }
 export const voicePicker = (voices: Array<object>, language: any) => {
  voiceChoice = voices.find((voice: any) => voice.code === language.code)
  if (language.code.split('-')[0] === 'ar') {
    voiceChoice = { "name": "Laila", "code": "ara-XWW" }
  };
  if (voiceChoice === undefined) {
    voiceChoice = { "name": "Salli", "code": "en-US" }
  };
  return voiceChoice.name;
};

Eso es todo en cuanto a funciones. Si has llegado hasta aquí, ya casi hemos llegado a la meta.

Los últimos elementos de los que debemos ocuparnos son asignar los valores a nuestras variables de entorno y aprovisionar el número de teléfono virtual de Vonage. .env variables de entorno y aprovisionar un número de teléfono virtual de Vonage.

Definición de las variables de entorno

Hay tres valores que debemos asignar en el archivo .env archivo:

  • CLAVE_SUSCRIPCIÓN_AZURE

  • AZURE_ENDPOINT

  • VONAGE_ASR_WEBHOOK

Los dos primeros son nuestra clave de API de Azure y nuestro punto final de URL de Azure, respectivamente.

Este último es la URL del webhook para los datos devueltos por la función de reconocimiento automático de voz de Vonage. Este último valor debe ser una URL accesible externamente. Una buena herramienta para usar durante el desarrollo es ngrok para hacer que tu entorno local esté disponible externamente. Puedes encontrar una guía para configurar ngrok localmente en nuestro sitio web para desarrolladores.

Cómo aprovisionar un número de teléfono virtual de Vonage

Existen dos maneras de aprovisionar un número de teléfono virtual de Vonage. Una vez que tengas una cuenta de desarrollador de Vonage puedes comprar un número de teléfono a través del panel o usando la CLI de Vonage. Lo haremos aquí usando la CLI.

Para instalar la CLI puedes usar yarn o npm: yarn global add @vonage/cli o npm install @vonage/cli -g. Después de la instalación necesitas proporcionarle tus credenciales API obtenidas del dashboard:

vonage config:set --apiKey=VONAGE_API_KEY --apiSecret=VONAGE_API_SECRET

Una vez configurada la CLI, puede utilizarla para buscar los números disponibles en su país. Para ello, ejecute lo siguiente utilizando el código de dos letras de su país. El ejemplo siguiente muestra una búsqueda de números en Estados Unidos. Asegúrese de añadir el indicador --features=VOICE para que sólo aparezcan los números habilitados para voz:

vonage numbers:search US --features=VOICE

Una vez que haya encontrado el número que desea, puede adquirirlo también con la CLI:

vonage numbers:buy NUMBER COUNTRYCODE

Se le pedirá que escriba confirm después de enviar el comando para comprar oficialmente el número.

Como estamos creando una aplicación de voz, también debemos crear una aplicación de Vonage. Esto también se puede hacer con la CLI y, una vez finalizado, podemos vincular el número de teléfono recientemente aprovisionado a la aplicación. También puedes utilizar la creación de la aplicación para suministrarle las URL del webhook de respuesta y del webhook de evento. Si está creando en desarrollo, ahora es un buen momento para crear su servidor ngrok y suministrar las URLs ngrok:

vonage apps:create APP_NAME --voice_answer_url=https://www.example.com/answer --voice_event_url=https://www.example.com/event

El comando le devolverá el ID de la aplicación: Application ID: asdasdas-asdd-2344-2344-asdasdasd345. Ahora utilizaremos ese ID para vincular la aplicación al número de teléfono:

vonage apps:link APP_ID --number=YOUR_VONAGE_NUMBER

Una vez finalizados estos comandos, ¡ya está listo para ejecutar su aplicación!

Ejecutar la aplicación

Para utilizar su aplicación, inicie tanto su servidor ngrok como su servidor web Deno. Para iniciar la aplicación Deno ejecute lo siguiente desde la carpeta raíz:

deno run --allow-read --allow-env --allow-net server.ts

Ahora que está en funcionamiento, puedes llamar a tu número de teléfono provisto de Vonage y seguir las indicaciones para decir un mensaje. Tu mensaje se convertirá en texto usando la función de reconocimiento automático de voz de Vonage y luego se traducirá a un segundo idioma al azar usando Microsoft Azure y luego se te devolverá. ¡Que lo disfrutes!

Compartir:

https://a.storyblok.com/f/270183/384x384/e5480d2945/ben-greenberg.png
Ben GreenbergAntiguos alumnos de Vonage

Ben es un desarrollador de segunda carrera que anteriormente pasó una década en los campos de la educación de adultos, la organización comunitaria y la gestión de organizaciones sin ánimo de lucro. Trabajó como defensor de los desarrolladores para Vonage. Escribe regularmente sobre la intersección entre el desarrollo comunitario y la tecnología. Originario del sur de California y residente durante mucho tiempo en Nueva York, Ben reside ahora cerca de Tel Aviv (Israel).