https://d226lax1qjow5r.cloudfront.net/blog/blogposts/dial-ynab-dr/TW_DialYNAB.png

Suivez votre budget avec le cadran YNAB

Publié le April 29, 2021

Temps de lecture : 17 minutes

Entre le paiement de l'hypothèque, l'épargne d'un fonds d'urgence et l'achat de beaucoup trop de jeux de société, j'avais du mal à savoir où allait tout mon argent chaque mois. Heureusement, j'ai découvert Vous avez besoin d'un budget (YNAB) qui me permet de répartir mon argent dans différentes catégories chaque mois et de suivre le montant de chaque catégorie.

Leur application mobile et leur site web sont plutôt bons, mais quand j'ai vu que YNAB avait récemment lancé une API, cela m'a fait réfléchir à d'autres façons d'accéder aux données de mon budget. L'inspiration n'a pas tardé à venir.

Depuis des années, vous pouvez appeler votre banque pour vérifier le solde de votre Account, mais cela ne me sert à rien. Le solde total ne reflète pas l'argent qui a déjà été affecté à un achat futur. Je voulais plutôt appeler un numéro pour savoir combien il me restait dans ma catégorie jeux de société, etc, dial-ynab est né.

Vue d'ensemble

Dans ce billet, nous allons construire une application node.js qui utilise la plateforme Nexmo pour faire ce qui suit :

  1. Recevoir un appel vocal.

  2. Introduire les données audio dans l'API de conversion de la parole en texte de Google

  3. Interroger l'API YNAB pour connaître le solde actuel de la catégorie demandée.

  4. Utilisez la fonctionnalité de synthèse vocale de Nexmo pour dire le solde lors de l'appel.

Dial YNAB Sequence DiagramDial YNAB Sequence Diagram

Pour ce faire, nous devrons suivre les étapes suivantes :

  1. Bootstrap un projet Node.js avec express et express-ws

  2. Configurer une application Nexmo

  3. Obtenir les identifiants d'authentification pour Google Cloud et YNAB

  4. Traiter un appel entrant à l'aide de Nexmo

  5. Connecter l'appel à notre application à l'aide d'un websocket

  6. Transmettre les données audio de Nexmo à Google pour la transcription

  7. Traiter les données transcrites renvoyées par Google

  8. Récupérer les soldes de nos comptes courants dans YNAB

  9. Répétez le solde lors de l'appel en utilisant la fonctionnalité Text-To-Speech de Nexmo.

Il y a beaucoup de choses à dire, alors commençons !

Conditions préalables

