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

ダイヤルYNABで予算を管理する

最終更新日 April 29, 2021

所要時間:7 分

住宅ローンの支払い、緊急資金の貯蓄、ボードゲームの買いすぎの間に、私は毎月のお金の使い道を追跡するのに苦労していた。ありがたいことに、私は You Need A Budget (YNAB)を数年前に発見した。このアプリを使えば、毎月お金をさまざまなカテゴリーに分類し、それぞれのカテゴリーにいくら入っているかを記録することができる。

YNABのモバイル・アプリケーションとウェブサイトはかなり良いが、最近APIを発表したのを見たとき、予算のデータにアクセスする他の方法について考えさせられた。インスピレーションが湧くのに時間はかからなかった。

口座の残高を確認するために銀行に電話することは何年も前からできるが、それは私にとって役に立たない。総残高には、将来の買い物のためにすでに割り当てられたお金は反映されないからだ。その代わりに、ある番号に電話して、ボードゲームのカテゴリーにいくら残っているかを知りたかったのだ、 ダイヤル・イナブが生まれた。

概要

この記事では、Nexmoプラットフォームを使って以下のことを行うnode.jsアプリケーションを構築します:

  1. Voiceコールを受ける。

  2. 音声データをGoogleのSpeech-to-Text APIに送り込む。

  3. YNAB API に問い合わせて、リクエストされたカテゴリの現在の残高を調べる。

  4. Nexmoの音声合成機能を利用して、通話中に残高を言い返す。

Dial YNAB Sequence DiagramDial YNAB Sequence Diagram

そのためには、以下のステップを踏む必要がある:

  1. でNode.jsプロジェクトをブートストラップする。 expressexpress-ws

  2. Nexmoアプリケーションを設定する

  3. GoogleクラウドとYNABの認証情報を取得する

  4. Nexmoを使ってインバウンドコールを処理する

  5. ウェブソケットを使ってアプリケーションに接続する。

  6. NexmoからGoogleに音声データを渡し、文字起こしを行う。

  7. Googleから返された転記データを処理する

  8. YNABから現在の口座残高を取得する

  9. Nexmoの音声合成機能を使って、通話中に残高を言い返す。

たくさんあるから、そろそろ始めよう!

前提条件

このチュートリアルに必要なものは以下の通り:

  • node.js(バージョン10.0.0)とnpmをインストールした。

  • ngrokローカルアプリケーションをインターネットに公開し、Nexmoからアクセスできるようにする。

  • nexmo-cliを利用することができます (Nexmoダッシュボードから同じタスクを実行できるので、これはオプションです)。

  • GoogleとYNABの認証情報(後ほど説明する)

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.

すべての準備が整ったら ngrokトンネルを開始する。 ngrok http 3000を実行してトンネルを開始し、URL(私の場合は http://e7dddad9.ngrok.io).この記事で ngrokこの記事のURLと自分のURLを入れ替えてください。

dial ynab ngrokdial ynab ngrok

プロジェクトのブートストラップ

まずは dial-ynabという名前のフォルダを作成し、そこにディレクトリを変更することから始めよう。プロジェクトを開始するには npm initを実行し、いくつかの依存関係をインストールする必要がある:

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

これらの依存関係は最初からすべて必要なわけではないが、前もってすべてインストールしておいた方が後で心配する必要がない。

Nexmoアプリケーションの作成

インバウンドコールを処理する前に、Nexmoアプリケーションを作成し、番号をリンクする必要があります。そのためにNexmo CLIツールを使いますが、次のこともできます。 アプリケーションを作成しを作成し、ダッシュボードで番号をリンクすることもできます。

# 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

これを行うと、購入した電話番号に電話がかかるたびにNexmoは GETにリクエストします。 http://e7dddad9.ngrok.io/webhook/answerにリクエストして、その電話をどのように処理するかを調べます。では、Expressを使ってそのエンドポイントを実装してみよう。

着信の処理

Expressインスタンスをブートストラップするために必要なコードがたくさんある。以下の内容で index.jsという名前のファイルを作成します。 dotenvという名前のファイルを作成します。 expressインスタンスを作成します:

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

という変数も参照しているが、まだ定義していない。 process.env.PORTという変数も参照しているが、まだ定義していない。定義するために .envという名前のファイルを作成する:

PORT=3000

パズルの最後の部分は /webhooks/answerURLを定義することです。これは app.get()メソッドを定義する。 app.listen().Nexmoがわれわれのアプリケーションにリクエストをするとき、彼らはわれわれが NCCO.この場合、私たちは talkアクションを返します:

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

Nexmoでインバウンドコールを処理するのに必要なものはこれだけです。試しに node index.jsを実行して試してみてください。通話が終わる前に This is a text to speech demo from Nexmo. Thanks for calling通話が終わる前に

おめでとう!この記事の残りの部分は、いくつかの異なる外部サービスを配線するだけです。

サービスの設定

この記事の続きを書く前に、Google Cloud SpeechとYou Need A Budgetの認証情報が必要だ。

対象 グーグル・クラウド・スピーチには サービス・アカウント・キーの作成を作成し、認証情報をJSONとしてダウンロードする必要があります。新しいサービスアカウントを作成し、それを dial-ynabを呼び出し Project->Ownerロールを与える。本番環境にデプロイするには、特定のIAMロールを作成する必要がありますが、今はこれが一番簡単な方法です。資格情報ファイルをダウンロードし、名前を google-creds.jsonにリネームし、プロジェクトフォルダ内の index.js.

YNABAPI認証情報を見つけるのはもう少し簡単です。個人用のアクセストークンは アカウント設定.また、バジェットIDも必要です。 ウェブインターフェースにアクセスし、URLのIDをコピーしてください。 58f1ca9a-abcd-123a-96ef-21aac7e2865c)

