チカラの技術

電子工作やプログラミング

ラップの韻を自動で踏んでくれるIoTシステムを作りました

こんにちは、元気です!

私は日本語ラップが好きで毎日聞いています。
f:id:powerOfTech:20180820232444p:plain:w200
ラップを聴いてて一番楽しいと思うときはうまい韻(いん)を聞いたときですね。
良い韻は何十年も頭に残るものです。
「Kick the verse!歌詞蹴っ飛ばす! まるでストレス飛ばすジェットバス!」
ね?

単純に韻を聞いたり考えたりするのが楽しいので、自分の工学分野の用語で
韻を考えたりもするのですが、声で韻を教えてくれるシステムがあったら面白いと思って、
今回はGoogle Homeに頼むと自動でwebサイトから韻を探して踏んでくれるIoTシステムを作りました。

そもそも韻(いん)ってなに?

韻(いん,ライム)とは簡単に言うと「同じ母音で別の言葉を繰り返すこと」です。
要するに下の表の横列の言葉を使います。
f:id:powerOfTech:20180820221841p:plain:w400

例として私が凄いと思った韻を紹介します。
「一網打尽 REMIX (SHING02, MEISO, CANDLE, SPIN MASTER A-1) - 韻踏合組合」MEISOさんパート から
『I Got The 天然クイックルワイパー MEISOはカミソリSick二枚刃(シックにまいば) 伝授しに来た首振る快感
韻もフロー(リズムの取り方、音程)もヤバい!

システムの紹介

私が温まってきたところで作ったIoTシステムの紹介動画です。
www.youtube.com

設計現場で使える韻のダイジェスト付きです。 打ち合わせなどで積極的に使っていきましょう。
「ここで使うなフォトカプラ、遅延が酷くて情報格差、世代交代iCoupler、これが正しい開発だ。Yeah...」

これは便利ですね。なかなか実用性のあるシステムになった感があります。
文字で検索する場合と比べたメリットは、音声入力なので文字入力不要ということ、
声で入力し結果も声で聞くことで音のイメージを正確に把握できることですね。
デメリットは「勢至菩薩」などの複雑な単語は音を聞いただけでは理解できないことですが、
一応、raspberry PiSSHで接続すれば以下のように結果をモニターする事も出来ます。
(声の回答より先に結果がでます)
f:id:powerOfTech:20180820231809p:plain:w300

これから毎朝起き掛けに使って韻のレパートリーを増やしていきたいですね。

技術解説

動画内にもあった概要図です。

f:id:powerOfTech:20180821000050p:plain

 ① Google Homeが声を認識「電子工作」へ文字変換、IFTTTに送信。
 ② IFTTTは文字をWeb hooksでAzureへPOST転送
 ③ Azureはwebサーバーとして、クライアント(RaspberryPi)に文字情報「電子工作」を転送
 ④ Raspberry Piは文字情報を韻ノートに送信して結果を文字情報で受け取る。
 ⑤ 受け取った入力と韻をGoogle Homeに送信、喋ってもらう。

本システムではwebサービス韻ノート」で韻を検索しています。
韻ノートでは形態素解析やwebワードの自動分析により、高い精度と最新の語彙で韻を踏んでくれるとのことです。

文字と音との相互変換という点でも韻ノートとGoogle Homeはかなり高度な解析をしています。

①では音声の「でんしこうさく」を「電子工作」に変換、④の韻ノート内部では「電子工作」を「でんしこうさく」に戻して音を解析、韻を検索してます。韻ノートから漢字の結果を受け取り、⑤でGoogle Homeは「てんしとだんす」という音にまた変換して発声しています。
技術の進歩は凄いですね・・・

ちなみに韻ノートはひらがなの結果も返してくれるのですが、漢字の方がアクセントなどが正確になると考えてGoogle Homeには漢字で送っています。(勢至菩薩近藤春菜がちゃんと読めたことは驚きました)

それからAzureのクラウドwebサーバーを間に置いているのは、
私の住処は集合住宅でセキュリティやポート設定の問題があるため、 RaspberryPiをwebサーバーとして公開したくなかったからです。

ソースコードと解説

IFTTT

IF
f:id:powerOfTech:20180821002218p:plain:w300

