https://a.storyblok.com/f/270183/145891/383bf3db42/tw_dialynab.png

Acompanhe seu orçamento com o Dial YNAB

Publicado em April 29, 2021

Tempo de leitura: 17 minutos

Entre pagar a hipoteca, economizar para um fundo de emergência e comprar jogos de tabuleiro em excesso, eu costumava ter muita dificuldade em acompanhar para onde todo o meu dinheiro estava indo a cada mês. Felizmente, descobri o o You Need A Budget (YNAB) há alguns anos, que me permite distribuir o dinheiro por diferentes categorias a cada mês e acompanhar quanto há em cada uma delas.

O aplicativo móvel e o site deles são bem bons, mas quando vi que o YNAB havia lançado recentemente uma API, isso me fez pensar em outras maneiras de acessar os dados do meu orçamento. Não demorou muito para que a inspiração surgisse.

Há anos você consegue ligar para o seu banco para verificar o saldo da sua conta, mas isso não me serve. O saldo total não reflete o dinheiro que já foi reservado para uma compra futura. Em vez disso, eu queria ligar para um número e saber quanto ainda tinha na minha categoria de jogos de tabuleiro e, por isso, o dial-ynab nasceu.

Visão geral

Nesta postagem, vamos criar um aplicativo em Node.js que utiliza a plataforma Nexmo para realizar o seguinte:

  1. Receber uma chamada de voz.

  2. Envie os dados de áudio para a API de conversão de fala em texto do Google

  3. Consulte a API do YNAB para verificar o saldo atual da categoria solicitada.

  4. Utilize a funcionalidade de conversão de texto em fala da Nexmo para informar o saldo durante a ligação.

Dial YNAB Sequence DiagramDial YNAB Sequence Diagram

Para isso, precisaremos seguir as etapas a seguir:

  1. Inicie um projeto Node.js com express e express-ws

  2. Configurar um aplicativo Nexmo

  3. Obter credenciais de autenticação para o Google Cloud e o YNAB

  4. Atender uma chamada recebida usando o Nexmo

  5. Conecte a chamada ao nosso aplicativo usando um WebSocket

  6. Envie os dados de áudio do Nexmo ao Google para transcrição

  7. Processar os dados transcritos retornados pelo Google

  8. Buscar os saldos atuais das nossas contas correntes no YNAB

  9. Diga o saldo de volta para a chamada usando a funcionalidade de conversão de texto em fala da Nexmo

Tem muita coisa aí, então vamos começar!

Pré-requisitos

Para seguir este tutorial, você precisará do seguinte:

  • node.js (estou usando a versão 10.0.0) e o npm instalado

  • ngrok para disponibilizar seu aplicativo local na internet, de modo que a Nexmo possa acessá-lo

  • nexmo-cli disponível (isso é opcional, pois você pode realizar as mesmas tarefas pelo painel do Nexmo)

  • Credenciais do Google e do YNAB (falaremos sobre elas mais tarde)