この時点であなたは持っているはずだ:

  • NexmoアプリケーションID

  • Googleアプリケーションの認証情報

  • YNAB予算IDとアクセストークン

アプリケーションで使えるように .envファイルに追加しよう:

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_KEYGOOGLE_APPLICATION_CREDENTIALSと並んでプロジェクト・フォルダーに存在するファイルへのパスです。 index.jsと並んでプロジェクトフォルダに存在するファイルへのパスです。

ウェブソケットに接続する

インバウンドコールを処理し、Googleの認証情報を手に入れたので、次は電話の音声をGoogleのテープ起こしサービスに送る番だ。1つはNexmoからアプリケーションへ、もう1つはアプリケーションからGoogleへ。

まずは /webhooks/answerエンドポイントを connectアクションを使うように変更します。これはNexmoに /transcriptionエンドポイントに接続します。また、コールUUIDをwebsocketに渡すために headersオプションを使用して呼び出しUUIDをウェブソケットに渡すように指示します。

既存の /webhooks/answerエンドポイントを以下のように置き換える:

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

に接続するようにNexmoに指示するだけでなく、ウェブソケット接続をリッスンするエンドポイントを作成する必要がある。 /transcriptionに接続するようにNexmoに指示するだけでなく、ウェブソケット接続をリッスンするエンドポイントを作成する必要がある。ここで express-wsパッケージの出番だ。これは app.ws()メソッドを追加します。あなたの app.get()メソッドを追加します:

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

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

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

Nexmoから受信する最初のメッセージは、NCCOで要求したもの(この場合は通話UUID)を含むJSONメッセージになります。 headersを含むJSONメッセージになり、それ以降のメッセージはすべてオーディオデータのバッファになります。この知識を使って ws.on('message')メッセージがバッファであればGoogleに転送し、そうでなければUUIDを保存する。

let UUID;

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

Googleからのトランスクリプトの処理

音声データをGoogleに送信する前に、Googleのクラウド・スピーチ・クライアントのインスタンスを設定する必要がある。ファイル冒頭の 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
};

これでクラウドスピーチクライアントの新しいインスタンスが作成されます。この設定オプションはNexmoでうまく機能しますが、Nexmo以外の言語を使用する場合は languageCode以外のものを話す場合は en-GB.サポートされている言語の完全なリストは Googleクラウドスピーチのドキュメント.

の音声読み上げ機能を使うには、次のようにします。 SpeechClientで音声読み上げ機能を使うには speech.streamingRecognize()メソッドを使用します。更新 app.ws('/transcription')の新しいインスタンスを作成します。 speech.streamingRecognizeの新しいインスタンスを作成します:

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

メソッドで .on('data')メソッドで data.results[0].alternatives[0].transcript.これはGoogleから返されたテキストを書き起こしたものです。設定で interimResults: falseを設定したからです。

新しい speech.streamingRecognize()インスタンスを作成したので、呼び出しが切断されたときにインスタンスをクリーンアップする必要があります。そのためには speechStreamインスタンスを ws.on('close')メソッドでインスタンスを破棄します:

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

最後にすることは ws.on('message')にデータを転送することである。 speechStreamに転送する。

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

    speechStream.write(msg);
});

アプリケーション(node index.js)を実行し、Nexmo番号に電話をかけると、通話中に話すことができ、リアルタイムでコンソールに書き起こされたテキストを見ることができるはずです。

YNABに接続する

さて、トランスクリプションが動作するようになったので、次にすることはYNABの予算データを取得することです。ファイルの先頭で(オブジェクトを作成した後で googleConfigオブジェクトを作成した後)ファイルの先頭に以下を追加して ynabAPIクライアントを作成します:

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

このクライアントを使用して YNAB API に接続し、すべてのカテゴリグループとカテゴリを一覧表示することができます。マスターグループには興味がなく、カテゴリそのものに興味があるので、以下の関数を使ってカテゴリ名と残高のリストを作成します。これをファイルの一番下に追加します:

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

