こんにちは、元気です!
私は日本語ラップが好きで毎日聞いています。
ラップを聴いてて一番楽しいと思うときはうまい韻(いん)を聞いたときですね。
良い韻は何十年も頭に残るものです。
「Kick the verse!歌詞蹴っ飛ばす! まるでストレス飛ばすジェットバス!」
ね?
単純に韻を聞いたり考えたりするのが楽しいので、自分の工学分野の用語で
韻を考えたりもするのですが、声で韻を教えてくれるシステムがあったら面白いと思って、
今回はGoogle Homeに頼むと自動でwebサイトから韻を探して踏んでくれるIoTシステムを作りました。
そもそも韻(いん)ってなに?
韻(いん,ライム)とは簡単に言うと「同じ母音で別の言葉を繰り返すこと」です。
要するに下の表の横列の言葉を使います。
例として私が凄いと思った韻を紹介します。
「一網打尽 REMIX (SHING02, MEISO, CANDLE, SPIN MASTER A-1) - 韻踏合組合」MEISOさんパート から
『I Got The 天然クイックルワイパー MEISOはカミソリSick二枚刃(シックにまいば) 伝授しに来た首振る快感』
韻もフロー(リズムの取り方、音程)もヤバい!
システムの紹介
私が温まってきたところで作ったIoTシステムの紹介動画です。
www.youtube.com
設計現場で使える韻のダイジェスト付きです。 打ち合わせなどで積極的に使っていきましょう。
「ここで使うなフォトカプラ、遅延が酷くて情報格差、世代交代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
THEN
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 pearserapp.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 rooomio.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 tokenif (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 serverhttp.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 settingsvar googlehome = require('google-home-notifier');var language = 'ja';googlehome.ip('192.168.123.45');googlehome.device('Google Home', language);// server settingsconst BaseUrl = 'https://iotwebappraiot.azurewebsites.net/';const aiaiUrl = BaseUrl + 'getRyme/';const authQuery = {authToken: "D462253D54FB3C4BE77AE1992341A279",user: "nullo"}// InNote settingconst 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システムを作った。
- 韻、その繰り返しはプログラミングや人生に通ずるものがある。
- これから自分の韻のレパートリーを増やしていきたい。
質問など有ればコメント頂ければと存じます。
それでは!