Quando tiver tudo à mão, inicie um ngrok túnel executando ngrok http 3000 e anote a URL (no meu caso, é http://e7dddad9.ngrok.io). Sempre que encontrar um ngrok URL nesta postagem, substitua-a pela sua própria.

dial ynab ngrokdial ynab ngrok

Iniciar um projeto

Vamos começar criando uma pasta chamada dial-ynab e mudando para essa pasta. Para iniciar nosso projeto, precisamos executar npm init e instalar algumas dependências:

npm init -y npm install nexmo dotenv express express-ws @google-cloud/speech ynab fast-levenshtein --save

Não precisamos de todas essas dependências logo de início, mas é mais fácil instalá-las todas de uma vez para não precisarmos nos preocupar com elas mais tarde.

Criação de uma aplicação Nexmo

Antes de podermos atender uma chamada recebida, precisamos criar um aplicativo Nexmo e vincular um número a ele. Usaremos a ferramenta CLI do Nexmo para fazer isso, mas você também pode criar um aplicativo e associar um número a ela no painel de controle, se preferir.

# Create an application, make a note of the application ID returned nexmo app:create "DialYnab" http://e7dddad9.ngrok.io/webhooks/answer http://e7dddad9.ngrok.io/webhooks/event --keyfile private.key # => Application created: aaaaaaaa-bbbb-cccc-dddd-0123456789ab # Purchase a number to use with our application nexmo number:buy -c GB # => Number purchased: 447700900000 # Link the number to our application nexmo link:app 447700900000 aaaaaaaa-bbbb-cccc-dddd-0123456789ab # => Number updated

Depois de fazer isso, sempre que for feita uma ligação para o número que você adquiriu, a Nexmo enviará uma GET solicitação http://e7dddad9.ngrok.io/webhook/answer para saber como lidar com a chamada. Vamos implementar esse endpoint agora usando o Express.

Atender uma chamada recebida

É necessário bastante código para inicializar nossa instância do Express. Crie um arquivo chamado index.js com o seguinte conteúdo, que registrará dotenv os valores de configuração e criará uma express instância sem nenhuma rota 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}!`);
});

Também fizemos referência a uma variável chamada process.env.PORT mas ainda não a definimos. Para fazer isso, crie um arquivo chamado .env com o seguinte conteúdo para fazer isso:

PORT=3000

A parte final do quebra-cabeça é definir nossa /webhooks/answer URL. Isso é feito definindo um app.get() método logo antes de chamarmos app.listen(). Quando a Nexmo faz uma solicitação ao nosso aplicativo, ela espera que retornemos um NCCO. Nesse caso, retornamos uma talk ação que reproduzirá uma resposta para o chamador usando a tecnologia 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"
        }
    ]);
});

É tudo o que você precisa para atender uma chamada recebida com o Nexmo. Experimente executando node index.jse, em seguida, ligue para o número que você comprou anteriormente. Você deverá ouvir This is a text to speech demo from Nexmo. Thanks for calling antes que a ligação seja encerrada.

Parabéns! Você já superou a parte mais difícil — o restante deste post trata apenas de integrar alguns serviços externos diferentes

Configuração de serviços

Antes de continuarmos com o restante da postagem, precisamos das credenciais de autenticação para o Google Cloud Speech e o You Need A Budget.

Para o Google Cloud Speech , você precisa criar uma chave de conta de serviço e baixar as credenciais no formato JSON. Crie uma nova conta de serviço, nomeie-a dial-ynab e atribua a ela a Project->Owner função. Você vai querer criar uma função IAM específica para implantar em produção, mas, por enquanto, essa é a maneira mais fácil de começar. Baixe o arquivo de credenciais, renomeie-o para google-creds.json e coloque-o na pasta do seu projeto junto com index.js.

YNAB As credenciais da API do YNAB são um pouco mais fáceis de encontrar. Você pode gerar um token de acesso pessoal nas suas configurações da conta. Você também precisará do seu ID de orçamento, que pode ser encontrado acessando a interface da web e copiando o ID na URL (ele será semelhante a 58f1ca9a-abcd-123a-96ef-21aac7e2865c)

Neste momento, você já deve ter:

  • ID do aplicativo Nexmo

  • Credenciais de aplicativos do Google

  • ID do orçamento do YNAB e token de acesso

Vamos adicioná-los ao nosso .env arquivo para que possamos usá-los em nosso aplicativo:

YNAB_ACCESS_TOKEN="YOUR_YNAB_ACCESS_TOKEN" YNAB_BUDGET_ID="YOUR_YNAB_BUDGET_ID" NEXMO_APPLICATION_ID="YOUR_NEXMO_APPLICATION_ID" NEXMO_PRIVATE_KEY=./private.key GOOGLE_APPLICATION_CREDENTIALS=./google-creds.json

NEXMO_PRIVATE_KEY e GOOGLE_APPLICATION_CREDENTIALS são caminhos para arquivos que existem na pasta do nosso projeto, juntamente com index.js que contêm nossas credenciais.

Conecte-se aos nossos WebSockets

Agora que já sabemos como lidar com uma chamada recebida e temos nossas credenciais do Google, é hora de enviar o áudio da chamada para o serviço de transcrição do Google. Isso é feito por meio de dois websockets: um da Nexmo para o nosso aplicativo e outro do nosso aplicativo para o Google.

Vamos começar alterando nosso /webhooks/answer ponto de extremidade para usar uma connect ação. Isso instrui o Nexmo a se conectar ao /transcription ponto de extremidade em nosso aplicativo usando um WebSocket. Também instruímos o sistema a passar o UUID da chamada para o WebSocket usando a headers opção, já que precisaremos disso um pouco mais adiante.

Substitua seu /webhooks/answer pelo seguinte:

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
                    }
                }
                ]
            }
    ]);
});

Além de instruir a Nexmo a se conectar a /transcription, precisamos criar um endpoint que aguarde uma conexão WebSocket. É aqui que entra o express-ws pacote entra em cena. Ele adiciona um app.ws() método como um wrapper em torno de um servidor WebSocket. Adicione o seguinte abaixo do seu app.get() método:

app.ws('/transcription', function(ws, req) {
    let UUID;

    ws.on('message', function(msg) {
    });

    ws.on('close', function(){
    });
});

A primeira mensagem recebida da Nexmo será uma mensagem JSON contendo qualquer headers o que solicitamos no NCCO (neste caso, o UUID da chamada) e todas as mensagens subsequentes serão buffers de dados de áudio. Podemos usar esse conhecimento para implementar ws.on('message'); se a mensagem for um buffer, nós a encaminhamos para o Google; caso contrário, armazenamos o UUID para uso posterior.

let UUID;

ws.on('message', function(msg) {
    if (!Buffer.isBuffer(msg)) {
        let data = JSON.parse(msg);
        UUID = data.user;
        return;
    }
});

Como lidar com um histórico escolar do Google

Antes de enviarmos os dados de áudio ao Google, precisamos configurar uma instância do cliente de reconhecimento de fala na nuvem deles. Adicione o seguinte no início do arquivo, logo após 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
};

Isso cria uma nova instância do cliente de reconhecimento de fala na nuvem para usarmos. As opções de configuração fornecidas funcionam bem com o Nexmo, mas talvez você queira alterar languageCode se estiver falando em qualquer outro idioma que não seja en-GB. Você pode encontrar uma lista completa dos idiomas suportados na documentação do Google Cloud Speech.

Para usar a funcionalidade de conversão de fala em texto no SpeechClient, utilizamos o speech.streamingRecognize() método. Atualize app.ws('/transcription') e crie uma nova instância de speech.streamingRecognize sempre que uma nova conexão WebSocket for recebida:

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) {

Você deve ter notado que, no .on('data') método registramos os resultados de data.results[0].alternatives[0].transcript. Esse é o texto transcrito retornado pelo Google. Sabemos que o primeiro item retornado é sempre a tradução final, pois definimos interimResults: false em nossa configuração.

Como criamos uma nova speech.streamingRecognize() instância, também precisamos liberá-la quando nossa chamada for encerrada. Fazemos isso destruindo nossa speechStream instância no ws.on('close') método:

ws.on('close', function(){
    speechStream.destroy();
});

A última coisa a fazer é atualizar ws.on('message') para encaminhar os dados para speechStream se for um buffer.

ws.on('message', function(msg) {
    if (!Buffer.isBuffer(msg)) {
        let data = JSON.parse(msg);
        UUID = data.user;
        return;
    }

    speechStream.write(msg);
});

Se você executar seu aplicativo (node index.js) e ligar para o seu número da Nexmo, você deverá conseguir falar durante a ligação e ver o texto transcrito no console em tempo real.

Conecte-se ao YNAB

Agora que a transcrição já está funcionando, o próximo passo é buscar nossos dados de orçamento do YNAB. No início do seu arquivo (depois de criar seu googleConfig objeto), adicione o seguinte para criar um ynab cliente de API:

const ynabClient = require("ynab");
const ynab = new ynabClient.API(process.env.YNAB_ACCESS_TOKEN);

Podemos nos conectar à API do YNAB usando esse cliente e listar todos os nossos grupos de categorias e categorias. Como não estamos interessados nos grupos principais, mas apenas nas próprias categorias, podemos criar uma lista com os nomes das categorias e seus saldos usando a função a seguir. Adicione isso ao final do seu arquivo:

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}; })
    ), []);
}

Isso busca todas as categorias do YNAB e retorna uma lista no seguinte formato:

[
  { name: 'Dining Out', balance: 38.11 },
  { name: 'Gaming', balance: 12.74 },
  { name: 'Music', balance: 43.85 },
  { name: 'Fun Money', balance: -13.44 }
]

Vamos usar esse fetchYnabBalanceData() método em nossa .on('data') função quando recebermos uma transcrição para associar o que foi dito a um nome de categoria. Infelizmente, é muito improvável que o que o Google retornar corresponda exatamente ao seu nome de categoria. Precisamos ser um pouco criativos para descobrir qual categoria o interlocutor pretendia. Para isso, podemos usar o fast-levenshtein pacote que instalamos anteriormente.

Para descobrir qual categoria o ouvinte desejava, podemos pegar a entrada (needle) e pesquisar todos os nomes de categorias (haystack), usando fast-levenshtein para calcular o menor número de alterações de letras necessárias para que um nome de categoria corresponda à nossa entrada. Essa é uma aproximação rudimentar, mas funciona bem o suficiente para nossas necessidades. Adicione o seguinte ao final do seu arquivo abaixo 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;
}

Você também precisará incluir o fast-levenshtein pacote no início do seu arquivo. Adicione-o logo após require('dotenv').config():

const levenshtein = require('fast-levenshtein');

Agora temos tudo o que precisamos para atualizar nossa .on('data') função para registrar uma categoria e um saldo no console:

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 é um bom momento para executar seu aplicativo novamente (node index.js) e ligar para o seu número da Nexmo para testar seu código. Tente dizer “Eating Out” e veja como ele retorna a categoria “Dining Out”.

Retorne a ligação

Só falta uma última coisa para concluir nosso dial-ynab projeto: fazer com que ele leia o saldo da categoria de volta para a chamada usando o recurso de conversão de texto em fala.

Para isso, precisaremos usar o nexmo pacote. Você não precisa de um apiKey nem apiSecret para usar a Voice API, portanto, fique à vontade para ignorar esses valores. Para acessar a Voice API, precisamos fornecer um applicationId e privateKey que, por acaso, acabamos de adicionar ao nosso .env arquivo anteriormente.

Adicione o código a seguir logo abaixo require('fast-levenshtein') no início do seu arquivo:

const Nexmo = require('nexmo');
const nexmo = new Nexmo({
    apiKey: 'unused',
    apiSecret: 'unused',
    applicationId: process.env.NEXMO_APPLICATION_ID,
    privateKey: process.env.NEXMO_PRIVATE_KEY,
});

Em seguida, atualize seu .on('data') método para chamar a API da Nexmo, adicionando o código a seguir abaixo 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); }
});

Se você ligar para o seu número da Nexmo novamente agora, ouvirá o saldo da categoria sendo lido em voz alta. No entanto, o saldo da categoria não soa muito bem, pois está sendo lido como um número decimal. Podemos indicar ao mecanismo de conversão de texto em fala que se trata de um valor monetário usando SSML. Atualize sua balanceText definição da seguinte forma:

const balanceText = `<speak>${category.name} has <say-as interpret-as="vxml:currency">GBP${category.balance}</say-as> available</speak>`;

Ligue para o seu número da Nexmo uma última vez e você ouvirá que o número foi interpretado como moeda, graças a interpret-as="vxml:currency".

Conclusão

Em pouco menos de 125 linhas de código, criamos um aplicativo que permite que você consulte seu orçamento no YNAB e verifique se ainda há saldo suficiente na categoria “refeições fora de casa” antes de sair, quando bater aquela vontade de pedir sua comida favorita para viagem.

Integramos o Nexmo, o Google e o YNAB usando suas APIs e websockets para oferecer transcrição de chamadas em tempo real e feedback de áudio durante uma chamada de voz ativa. Não sei o que você acha, mas eu acho isso incrível!

Se você quiser saber mais sobre a Voice API da Nexmo, a visão geral da Voice API é um bom ponto de partida. Você pode se interessar especialmente pela referência NCCO ou no guia conceitual sobre WebSockets.

Para discutir este post, a Voice API da Nexmo ou a comunicação em geral, fique à vontade para participar da Slack da Comunidade Nexmo, onde o equipe @NexmoDev está pronta e à disposição para ajudar.

Crédito de bônus

Você ainda está lendo? Ótimo! Minha parte favorita de todo esse post é que a única parte específica do YNAB é o fetchYnabBalanceData método. Seria muito fácil fazer isso funcionar com o recurso “pots” do Monzo, em vez do YNAB. Na verdade, vamos fazer isso agora mesmo!

Primeiro, obtenha seu token de acesso do Monzo no Monzo Playground e adicione-o ao .env:

MONZO_ACCESS_TOKEN="YOUR_MONZO_ACCESS_TOKEN"

Vamos usar a request-promise biblioteca para acessar a API do Monzo, então vamos instalá-la agora

npm install request-promise --save

Adicione o seguinte ao final do seu arquivo para definir a fetchMonzoBalanceData função. A API do Monzo retorna dados que contêm name e balance chaves, portanto, tudo o que precisamos fazer é reformatar o saldo para que fique na forma de moeda 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 fim, altere a chamada para fetchYnabBalanceData para que ela chame fetchMonzoBalanceData em vez disso. Agora ligue para o seu número Nexmo e diga o nome de um dos seus “pots” do Monzo. Parabéns! Agora você está trabalhando com a API do Monzo em vez do YNAB com apenas 6 linhas de código adicionais.

Compartilhar:

https://a.storyblok.com/f/270183/384x384/1c8825919c/mheap.png
Michael HeapEx-funcionários da Vonage

Michael é um engenheiro de software poliglota, empenhado em reduzir a complexidade dos sistemas e torná-los mais previsíveis. Trabalhando com diversas linguagens e ferramentas, ele compartilha seus conhecimentos técnicos com públicos de todo o mundo em grupos de usuários e conferências. No dia a dia, Michael é ex-representante de desenvolvedores da Vonage, onde dedicava seu tempo a aprender, ensinar e escrever sobre todos os tipos de tecnologia.