THEN
Tokenを持つPOSTだけ後段で受け付けられます。
なおこのTokenは現在無効です。
f:id:powerOfTech:20180821002325p:plain:w300

Azureのwebサーバー

サーバーサイドの全コードです。説明はコメントをご覧ください。
前回の「アイアイ機能」の認証も入っています。

// このプログラムはIFTTTから来たデータをSocketIOを通じて機能ごとに割り当てられたクライアントに送信する。
// IFTTTはトークンをデータと共に送付して認証する。
// クライアントはまずPOSTメソッドで認証用トークンを送り、サーバーが認証し機能用トークン(JWT)を返す。
// クライアントはこの機能用トークンを用いてsocketIOを接続する。
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var bodyParser = require('body-parser');
var jwt = require("jsonwebtoken");
// 認証用トークン。IFTTT及びクライアントから同じトークンが送られて来た場合のみ本プログラムは動作する
const authToken = "D462253D54FB3C4BE77AE1992341A279";
const jwtKey = authToken;
const funcs = { aiai: "aiai", getRyme: "getRyme" };
// setting body pearser
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(bodyParser.json());
//AzureにアップロードするときはPORTを環境変数で決めるため、必ず下記が必要になる。
var port = process.env.PORT || 8080;
//IFTTTからのデータ受信
app.post(`/ifttt`, (req, res) => {
console.log('IFTTT posted!' + req);
//check token (authenticated User)
if (req.body.token == authToken) {
const func = req.body.func;
//アイアイ機能
if (func == funcs.aiai) {
req.body.name = req.body.name.trim();
var check = req.body.name;
var sendData;
// verify word from IFTTT(google home)
if (check == "II" || check == "ii" || check == "I愛" || check == "I 愛" || check == "i愛") {
sendData = "アイアイ";
} else {
sendData = req.body.name;
}
}
//ライムを得る機能
else if (func == funcs.getRyme) {
sendData = req.body.origin.replace(/\s+/g, "");;
}
// send to raspberry pi to specific socket.IO rooom
io.to(func).emit('ifttt', sendData);
}
});
//クライアントからのfunctionトークン取得依頼を処理
app.post(`/aiai`, (req, res) => {
processToken(funcs.aiai, req, res)
});
app.post(`/getRyme`, (req, res) => {
processToken(funcs.getRyme, req, res)
});
//認証と関数用トークンの発行(トークンのfuncでsocket.ioのルーム割り当てが決まる)
function processToken(func, req, res) {
//send token
if (req.body.authToken == authToken) {
//トークンには機能とユーザー名(未使用)を含める。
var funcToken = jwt.sign({func:func, user:req.body.user}, jwtKey,
{
expiresIn: '24h'
});
res.send(funcToken);
} else {
res.send("err");
}
}
//接続先時、クライアントのトークンを確認して対応したルームにjoinさせる。
io.sockets.on('connection', (socket) => {
jwt.verify(socket.handshake.query.token, jwtKey, (err, decoded) => {
if (err) {
//エラーメッセージを返す
socket.emit('join', { data: "error" });
console.log('connection error:' + err);
} else {
//正規のトークンを受信
socket.join(decoded.func);
socket.emit('join', { data: "welcome function:" + decoded.func });
console.log('room joined! func:' + decoded.func);
}
});
});
//start server
http.listen(port, function () {
console.log('listening on *:' + port);
});

クライアント(RaspberryPi)

クライアントサイドのソース全文です。
今回はアイアイの時と違って、韻ノートにRestAPIがないため、
puppeteerというwebブラウザをコントロールする
ライブラリを使ってブラウザ操作をエミュレートして韻の検索結果を得ました。
また、puppeteerをRaspberry Piで扱う上でハマッたポイントは別の記事にまとめています。
(なお、結果の応答が10秒程度と長かった主要因はpuppeteerの処理がRaspberryPiにとって重かったからです。まぁ韻を予想して楽しむ時間だと思えば良いですね)

