Edited at
ClojureDay 18

ClojureScriptを使ってGoogle Home に say させる

皆さま、おはようございます。

自称「永遠のClojure Newbie」toku345です。

2018年も Clojure Advent Calendar に馳せ参じました。

お願い

本記事はぶっちゃけそんなに深い内容ではありません...

各種アドベントカレンダーで胃もたれした際の箸休め程度にお読みいただければ幸いです。

去年は ClojureScriptでAlexaスキルを書いてみた という内容で書かせていただきました。

今年は ClojureScript で Google Actions1 作成ネタでも書くか...と考えていたのですが、それだとあまり面白くなさそうなので、ちょっと趣向を変えて Google Home版 say コマンドを作ってみました2


say コマンドと何?

macOS に備わっているコマンドで任意の文字列を音声にして再生してくれるコマンドです。

使い方はこんな感じで、console を開いて

$ say "こんにちは世界"

と入力すると、mac が 「こんにちは世界」としゃべってくれます。

これを mac ではなく、Google Home がしゃべるようトライしてみました。


実現方法の説明

Text -> 音声ファイルに変換して、Goole Home に喋らせるという部分は google-home-notifier という npm を使用すると、あっという間に解決しました😆


Google Home Notifier の役割り



  1. ネットワーク上の Google Home Mini 探索


    • mDNSを使って同一ネットワーク上を探索して Google Home を見つけます3




  2. text → 音声変換


    • Google の Cloud text-to-speech を使ってtext → 音声ファイルに変換し、音声ファイルURLを取得する




  3. Google Home に喋らせる(=再生させる)


    • Chromecast cast v2 プロトコル4を使ってGoogle Home にファイル再生させる




google-home-notifier のシンプルな使用例

var googlehome = require('google-home-notifier');

var language = 'ja'; // if not set 'us' language will be used

googlehome.ip('192.168.3.11', language);

googlehome.notify('Hey ClojureScript', function(res) {
console.log(res);
});

これを ClojureScript で書いてみると↓

(require '["google-home-notifier" :as googlehome])

(defonce device-ip "192.168.3.11")
(defonce language "ja")

(.ip googlehome device-ip language)
(.notify googlehome "Hey ClojureScript" (fn [res] (prn res)))


構成図


Let's Coding in ClojureScript!

それでは、say コマンドとして console 上から使えるようにしていきましょう!

今回は ClojureScript -> JavaScript は shadow-cljs を使用しました。

Leiningen & cljsbuild の組み合わせよりもストレス無く使えてよかったです5


ディレクトリ構成

※ build時作成されるディレクトリは一部省略しています

.

├── node_modules/
├── package-lock.json
├── package.json
├── shadow-cljs.edn
└── src/
    └── main/
    └── app/
    └── core.cljs

設定ファイルはこんな感じ。

シンプルですね☺️


shadow-cljs.edn

{:source-paths

["src/main"]

:dependencies
[]

:builds
{:app {:target :node-script
:output-to "bin/say"
:main app.core/main!
:devtools {:after-load app.core/reload!}}}


npm を使いたい場合は別途 package.json で管理すればOKなので、すごくお手軽です。

※ google-tts のバージョンのアップに本家が追従してくれないので、今回は私がforkして手を入れたものを使っています。


package.json

{

"dependencies": {
"google-home-notifier": "toku345/google-home-notifier#update-google-tts-api"
},
"devDependencies": {
"shadow-cljs": "^2.7.8",
"source-map-support": "^0.5.9",
"ws": "^6.1.2"
},
"scripts": {
"watch": "shadow-cljs watch app",
"test": "shadow-cljs compile test",
"build": "shadow-cljs compile app"
}
}

npm install すれば、ClojureScript から参照できるようになります。

肝心のClojureScriptのコードはこんな感じ。


src/main/app/core.cljs

(ns app.core

(:require ["google-home-notifier" :as googlehome]))

(defonce device-ip "192.168.3.11")
(defonce language "ja")

(defn say [text]
(.ip googlehome device-ip language)
(.notify googlehome text (fn [res] (prn res))))

(defn reload! []
(println "Code updated."))

(defn main! [& args]
(say (first args)))



build

$ npx shadow-cljs release app

$ chmod +x bin/say

これで bin/say スクリプト(実体はnode.jsスクリプト)が生成され

$ ./bin/say "こんにちはClojureScript"

と実行と、Google Home が「こんにちはClojureScript」とsayしてくれます。


デモ

コードはこちら ※ 一部開発のためのコードを含んでいるため↑と完全一致していません...


まとめ


  • ClojureScript + shadow-cljs を使えば、すごく簡単にnpmと連携するコードを書ける

  • google-home-notify を使えば簡単に Google Home おしゃべりさせることができる

  • ClojureScript 書くのすっごく楽しいよ





  1. Alexaで言うところのSkillのこと 



  2. というのはタテマエで、ホンネは会社のAdvent Calendarや会社ブログ、おまけに前々職のAdvent Calendarにも召喚されてしまったので、余裕が無かったのです。ごめんなさい... 



  3. 試してみた所、手元の環境では動かなかったので、今回は Google Home のIPアドレスを直接指定して狙い撃ちしています。 



  4. Google は Chromecast / Google Home とのやり取りをするプロトコルを公開していないので、有志が解析したものをそう呼んでいるみたい。詳しくは node-castv2を参照してください 



  5. 去年、Alexa Skill を作った際は Leiningen & cljsbuild を使ったのですが、REPLの立ち上がりやbuildの待ち時間が長く、設定ファイルもごちゃごちゃしていて苦労しました...