
Compartir:
Michael es un ingeniero de software políglota, empeñado en reducir la complejidad de los sistemas y hacerlos más predecibles. Trabaja con una gran variedad de lenguajes y herramientas, y comparte sus conocimientos técnicos con audiencias de todo el mundo en grupos de usuarios y conferencias. En el día a día, Michael es un antiguo defensor de los desarrolladores en Vonage, donde pasaba su tiempo aprendiendo, enseñando y escribiendo sobre todo tipo de tecnología.
Controla tu presupuesto con Dial YNAB
Tiempo de lectura: 16 minutos
Entre pagar la hipoteca, ahorrar un fondo de emergencia y comprar demasiados juegos de mesa, me resultaba difícil saber adónde iba todo mi dinero cada mes. Por suerte, descubrí Necesitas un presupuesto (YNAB) que me permite asignar dinero a distintas categorías cada mes y controlar cuánto hay en cada una de ellas.
Su aplicación móvil y su sitio web son bastante buenos, pero cuando vi que YNAB había lanzado recientemente una API me hizo pensar en otras formas de acceder a los datos de mi presupuesto. La inspiración no tardó en llegar.
Hace años que puedes llamar a tu banco para consultar el saldo de tu Account, pero a mí eso no me sirve. El saldo total no refleja el dinero que ya se ha destinado a una compra futura. En cambio, quería llamar a un número y averiguar cuánto me quedaba en la categoría de juegos de mesa, y así, dial-ynab nació.
Visión general
En este post vamos a construir una aplicación node.js que utiliza la plataforma Nexmo para hacer lo siguiente:
Recibir una llamada de voz.
Introducir los datos de audio en la API de conversión de voz a texto de Google
Consulta la API de YNAB para averiguar el saldo actual de la categoría solicitada.
Utilice la función de texto a voz de Nexmo para decir el saldo de nuevo en la llamada.
Dial YNAB Sequence Diagram
Para lograrlo, tendremos que seguir los siguientes pasos:
Bootstrap un proyecto Node.js con
expressyexpress-wsConfigurar una aplicación Nexmo
Obtener credenciales de autenticación para Google Cloud y YNAB
Gestionar una llamada entrante con Nexmo
Conectar la llamada a nuestra aplicación mediante un websocket
Pasar los datos de audio de Nexmo a Google para su transcripción
Manejar los datos transcritos devueltos por Google
Consultar los saldos de nuestras cuentas corrientes en YNAB
Repita el saldo en la llamada utilizando la función de texto a voz de Nexmo.
Hay mucho ahí, ¡así que deberíamos empezar!
Requisitos previos
Para realizar este tutorial necesitarás lo siguiente:
node.js (estoy ejecutando la versión 10.0.0) y npm instalado
ngrok para exponer su aplicación local a Internet para que Nexmo pueda llegar a ella
nexmo-cli disponible (esto es opcional, ya que puede realizar las mismas tareas a través del panel de Nexmo)
Credenciales de Google y YNAB (las veremos más adelante)
Vonage API Account
To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.
Una vez que tengas todo a mano, inicia un ngrok túnel ejecutando ngrok http 3000 y anota la URL (en mi caso, es http://e7dddad9.ngrok.io). Cada vez que veas una ngrok URL en este post, cámbiala por la tuya.
dial ynab ngrok
Arrancar un proyecto
Empecemos creando una carpeta llamada dial-ynab y cambiando de directorio en ella. Para iniciar nuestro proyecto necesitamos ejecutar npm init e instalar algunas dependencias:
No necesitamos todas estas dependencias para empezar, pero es más fácil instalarlas todas por adelantado para no tener que preocuparnos de ellas más tarde.
Creación de una aplicación Nexmo
Antes de que podamos manejar una llamada entrante tenemos que crear una aplicación Nexmo y vincular un número a la misma. Vamos a utilizar la herramienta Nexmo CLI para lograr esto, pero también puede crear una aplicación y vincular un número a la misma en el tablero de instrumentos si lo prefiere.
Una vez que hayas hecho esto, cada vez que se realice una llamada al número que has comprado Nexmo hará una GET solicitud a http://e7dddad9.ngrok.io/webhook/answer para saber cómo gestionar la llamada. Vamos a implementar ese endpoint ahora usando Express.
Gestionar una llamada entrante
Hay un montón de código necesario para arrancar nuestra instancia Express. Cree un archivo llamado index.js con el siguiente contenido, que registrará dotenv para los valores de configuración y creará una instancia express instancia sin ninguna ruta definida:
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const expressWs = require('express-ws')(app);
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// Routes go here
app.listen(process.env.PORT, function () {
console.log(`dial-ynab listening on port ${process.env.PORT}!`);
});También hemos referenciado una variable llamada process.env.PORT pero aún no la hemos definido. Cree un archivo llamado .env con el siguiente contenido para hacerlo:
La última parte del rompecabezas es definir nuestra /webhooks/answer URL. Esto se hace definiendo un método app.get() justo antes de llamar a app.listen(). Cuando Nexmo hace una petición a nuestra aplicación, espera que devolvamos un método NCCO. En este caso, devolvemos una talk que dará una respuesta a la persona que llama utilizando Text-To-Speech:
app.get('/webhooks/answer', function (req, res) {
return res.json([
{
"action": "talk",
"text": "This is a text to speech demo from Nexmo. Thanks for calling"
}
]);
});Eso es todo lo que necesita para gestionar una llamada entrante con Nexmo. Pruébelo ejecutando node index.jsy, a continuación, llame al número que ha adquirido anteriormente. Deberá oír This is a text to speech demo from Nexmo. Thanks for calling antes de que finalice la llamada.
Enhorabuena. Ya has hecho la parte difícil, el resto de este post es sólo la conexión de algunos servicios externos diferentes.
Configuración de servicios
Antes de continuar con el resto del post, necesitamos credenciales de autenticación para Google Cloud Speech y You Need A Budget.
Para Google Cloud Speech necesita crear una clave de cuenta de servicio y descargar las credenciales como JSON. Cree una nueva cuenta de servicio, llámela dial-ynab y asígnale el Project->Owner rol. Querrás crear un rol IAM específico para desplegar en producción, pero por ahora esta es la forma más fácil de empezar. Descarga el archivo de credenciales, renómbralo a google-creds.json y colócalo en la carpeta del proyecto junto a index.js.
YNAB Las credenciales API son un poco más fáciles de encontrar. Puede generar un token de acceso personal en su configuración de la Account. También necesitarás tu ID de presupuesto, que puedes encontrar visitando la interfaz web y copiando el ID en la URL (tendrá un aspecto similar a 58f1ca9a-abcd-123a-96ef-21aac7e2865c)
En este punto deberías tener:
ID de la aplicación Nexmo
Credenciales de aplicaciones de Google
ID del presupuesto de YNAB y token de acceso
Añadámoslos a nuestro archivo .env para poder utilizarlos en nuestra aplicación:
NEXMO_PRIVATE_KEY y GOOGLE_APPLICATION_CREDENTIALS son rutas a archivos que existen en nuestra carpeta de proyecto junto a index.js que contienen nuestras credenciales.
Conéctese a nuestros WebSockets
Ahora que podemos gestionar una llamada entrante y tenemos nuestras credenciales de Google, es hora de enviar el audio de la llamada telefónica al servicio de transcripción de Google. Esto se hace usando dos websockets: uno desde Nexmo a nuestra aplicación y otro desde nuestra aplicación a Google.
Empecemos por cambiar nuestro /webhooks/answer para utilizar una acción connect acción. Esto le dice a Nexmo que se conecte al /transcription en nuestra aplicación usando un websocket. También le decimos que pase el UUID de la llamada al websocket usando la opción headers ya que lo necesitaremos más adelante.
Sustituya su /webhooks/answer con lo siguiente:
app.get('/webhooks/answer', function (req, res) {
return res.json([
{
"action": "talk",
"text": "Please say the name of the category you would like the balance for"
},
{
"action": "connect",
"endpoint": [
{
"type": "websocket",
"content-type": "audio/l16;rate=8000",
"uri": `ws://${req.get('host')}/transcription`,
"headers": {
"user": req.query.uuid
}
}
]
}
]);
});Además de decirle a Nexmo que se conecte a /transcriptionnecesitamos crear un endpoint que escuche una conexión websocket. Aquí es donde entra en juego el paquete express-ws entra en juego. Añade un método app.ws() como una envoltura alrededor de un servidor websocket. Añade lo siguiente debajo de tu método app.get() método:
app.ws('/transcription', function(ws, req) {
let UUID;
ws.on('message', function(msg) {
});
ws.on('close', function(){
});
});El primer mensaje recibido de Nexmo será un mensaje JSON que contendrá cualquier headers que hayamos solicitado en la OCNC (en este caso, el UUID de llamada) y todos los mensajes posteriores serán buffers de datos de audio. Podemos utilizar este conocimiento para implementar ws.on('message')si el mensaje es un buffer lo reenviamos a Google, en caso contrario almacenamos el UUID para más tarde.
let UUID;
ws.on('message', function(msg) {
if (!Buffer.isBuffer(msg)) {
let data = JSON.parse(msg);
UUID = data.user;
return;
}
}); Gestión de una transcripción de Google
Antes de poder enviar los datos de audio a Google, tenemos que configurar una instancia de su cliente de voz en la nube. Añade lo siguiente al principio del archivo justo después de require('dotenv').config();
const Speech = require('@google-cloud/speech');
const speech = new Speech.SpeechClient();
const googleConfig = {
config: {
encoding: 'LINEAR16',
sampleRateHertz: 8000,
languageCode: 'en-GB'
},
interimResults: false
};Esto crea una nueva instancia del cliente de voz en la nube para que lo utilicemos. Las opciones de configuración proporcionadas funcionan bien con Nexmo, pero es posible que desee cambiar languageCode si usted está hablando de otra cosa que en-GB. Puedes encontrar una lista completa de los idiomas soportados en la documentación de Google Cloud Speech docs.
Para utilizar la función de voz a texto del SpeechClientutilizaremos el método speech.streamingRecognize() método. Actualizar app.ws('/transcription') y creamos una nueva instancia de speech.streamingRecognize cada vez que se reciba una nueva conexión websocket:
app.ws('/transcription', function(ws, req) {
let UUID;
const speechStream = speech.streamingRecognize(googleConfig)
.on('error', console.log)
.on('data', async (data) => {
if (!data.results) { return; }
const translation = data.results[0].alternatives[0];
console.log(translation.transcript);
});
ws.on('message', function(msg) {
Puede observar que en el método .on('data') registramos los resultados de data.results[0].alternatives[0].transcript. Este es el texto transcrito devuelto por Google. Sabemos que el primer elemento devuelto es siempre la traducción final, ya que establecimos interimResults: false en nuestra configuración.
Como hemos creado una nueva speech.streamingRecognize() también debemos limpiar la instancia cuando se desconecte nuestra llamada. Para ello, destruiremos nuestra instancia speechStream en el método ws.on('close') método
ws.on('close', function(){
speechStream.destroy();
});Lo último que hay que hacer es actualizar ws.on('message') para reenviar los datos a speechStream si se trata de un búfer.
ws.on('message', function(msg) {
if (!Buffer.isBuffer(msg)) {
let data = JSON.parse(msg);
UUID = data.user;
return;
}
speechStream.write(msg);
});Si ejecuta su aplicación (node index.js) y llamas a tu número Nexmo deberías poder hablar en la llamada y ver el texto transcrito en la consola en tiempo real.
Conectar con YNAB
Ahora que ya tenemos la transcripción funcionando, lo siguiente que tenemos que hacer es obtener los datos de nuestro presupuesto YNAB. En la parte superior del archivo (después de crear el objeto googleConfig añade lo siguiente para crear un cliente ynab cliente API:
const ynabClient = require("ynab");
const ynab = new ynabClient.API(process.env.YNAB_ACCESS_TOKEN);Podemos conectarnos a la API de YNAB utilizando este cliente y listar todos nuestros grupos de categorías y categorías. Como no estamos interesados en los grupos maestros, sólo en las categorías en sí, podemos construir una lista de nombres de categorías y saldos utilizando la siguiente función. Añade esto al final de tu archivo:
async function fetchYnabBalanceData() {
let r = await ynab.categories.getCategories(process.env.YNAB_BUDGET_ID);
return r.data.category_groups.reduce((acc, v) => acc.concat(
v.categories.map((c) => { return {"name":c.name, "balance":c.balance/1000}; })
), []);
}
Esto obtiene todas las categorías de YNAB y devuelve una lista en el siguiente formato:
[
{ name: 'Dining Out', balance: 38.11 },
{ name: 'Gaming', balance: 12.74 },
{ name: 'Music', balance: 43.85 },
{ name: 'Fun Money', balance: -13.44 }
]Utilizaremos este método fetchYnabBalanceData() en nuestra función .on('data') cuando recibamos una transcripción para hacer coincidir lo que se dijo con un nombre de categoría. Por desgracia, es muy poco probable que lo que Google devuelva coincida exactamente con el nombre de la categoría. Tenemos que ser un poco creativos para averiguar qué categoría quería la persona que llama. Para ello, podemos utilizar el paquete fast-levenshtein que hemos instalado antes.
Para averiguar qué categoría quería nuestro interlocutor, podemos tomar la entrada (needle) y buscar en cada nombre de categoría (haystack), utilizando fast-levenshtein para calcular el menor número de cambios de letra necesarios para que un nombre de categoría coincida con nuestra entrada. Esta es una aproximación rudimentaria, pero funciona lo suficientemente bien para nuestras necesidades. Añada lo siguiente al final de su archivo function fetchYnabBalanceData():
function findClosestName(needle, haystack) {
needle = needle.toLowerCase();
let shortestDistance = {"value": [], "distance": Number.MAX_SAFE_INTEGER};
for (let k of haystack) {
let name = k.name.toLowerCase();
if (needle == name) {
return k;
}
let distance = levenshtein.get(needle, name);
if (distance < shortestDistance.distance) {
shortestDistance.value = k;
shortestDistance.distance = distance;
}
}
return shortestDistance.value;
}
También necesitará el paquete fast-levenshtein al principio del archivo. Añádelo justo después de require('dotenv').config():
const levenshtein = require('fast-levenshtein');Ahora tenemos todo lo que necesitamos para actualizar nuestra función .on('data') para registrar una categoría y un saldo en la consola:
const speechStream = speech.streamingRecognize(googleConfig)
.on('error', console.log)
.on('data', async (data) => {
if (!data.results) { return; }
const translation = data.results[0].alternatives[0];
console.log(translation.transcript);
const categories = await fetchYnabBalanceData();
const category = findClosestName(translation.transcript, categories);
console.log(category);
});
Este es un buen momento para ejecutar su aplicación de nuevo (node index.js) y llama a tu número Nexmo para probar tu código. Prueba a decir "Comer fuera" y observa cómo te devuelve tu categoría "Cenar fuera".
Volver a hablar en la llamada
Sólo queda una cosa por hacer para terminar nuestro dial-ynab proyecto: hacer que vuelva a leer el saldo de la categoría en la llamada mediante Text-To-Speech.
Para ello, tendremos que utilizar el paquete nexmo paquete. No necesita un apiKey o apiSecret para utilizar la Voice API, así que siéntete libre de ignorar esos valores. Para acceder a la Voice API tenemos que proporcionar los valores applicationId y privateKey que acabamos de añadir a nuestro archivo .env anteriormente.
Añade el siguiente código justo debajo require('fast-levenshtein') en la parte superior de su archivo:
const Nexmo = require('nexmo');
const nexmo = new Nexmo({
apiKey: 'unused',
apiSecret: 'unused',
applicationId: process.env.NEXMO_APPLICATION_ID,
privateKey: process.env.NEXMO_PRIVATE_KEY,
});A continuación, actualice su método .on('data') para llamar a la API de Nexmo añadiendo el siguiente código console.log(category);:
const balanceText = `${category.name} has ${category.balance} available.`;
nexmo.calls.talk.start(UUID, { text: balanceText }, (err, res) => {
if(err) { console.error(err); }
});
Si vuelves a llamar a tu número Nexmo, oirás la lectura del saldo de la categoría. Sin embargo, el saldo de la categoría no suena del todo bien, ya que se lee como un número decimal. Podemos indicar al motor de texto a voz que se trata de un valor monetario utilizando SSML. Actualice su balanceText por lo siguiente:
const balanceText = `<speak>${category.name} has <say-as interpret-as="vxml:currency">GBP${category.balance}</say-as> available</speak>`;Llame por última vez a su número Nexmo y oirá que el número se ha interpretado como moneda gracias a interpret-as="vxml:currency".
Conclusión
En poco menos de 125 líneas de código hemos creado una aplicación que te permite llamar a tu presupuesto YNAB y asegurarte de que te queda suficiente en la categoría de salir a cenar antes de salir después de que se te antoje tu comida para llevar favorita.
Hemos conectado Nexmo, Google y YNAB utilizando sus API y websockets para proporcionar transcripción de llamadas en tiempo real y comentarios de audio en una llamada de voz activa. No sé a ti, pero a mí me parece alucinante.
Si desea obtener más información sobre la Voice API de Nexmo, puede consultar la sección Visión general de Voice API es un buen punto de partida. Puede que le interese especialmente la referencia NCCO o la guía de conceptos de websockets.
Para hablar sobre este post, la Voice API de Nexmo o la comunicación en general, no dudes en unirte a la Comunidad Nexmo Slackdonde el @NexmoDev están listos y esperando para ayudar.
Crédito de bonificación
¿Sigues leyendo? ¡Excelente! Mi parte favorita de todo este post es que la única parte específica de YNAB es el fetchYnabBalanceData método. Sería trivial hacer que esto funcionara con la función de macetas de Monzo en lugar de YNAB. De hecho, ¡hagámoslo ahora!
En primer lugar, obtén tu token de acceso a Monzo en el Monzo Playground y añádelo a .env:
Vamos a utilizar la biblioteca request-promise para acceder a la API de Monzo, así que vamos a instalarla ahora
Añada lo siguiente al final de su archivo para definir la función fetchMonzoBalanceData función. La API de Monzo devuelve datos que contienen name y balance por lo que todo lo que tenemos que hacer es reformatear el saldo para que sea moneda decimal:
const request = require("request-promise");
async function fetchMonzoBalanceData() {
const data = JSON.parse(await request({"uri": "https://api.monzo.com/pots", "headers": {"Authorization": `Bearer ${process.env.MONZO_ACCESS_TOKEN}`}}));
return data.pots.map((v) => { v.balance = v.balance/100; return v; });
}
Por último, cambie la llamada a fetchYnabBalanceData para que llame a fetchMonzoBalanceData en su lugar. Ahora llama a tu número Nexmo y di el nombre de uno de tus botes Monzo. ¡Enhorabuena! Ahora estás trabajando con la API de Monzo en lugar de YNAB con sólo 6 líneas de código adicional.
Compartir:
Michael es un ingeniero de software políglota, empeñado en reducir la complejidad de los sistemas y hacerlos más predecibles. Trabaja con una gran variedad de lenguajes y herramientas, y comparte sus conocimientos técnicos con audiencias de todo el mundo en grupos de usuarios y conferencias. En el día a día, Michael es un antiguo defensor de los desarrolladores en Vonage, donde pasaba su tiempo aprendiendo, enseñando y escribiendo sobre todo tipo de tecnología.