// このプログラムはwebサーバーにPOSTで認証トークンを送信し、機能トークンを受け取る。
// 機能トークンをSocketIOで渡し、サーバーと接続する。
// 接続したサーバーからオリジナルの文字を受け取り、puppeteerで韻ノートへ送信。結果を受け取り
// google-home-notifierでGoogle Homeへ送信する。
// google home settings
var googlehome = require('google-home-notifier');
var language = 'ja';
googlehome.ip('192.168.123.45');
googlehome.device('Google Home', language);
// server settings
const BaseUrl = 'https://iotwebappraiot.azurewebsites.net/';
const aiaiUrl = BaseUrl + 'getRyme/';
const authQuery = {
authToken: "D462253D54FB3C4BE77AE1992341A279",
user: "nullo"
}
// InNote setting
const addressOfInNote = 'http://in-note.com/';
// serverから機能トークンを得る
const axios = require('axios');
axios.post(aiaiUrl, authQuery)
.then((res) => {
if (res.data != "err") {
//サーバー認証用クエリに機能トークンを設定
let funcQuery = {
query: {
token: res.data
}
};
//ソケット通信開始
startSocket(funcQuery);
} else {
console.log('auth error:', err);
}
})
.catch(err => {
console.log('err:', err);
});
// socket.IO通信の開始とイベント登録
function startSocket(funcQuery) {
// socket IOを接続先(Azureサーバー)指定で読み込む
const io = require('socket.io-client');
let socket = io(BaseUrl, funcQuery);
// 機能トークンの認証結果がサーバーから返ってくる
socket.on('join', (result) => {
console.log("join:" + result.data);
});
socket.on('ifttt', (origin) => {
console.log("\nI received original word:\n" + origin + "\n");
// サーバから受け取った語句をwebサービスの韻ノートに送り、韻を受け取る
(async () => {
const say = await getAndModifyRyme(origin)
.catch(() => 'ごめん、韻が見つからなかったよ');
sayGoogleHome(say);
})();
});
}
//sayの内容をgoogle homeへ送信する。(話させる)
function sayGoogleHome(say) {
try {
googlehome.notify(say, (res) => {
console.log(res);
});
//error
} catch (err) {
console.log(err);
}
}
// 韻ノートにoriginから韻を得て、発話内容に加工する。
async function getAndModifyRyme(origin) {
label = "exec"
console.time(label);
getRyme = new ScrapeRymeInNote();
let data = await getRyme.scrapeRhyme(origin, addressOfInNote);
let say = origin + "\n";
for (let i = 0; i < data.length; i++) {
say += (i + 1) + ": " + data[i] + " (" + data[i] + ")\n";
}
console.log(say);
console.timeEnd(label);
return say;
}
// 韻ノートからスクレイピングするクラス
class ScrapeRymeInNote {
// アクセス
async _initPuppeteer(puppeteer, address) {
const browser = await puppeteer.launch({
headless: true,
executablePath: '/usr/bin/chromium-browser',
//args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless', '--disable-gpu']
});
const page = await browser.newPage();
await page.goto(address, { waitUntil: "domcontentloaded" });
return { page: page, browser: browser };
}
// データ入手
async _getRhyme(origin, page) {
await page.type("body > div.main > div.main-search > div > input", origin);// テキスト入力
//検索ボタンクリック
await page.evaluate(() => {
document.querySelector("body > div.main > div.main-search > div > button").click();
});
await page.waitFor('span[class="word-main"]', { timeout: 10000 });// 画面遷移を待つ
// 結果のテキストを入手
let data = await page.$$eval('span.word-main', items => {
// 得た複数の結果を配列化して返す
const resultNumber = 4;
let texts = [];
for (let i = 0; i < resultNumber; i++) {
if (items[i]) {
texts.push(items[i].textContent);
}
}
return texts;
});
return data;
}
//originキーワードから韻を入手する公開関数
async scrapeRhyme(origin, address) {
const puppeteer = require('puppeteer');
const pupp = await this._initPuppeteer(puppeteer, address);
let data = await this._getRhyme(origin, pupp.page);
await pupp.browser.close();
return data;
}
}

まとめ

  • 音声操作で韻を音声で返してくれるIoTシステムを作った。
  • 韻、その繰り返しはプログラミングや人生に通ずるものがある。
  • これから自分の韻のレパートリーを増やしていきたい。

質問など有ればコメント頂ければと存じます。
それでは!