Pour réaliser ce tutoriel, vous aurez besoin des éléments suivants :

  • node.js (j'utilise la version 10.0.0) et npm installé

  • ngrok pour exposer votre application locale à l'internet afin que Nexmo puisse l'atteindre

  • nexmo-cli disponible (ceci est optionnel, car vous pouvez effectuer les mêmes tâches via le tableau de bord Nexmo)

  • Identifiants Google et YNAB (nous y reviendrons plus tard)

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.

Une fois que vous avez tout ce qu'il vous faut, démarrez un ngrok en lançant ngrok http 3000 et notez l'URL (dans mon cas, il s'agit de http://e7dddad9.ngrok.io). Chaque fois que vous verrez une ngrok dans ce billet, remplacez-la par la vôtre.

dial ynab ngrokdial ynab ngrok

Démarrage d'un projet

Commençons par créer un dossier nommé dial-ynab et en y changeant de répertoire. Pour démarrer notre projet, nous devons lancer npm init et installer quelques dépendances :

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

Nous n'avons pas besoin de toutes ces dépendances pour commencer, mais il est plus facile de les installer dès le départ pour ne pas avoir à s'en préoccuper plus tard.

Création d'une application Nexmo

Avant de pouvoir traiter un appel entrant, nous devons créer une application Nexmo et y associer un numéro. Nous utiliserons l'outil Nexmo CLI pour ce faire, mais vous pouvez également créer une application et y associer un numéro dans le tableau de bord si vous préférez.

# 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

Une fois que vous avez fait cela, chaque fois qu'un appel est passé au numéro que vous avez acheté, Nexmo fera une GET demande à http://e7dddad9.ngrok.io/webhook/answer pour savoir comment traiter l'appel. Implémentons maintenant ce point final en utilisant Express.

Traiter un appel entrant

Il y a beaucoup de code nécessaire pour démarrer notre instance Express. Créez un fichier nommé index.js avec le contenu suivant, qui enregistrera dotenv pour les valeurs de configuration et créer une instance express sans routes définies :

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}!`);
});

Nous avons également référencé une variable nommée process.env.PORT mais nous ne l'avons pas encore définie. Pour ce faire, créez un fichier nommé .env avec le contenu suivant pour le faire :

PORT=3000

La dernière partie du puzzle consiste à définir notre /webhooks/answer URL. Pour ce faire, nous définissons une méthode app.get() juste avant d'appeler app.listen(). Lorsque Nexmo adresse une requête à notre application, il s'attend à ce que nous retournions une méthode NCCO. Dans ce cas, nous renvoyons une action talk qui prononcera une réponse à l'appelant en utilisant la synthèse vocale :

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

C'est tout ce dont vous avez besoin pour gérer un appel entrant avec Nexmo. Essayez-le en exécutant node index.jspuis appelez le numéro que vous avez acheté précédemment. Vous devriez entendre This is a text to speech demo from Nexmo. Thanks for calling avant que l'appel ne soit terminé.

Félicitations ! Vous avez fait le plus dur - le reste de cet article consiste simplement à câbler quelques services externes différents.

Configuration des services

Avant de poursuivre le reste de l'article, nous avons besoin d'informations d'authentification pour Google Cloud Speech et You Need A Budget.

Pour Google Cloud Speech vous devez créer une clé de compte de service et télécharger les informations d'identification au format JSON. Créez un nouveau compte de service, appelez-le dial-ynab et lui attribuer le rôle Project->Owner rôle. Vous voudrez créer un rôle IAM spécifique pour le déploiement en production, mais pour l'instant, c'est la façon la plus simple de commencer. Téléchargez le fichier credentials, renommez-le en google-creds.json et placez-le dans le dossier de votre projet à côté de index.js.

YNAB sont un peu plus faciles à trouver. Vous pouvez générer un jeton d'accès personnel dans vos paramètres de votre Account. Vous aurez également besoin de votre identifiant de budget, que vous pouvez trouver en visitant l'interface web et en copiant l'ID dans l'URL (il ressemblera à 58f1ca9a-abcd-123a-96ef-21aac7e2865c)

À ce stade, vous devriez avoir :

  • ID de l'application Nexmo

  • Identifiants de l'application Google

  • ID de budget YNAB et jeton d'accès

Ajoutons-les à notre fichier .env afin de pouvoir les utiliser dans notre application :

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 et GOOGLE_APPLICATION_CREDENTIALS sont des chemins vers des fichiers qui existent dans notre dossier de projet à côté de index.js qui contiennent nos informations d'identification.

Se connecter à nos WebSockets

Maintenant que nous pouvons gérer un appel entrant et que nous disposons de nos identifiants Google, il est temps de transmettre l'audio de l'appel téléphonique au service de transcription de Google. Pour ce faire, nous utilisons deux websockets : l'une de Nexmo vers notre application et l'autre de notre application vers Google.

Commençons par modifier notre /webhooks/answer pour utiliser une connect action. Cela indique à Nexmo de se connecter à l'extrémité /transcription dans notre application en utilisant un websocket. Nous lui demandons également de passer l'UUID de l'appel à la websocket en utilisant l'option headers car nous en aurons besoin un peu plus tard.

Remplacez votre point de terminaison /webhooks/answer par ce qui suit :

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

En plus d'indiquer à Nexmo de se connecter à /transcriptionnous devons créer un point de terminaison qui écoute une connexion websocket. C'est là que le paquet express-ws entre en jeu. Il ajoute une méthode app.ws() comme une enveloppe autour d'un serveur websocket. Ajoutez ce qui suit sous votre méthode app.get() méthode :

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

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

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

Le premier message reçu de Nexmo sera un message JSON contenant tout ce que nous avons demandé dans le NCCO (dans ce cas, l'UUID de l'appel). headers que nous avons demandé dans le NCCO (dans ce cas, l'UUID de l'appel) et tous les messages suivants seront des tampons de données audio. Nous pouvons utiliser cette connaissance pour mettre en œuvre ws.on('message')si le message est un tampon, nous le transmettons à Google, sinon nous stockons l'UUID pour plus tard.

let UUID;

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

Traitement d'une transcription à partir de Google

Avant d'envoyer les données audio à Google, nous devons configurer une instance de leur client cloud speech. Ajoutez ce qui suit au début du fichier, juste aprè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
};

Cette opération crée une nouvelle instance du client cloud speech que nous utiliserons. Les options de configuration fournies fonctionnent bien avec Nexmo, mais vous pouvez modifier les options suivantes languageCode si vous parlez autre chose que en-GB. Vous trouverez une liste complète des langues prises en charge dans la documentation de Google Cloud Speech docs.

Pour utiliser la fonctionnalité de synthèse vocale dans le SpeechClientnous utilisons la méthode speech.streamingRecognize() méthode. Mettre à jour app.ws('/transcription') et créons une nouvelle instance de speech.streamingRecognize chaque fois qu'une nouvelle connexion websocket est reçue :

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

Vous remarquerez que dans la méthode .on('data') nous enregistrons les résultats de data.results[0].alternatives[0].transcript. Il s'agit du texte transcrit renvoyé par Google. Nous savons que le premier élément renvoyé est toujours la traduction finale, comme nous l'avons défini dans notre configuration. interimResults: false dans notre configuration.

Comme nous avons créé une nouvelle instance speech.streamingRecognize() nous devons également nettoyer l'instance lorsque notre appel se déconnecte. Nous le faisons en détruisant notre instance speechStream dans la méthode ws.on('close') méthode :

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

La dernière chose à faire est de mettre à jour ws.on('message') pour transmettre les données à speechStream s'il s'agit d'un tampon.

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

    speechStream.write(msg);
});

Si vous lancez votre application (node index.js) et que vous appelez votre numéro Numbers, vous devriez pouvoir parler au cours de l'appel et voir le texte transcrit en temps réel dans la console.

Se connecter à YNAB

Maintenant que la transcription fonctionne, la prochaine chose à faire est de récupérer les données de notre budget YNAB. Au début de votre fichier (après avoir créé votre objet googleConfig ), ajoutez ce qui suit pour créer un ynab client API :

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

Nous pouvons nous connecter à l'API YNAB à l'aide de ce client et dresser la liste de tous nos groupes de catégories et de toutes nos catégories. Comme nous ne sommes pas intéressés par les groupes principaux, mais seulement par les catégories elles-mêmes, nous pouvons construire une liste de noms de catégories et de soldes à l'aide de la fonction suivante. Ajoutez cette fonction au bas de votre fichier :

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

Cette opération permet de récupérer toutes les catégories de YNAB et de renvoyer une liste dans le format suivant :

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

Nous utiliserons cette fetchYnabBalanceData() dans notre fonction .on('data') lorsque nous recevrons une transcription pour faire correspondre ce qui a été dit à un nom de catégorie. Malheureusement, il est très peu probable que ce que Google renvoie corresponde exactement au nom de votre catégorie. Nous devons faire preuve d'un peu de créativité pour déterminer la catégorie souhaitée par l'appelant. Pour ce faire, nous pouvons utiliser le paquet fast-levenshtein que nous avons installé plus tôt.

Pour déterminer la catégorie souhaitée par notre interlocuteur, nous pouvons prendre l'entrée (needle) et rechercher chaque nom de catégorie (haystack), en utilisant fast-levenshtein pour calculer le plus petit nombre de changements de lettres nécessaires pour qu'un nom de catégorie corresponde à notre entrée. Il s'agit d'une approximation grossière, mais qui fonctionne suffisamment bien pour nos besoins. Ajoutez ce qui suit au bas de votre fichier ci-dessous 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;
}

Vous devrez également exiger le paquetage fast-levenshtein au début de votre fichier. Ajoutez-le juste après require('dotenv').config():

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

Nous avons maintenant tout ce qu'il faut pour mettre à jour notre fonction .on('data') pour enregistrer une catégorie et un solde dans la 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);
    });

C'est le bon moment pour relancer votre application (node index.js) et appelez votre numéro Numbers pour tester votre code. Essayez de dire "Manger au restaurant" et observez le retour de votre catégorie "Manger au restaurant".

Reprendre l'appel

Il ne reste plus qu'une dernière chose à faire pour terminer notre projet : faire lire le solde de la catégorie lors de l'appel à l'aide de la synthèse vocale. dial-ynab : faire en sorte qu'il lise le solde de la catégorie dans l'appel à l'aide de la synthèse vocale.

Pour ce faire, nous devons utiliser le paquet nexmo . Vous n'avez pas besoin d'un apiKey ou apiSecret pour utiliser l'API Voice, alors n'hésitez pas à ignorer ces valeurs. Pour accéder à l'API Voice, nous devons fournir un élément applicationId et privateKey que nous avons ajouté par hasard à notre fichier .env plus tôt.

Ajoutez le code suivant juste en dessous de require('fast-levenshtein') au début de votre fichier :

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

Ensuite, mettez à jour votre méthode .on('data') pour appeler l'API Nexmo en ajoutant le code suivant 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 vous rappelez votre numéro Numbers, vous entendrez le solde de la catégorie vous être lu. Cependant, le solde de la catégorie ne semble pas tout à fait correct car il est lu sous la forme d'un nombre décimal. Nous pouvons indiquer au moteur de synthèse vocale qu'il s'agit d'une valeur monétaire en utilisant le langage SSML. Mettez à jour votre balanceText avec la définition suivante :

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

Appelez une dernière fois votre numéro Numbers et vous entendrez que le numéro a été interprété comme de la monnaie grâce à interpret-as="vxml:currency".

Conclusion

En un peu moins de 125 lignes de code, nous avons créé une application qui vous permet d'appeler votre budget YNAB et de vous assurer qu'il vous reste suffisamment d'argent dans votre catégorie restaurant avant de sortir après avoir eu une envie folle de votre plat à emporter préféré.

Nous avons connecté Nexmo, Google et YNAB en utilisant leurs API et websockets pour fournir une transcription d'appel en temps réel et un retour audio sur un appel vocal actif. Je ne sais pas ce qu'il en est pour vous, mais je pense que c'est assez génial !

Si vous souhaitez en savoir plus sur l'API Voice de Nexmo, la section Vue d'ensemble de l'API Voice est un bon point de départ. Vous pouvez être particulièrement intéressé par la référence NCCO ou le guide du concept de websockets.

Pour parler de ce billet, de l'API Voice de Nexmo, ou de la communication en général, n'hésitez pas à rejoindre le Communauté Nexmo Slackoù les membres de la communauté @NexmoDev est prête à vous aider.

Crédit bonus

Vous lisez toujours ? C'est excellent ! Ce que je préfère dans cet article, c'est que la seule partie spécifique à YNAB est la méthode. fetchYnabBalanceData méthode. Il serait trivial de faire fonctionner cela avec la fonction pots de Monzo au lieu de YNAB. En fait, faisons-le maintenant !

Tout d'abord, obtenez votre jeton d'accès Monzo à partir du Monzo Playground et ajoutez-le à .env:

MONZO_ACCESS_TOKEN="YOUR_MONZO_ACCESS_TOKEN"

Nous allons utiliser la bibliothèque request-promise pour accéder à l'API Monzo, alors installons-la maintenant

npm install request-promise --save

Ajoutez ce qui suit au bas de votre fichier pour définir la fonction fetchMonzoBalanceData pour définir la fonction. L'API Monzo renvoie des données qui contiennent name et balance donc tout ce que nous avons à faire est de reformater le solde pour qu'il soit en monnaie décimale :

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

Enfin, modifiez l'appel à fetchYnabBalanceData pour qu'il appelle fetchMonzoBalanceData à la place. Maintenant, appelez votre Numbers et dites le nom de l'un de vos pots Monzo. Félicitations à tous ! Vous travaillez maintenant avec l'API Monzo au lieu de YNAB avec seulement 6 lignes de code supplémentaires.

Partager:

https://a.storyblok.com/f/270183/384x384/1c8825919c/mheap.png
Michael HeapAnciens de Vonage

Michael est un ingénieur logiciel polyglotte qui s'attache à réduire la complexité des systèmes et à les rendre plus prévisibles. Travaillant avec une variété de langages et d'outils, il partage son expertise technique avec des publics du monde entier lors de groupes d'utilisateurs et de conférences. Au quotidien, Michael est un ancien défenseur des développeurs chez Vonage, où il a passé son temps à apprendre, enseigner et écrire sur toutes sortes de technologies.