これはYNABからすべてのカテゴリーを取得し、以下のフォーマットでリストを返す:

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

この fetchYnabBalanceData()メソッドを使います。 .on('data')関数でこのメソッドを使います。残念ながら、Googleが返すものがあなたのカテゴリー名と完全に一致する可能性は極めて低い。発信者がどのカテゴリーを希望したのかを調べるには、少し工夫が必要です。そのためには、先ほどインストールした fast-levenshteinパッケージを使うことができる。

発信者がどのカテゴリーを希望したかを調べるには、入力(needle)を受け取り、すべてのカテゴリー名(haystack)を検索します。 fast-levenshteinを使って、カテゴリー名が入力に一致するために必要な、文字の変化の最小数を計算します。これは粗雑な近似値ですが、私たちのニーズには十分機能します。以下のファイルの一番下に以下を追加する。 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;
}

また、ファイルの先頭に fast-levenshteinパッケージも必要だ。ファイルの先頭に require('dotenv').config():

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

これで .on('data')関数を更新する必要があります:

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

このタイミングでもう一度アプリケーションを実行し (node index.js)を実行し、Nexmo番号に電話をかけてコードをテストしてください。外食」と言うと、「外食」カテゴリーが返されます。

通話に戻る

プロジェクトの仕上げとして、最後にやることが1つだけ残っている。 dial-ynabそれは、Text-To-Speech(音声合成)を使って、カテゴリー・バランスを通話に読み戻すことです。

そのためには nexmoパッケージを使う必要がある。パッケージの apiKeyまたは apiSecretは必要ないので、これらの値は無視して構わない。Voice APIにアクセスするには applicationIdprivateKeyを指定する必要があります。 .envファイルに追加した。

次のコードをファイルの先頭に追加します。 require('fast-levenshtein')を追加する:

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

次に .on('data')メソッドを更新し、Nexmo APIを呼び出すようにします。 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); }
});

今もう一度ネクスモの番号に電話すると、カテゴリー残高が読み上げられます。しかし、カテゴリー残高は10進数で読み上げられるため、正しく聞こえません。音声合成エンジンに、これが通貨の値であることを知らせるには SSML.以下のように balanceTextの定義を次のように更新してください:

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

Nexmo番号に最後にもう一度電話をかけると、番号が通貨として解釈されたことを聞くことができる。 interpret-as="vxml:currency".

結論

たった125行のコードで、YNABの予算を呼び出し、お気に入りのテイクアウトフードが食べたくなって出かける前に、外食カテゴリーに十分な予算が残っているか確認できるアプリケーションを作った。

私たちは、Nexmo、Google、YNABのAPIとウェブソケットを使って、アクティブな音声通話でリアルタイムの通話記録と音声フィードバックを提供するように配線しました。あなたのことは知らないが、これはかなりすごいことだと思う!

NexmoのVoice APIについてもっと知りたい方は Voice APIの概要をご覧ください。特に NCCOリファレンスウェブソケットコンセプトガイド.

この投稿、Nexmo Voice API、またはコミュニケーション全般について話すには、お気軽に以下のコミュニティに参加してください。 NexmoコミュニティSlackにご参加ください。 NexmoDevチームがお待ちしています。

ボーナス・クレジット

まだ読んでるの?素晴らしい!この記事の中で私が一番気に入っているのは、YNAB特有の部分が fetchYnabBalanceDataメソッドだけです。YNABではなく、Monzoのポット機能でこれを実現するのは簡単です。実際、今すぐやってみましょう!

まず、以下のページからMonzoアクセストークンを取得してください。 Monzoプレイグラウンドに追加します。 .env:

MONZO_ACCESS_TOKEN="YOUR_MONZO_ACCESS_TOKEN"

MonzoのAPIにアクセスするために request-promiseライブラリを使用してMonzo APIにアクセスします。

npm install request-promise --save

を定義するために、ファイルの一番下に以下を追加する。 fetchMonzoBalanceData関数を定義します。Monzo APIは namebalanceキーを含むデータを返すので、残高を10進数の通貨に再フォーマットするだけです:

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

最後に fetchYnabBalanceDataを呼び出すように変更する。 fetchMonzoBalanceDataに変更します。Nexmoの番号に電話をかけ、Monzoのポットの名前を言ってください。おめでとうございます!たった6行のコードを追加するだけで、YNABの代わりにMonzo APIを使えるようになりました。

シェア:

https://a.storyblok.com/f/270183/384x384/1c8825919c/mheap.png
Michael Heapヴォネージの卒業生

マイケルはポリグロット・ソフトウェア・エンジニアであり、システムの複雑性を軽減し、より予測可能なものにすることに尽力している。さまざまな言語やツールを使いこなし、ユーザーグループやカンファレンスで世界中の聴衆と技術的な専門知識を共有している。日々、マイケルはVonageの元開発者支持者であり、あらゆるテクノロジーについて学び、教え、書くことに時間を費やしている。