こんにちは、元気です!
私は日本語ラップが好きで毎日聞いています。
ラップを聴いてて一番楽しいと思うときはうまい韻(いん)を聞いたときですね。
良い韻は何十年も頭に残るものです。
「Kick the verse!歌詞蹴っ飛ばす! まるでストレス飛ばすジェットバス!」
ね?
単純に韻を聞いたり考えたりするのが楽しいので、自分の工学分野の用語で
韻を考えたりもするのですが、声で韻を教えてくれるシステムがあったら面白いと思って、
今回はGoogle Homeに頼むと自動でwebサイトから韻を探して踏んでくれるIoTシステムを作りました。
そもそも韻(いん)ってなに?
韻(いん,ライム)とは簡単に言うと「同じ母音で別の言葉を繰り返すこと」です。
要するに下の表の横列の言葉を使います。
例として私が凄いと思った韻を紹介します。
「一網打尽 REMIX (SHING02, MEISO, CANDLE, SPIN MASTER A-1) - 韻踏合組合」MEISOさんパート から
『I Got The 天然クイックルワイパー MEISOはカミソリSchick二枚刃(シックにまいば) 伝授しに来た首振る快感』
韻もフロー(リズムの取り方、音程)もヤバい!
システムの紹介
私が温まってきたところで作ったIoTシステムの紹介動画です。
設計現場で使える韻のダイジェスト付きです。 打ち合わせなどで積極的に使っていきましょう。
「ここで使うなフォトカプラ、遅延が酷くて情報格差、世代交代iCoupler、これが正しい開発だ。Yeah...」
これは便利ですね。なかなか実用性のあるシステムになった感があります。
文字で検索する場合と比べたメリットは、音声入力なので文字入力不要ということ、
声で入力し結果も声で聞くことで音のイメージを正確に把握できることですね。
デメリットは「勢至菩薩」などの複雑な単語は音を聞いただけでは理解できないことですが、
一応、raspberry PiにSSHで接続すれば以下のように結果をモニターする事も出来ます。
(声の回答より先に結果がでます)
これから毎朝起き掛けに使って韻のレパートリーを増やしていきたいですね。
技術解説
動画内にもあった概要図です。
① 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 : Google Assistant
THEN : Webhooks
Tokenを持つPOSTだけ後段で受け付けられます。
なおこのTokenは現在無効です。
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'] }); 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システムを作った。
- 韻、その繰り返しはプログラミングや人生に通ずるものがある。
- これから自分の韻のレパートリーを増やしていきたい。
質問など有ればコメント頂ければと存じます。
それでは!