Java
spring-boot
Watson
React
GoogleHome

Watson Discovery + Google Home + Spring Boot + Reactでミュージックアプリを作る(ソース付き)

はじめに

勉強で作ったWebアプリのご紹介兼備忘録となります
参考にした資料を多く貼るよう心がけたので、似たような構成で何か作ろうとされてる方の一助になれば幸いです

Watson Music

どんなアプリ?

🤔 オフィスにBGMを流したいけど、音楽の趣味はみんな違う...公平にシャッフルしても脈絡のない選曲になるし、何かいい方法はないかな...

😃 そうだ! Watsonに曲調や今の気分をリクエストして、流す曲を決めてもらえばいいんだ!
🤗 仕事が忙しすぎて手が離せない人も、音声でリクエストを送れるようにすれば万事解決ですね!!

経緯

  • 先日いじったRetrieve And RankがDeprecatedとなり、後継としてDiscoveryが出てきたので、使ってみようと思った
    • この投稿を読んで、使うだけならそこまで難しくなさそうと思えたのが大きかったです
  • 長らくjQueryしか使えなかったが、こちらの投稿を読んで魂が震えたReact(とその周辺技術)を知りたくなった
    • AngularJSと迷いましたが、なんとなく敷居が低そうなこっちを選びました
  • フロントからバックエンドまでひとりで設計/実装したらどのようなものが作れるか試したかった
    • 最後まで読んでいただくと分かりますが、Watsonはそこまで使い込めていません...
    • 役に立つアプリを作る < 未経験技術のキャッチアップ の優先度で取り組みました

開発ポリシー

✨個々のサービスについて深掘り🕳するより、システムとしてちゃんとまとまる👪ように作る✨

  • 例えば、前回はWatsonへのデータ取り込みなどに手操作(Curlコマンド打つ)が介在してしまったので、全部バッチ化&Web画面からキックできるようにしました
  • 構成についても、前回のように訳も分からずNode-REDを使って消耗したりせず、フロント:JS、バックエンド:Javaでできるだけ統一しました
  • Discoveryに対するカスタム辞書適用や、MP3格納ディレクトリおよびアプリサーバーのクラウド化など、押さえておきたかったところもあるのですが、実装よりも環境構築の側面が強そうだったので、上記方針よりスコープから外しました

ユースケース

1. ユーザー(オフィスの人々)が、ファイルサーバーに各自MP3を格納する

MP3ファイル

2. ブラウザからWatson Musicにアクセスし、音楽取り込みボタンを押すと、ファイルサーバーのMP3がWatsonに取り込まれ、検索可能となる

バッチ実行

3. Watson Musicに選曲のリクエストを投稿すると、Watsonが流す曲を決めて再生する。リクエストの投稿や曲の再生状況は、Watson Musicを開いている全てのユーザーのブラウザに即座に反映される

即時反映

4. 再生中の曲が気に入った/気に入らなかった場合、ボタンを押してWatsonにフィードバックを行うことができる。学習ボタンを押すと、Watsonにフィードバックが取り込まれ、検索結果が改善される

フィードバック

5. 選曲のリクエストおよびフィードバックはGoogle Homeからも行うことができる

呼びかけ方 アクション
Watsonに投稿 XXXXX XXXXXで選曲リクエストを投稿
Watsonにフィードバック 気に入った 流れている曲にフィードバック(気に入った)
Watsonにフィードバック 気に入らない 流れている曲にフィードバック(気に入らない)

システム構成

Javaと他レイヤとのインターフェースとなっているものまで書いたらだいぶゴチャッとしてしまいましたが、
基本的にMySQLとEclipseを用意して、IBM Cloud上にWatsonの環境を用意すれば動かせます

構成図.jpeg

サーバサイド

Spring Bootを中心に構築しました。DBアクセスにはMyBatisを使いましたが、Connectionのことなど考えずによく、楽でした(正しくトランザクション制御できているのか若干不安ではありますが...)

バッチ

  • Spring Batchで延々とコーディングしました...色々試行錯誤したせいもあり、工数の2/3くらいこちらに掛かりました
  • Jaudiotaggerで抽出したMP3タグ情報を使って、以下のような文章を生成しました
    1. アーティスト名、アルバム名、ジャンル等にてWikipediaを検索し、記事を取得
      • MediaWiki API経由でアクセスしました。こちらの投稿がわかりやすいです
      • レスポンスはJersey Clientを使ってHTMLで受け取り、jsoupでテキストを抽出しました
      • 先日の投稿でも少し書きましたが、どうしても化けてしまうタグが少しばかりあり、諦めました...
    2. MP3に設定されているアルバムアートワークにて、Watson Visual Recognitionにアクセスし、画像分析結果を取得し、クラス分類でWikipediaの記事を取得
      • Java SDKが提供されているので、思ってたより簡単にできました。(クラス等の説明がほぼ無いのはなんですが...)
    3. 曲名にて歌詞検索サイトのJ-Lyric.netから歌詞を取得
      • 利用規約を読んだ感じダメという感じでもなさそうだったので、jsoupでやってみました。この投稿も読むと良いと思います
    4. 1.で取得したWikipediaの記事について、MP3タグと類似度の高い記事を紐付け
      • 1.だけだと、Wikipediaの検索エンジン依存になってしまうので、こちらの投稿のようにレーベンシュタイン距離を使って、表記ぶれがあっても記事を紐付けられるようにしました
    5. 1.で取得したWikipediaの記事内の各リンク(=別のWikipedia記事のタイトル)に対して、BM25を用いて重み付けを行い、記事ごとに数値の高いものも関連記事として紐付け
    6. MP3タグ情報をそのまま文章として登録
  • 生成された各文章を結合し、Watson Discoveryに登録
    • こちらもJava SDKがあるので、Visual Recognition同様に手軽に使えます
    • ドキュメント形式は幾つかから選べますが、扱いやすそうなJSONにしました
Discoveryに投入するJSON
{
  "path" : "I LOVE U\/03 03. 未来.mp3",
  "trackLength" : 322,
  "encoding" : "mp3",
  "musicTags" : {
    "ALBUM_ARTIST_SORT" : "ミスターチルドレン",
    "TITLE" : "–未来",
    "YEAR" : "2005",
    "TRACK" : "3",
    "ARTIST" : "Mr.Children",
    "ALBUM_ARTIST" : "Mr.Children",
    "ENCODER" : "iTunes v6.0.0.18",
    "ARTIST_SORT" : "ミスターチルドレン",
    "GENRE" : "Pop",
    "ALBUM" : "I LOVE U",
    "COVER_ART" : "image\/png::474078"
  },
  "musicId" : 821,
  "text" : "2005 3  I LOVE U Mr.Children Pop iTunes v6.0.0.18 image\/png::474078 ミスターチルドレン Mr.Children > I ♥ U 本来の表記は「I ♥ U」です。この記事に付けられた題名は、技術的な制限により、記事名の制約から不正確なものとなっています。 『I ♥ U』 Mr.Children の スタジオ・アルバム リリース 2005年9月21日 ジャンル ロック 時間 63分58秒 レーベル トイズファクトリー プロデュース 小林武史 チャート最高順位 週間1位(オリコン) 2005年10月度月間2位(オリコン) 2005年度年間8位(オリコン) オリコン歴代アルバムランキング226位 ゴールドディスク ミリオン(日本レコード協会) Mr.Children 年表 シフクノオト (2004年) ..."
}
BM25で関連するドキュメントを抽出
2018-05-15 21:41:18.209  INFO SimpleStepHandler     : Executing step: [enrichTextsByWikipedia]
2018-05-15 21:41:21.288  INFO TextSimilarityService    : text similarity service initialized: k1 = 2.0, b = 0.75, avgdl = 507.0
2018-05-15 21:41:21.288  INFO RelatedWikiPageProcessor  : similarity threshold: relevance > 0.05 and top 5 pages
2018-05-15 21:41:21.555  INFO RelatedWikiPageProcessor  : related wikipages: /05 -> (坂本龍一,0.060225320883512914), (アウト・オブ・ノイズ,0.05005133843882276)
2018-05-15 21:41:23.769  INFO RelatedWikiPageProcessor  : related wikipages: B.A.N.D. -> (YOUR SONG IS GOOD,0.12316413842154202), (THE ACTION,0.07945721944782655), (NAYUTAWAVE RECORDS,0.06290821951833057), (3月3日,0.06056176459670045), (カクバリズム,0.06056176459670045)
2018-05-15 21:41:23.943  INFO RelatedWikiPageProcessor  : related wikipages: CAR SONGS OF THE YEARS -> (奥田民生,0.0633027466742595)
2018-05-15 21:41:24.129  INFO RelatedWikiPageProcessor  : related wikipages: BETTER SONGS OF THE YEARS -> (奥田民生,0.064631488592711), (2008年,0.05039573681355406), (Fantastic OT9,0.05024107009270254), (僕らのワンダフルデイズ サウンドトラック,0.05024107009270254), (10月29日,0.05011415549211778)
2018-05-15 21:41:24.321  INFO RelatedWikiPageProcessor  : related wikipages: 井上陽水奥田民生 -> (井上陽水,0.05551309994270471), (奥田民生,0.05410484019209229)
2018-05-15 21:41:27.536  INFO RelatedWikiPageProcessor  : related wikipages: COBALT HOUR -> (1975年,0.05014774776777056)
2018-05-15 21:41:28.770  INFO RelatedWikiPageProcessor  : related wikipages: Cymbals -> (沖井礼二,0.09956893392254533), (矢野博康,0.05183281021348527), (土岐麻子,0.05076091736900876)
2018-05-15 21:41:28.922  INFO RelatedWikiPageProcessor  : related wikipages: The band apart -> (1,0.05656246896271063)
2018-05-15 21:41:29.520  INFO RelatedWikiPageProcessor  : related wikipages: 東京事変 -> (椎名林檎,0.05006418542278235)
2018-05-15 21:41:29.643  INFO RelatedWikiPageProcessor  : related wikipages: Cornelius -> (コーネリアス・ヴァンダービルト,0.11629418762801481), (猿の惑星,0.07515443060345392)
2018-05-15 21:41:30.052  INFO RelatedWikiPageProcessor  : related wikipages: FOR YOU -> (フォー・ユー,0.09920778243902893)
2018-05-15 21:41:30.305  INFO RelatedWikiPageProcessor  : related wikipages: The Fantastic Plastic Machine -> (Fantastic Plastic Machine,0.14436623255578224), (1997年,0.05321008399144652), (10月10日,0.0515236509976896), (日本コロムビア,0.05103357778415948), (スタジオ・アルバム,0.050605023893409457)
2018-05-15 21:41:30.607  INFO RelatedWikiPageProcessor  : related wikipages: Fantôme -> (宇多田ヒカル,0.05042149540012731), (2016年,0.050066569362434594)
2018-05-15 21:41:31.399  INFO RelatedWikiPageProcessor  : related wikipages: 小袋成彬 -> (宇多田ヒカル,0.050095502755357096)
2018-05-15 21:41:31.884  INFO RelatedWikiPageProcessor  : related wikipages: Fishmans -> (ボーカル,0.05003627945421543)
2018-05-15 21:41:31.936  INFO RelatedWikiPageProcessor  : related wikipages: 宇宙 日本 世田谷 -> (佐藤伸治,0.342280868725777), (フィッシュマンズ,0.07414079951506487), (LONG SEASON,0.05398063786198843), (スタジオ・アルバム,0.05126803822945513), (オリコンチャート,0.050995225603264395)
2018-05-15 21:41:32.087  INFO RelatedWikiPageProcessor  : related wikipages: FreeTEMPO -> (DJ,0.0502637876950748)
2018-05-15 21:41:32.153  INFO RelatedWikiPageProcessor  : related wikipages: 空気公団 -> (トイズファクトリー,0.05230039242246507), (SPACE SHOWER NETWORK,0.050821297098439105), (ポップ・ロック,0.05036436461861145), (ピアノ,0.050052459677419246)
2018-05-15 21:41:50.949  INFO RelatedWikiPageProcessor  : related wikipages: スピッツ -> (犬,0.14141142226123585), (プラネタリウム,0.06426989664685567)
2018-05-15 21:41:51.107  INFO RelatedWikiPageProcessor  : related wikipages: ハイファイ新書 -> (ハイファイ,0.14901153285452806), (オリコン,0.09449390740803565), (2009年,0.0702335178109741), (シンクロニシティーン,0.06670556020884505), (シフォン主義,0.05874506473049359)
2018-05-15 21:41:51.149  INFO RelatedWikiPageProcessor  : related wikipages: メグ -> (MEG,0.2621526213897821), (Meg,0.09827216190077466), (MEGU,0.05723108345597966), (めぐみ,0.05723108345597966)
2018-05-15 21:41:51.220  INFO RelatedWikiPageProcessor  : related wikipages: 夢みる惑星 -> (佐藤史生,0.09846793349530256), (小学館,0.08587651861800526), (プチフラワー,0.08097989398268286), (小学館文庫,0.05744113400613156), (少女漫画,0.05149429975735787)
2018-05-15 21:41:51.310  INFO RelatedWikiPageProcessor  : related wikipages: 安全地帯 -> (ゲーメスト,0.3562589491573924), (吉野二郎,0.3562589491573924), (緩衝地帯,0.3562589491573924), (路面電車,0.3066400828610433), (日本映画,0.2776149067493823)
2018-05-15 21:41:51.399  INFO RelatedWikiPageProcessor  : related wikipages: 岸田メル -> (イラストレーター,0.05039222495893547)
2018-05-15 21:41:51.651  INFO RelatedWikiPageProcessor  : related wikipages: 比屋定篤子 -> (1997年,0.05454346478565931), (笹子重治,0.054119433283179794), (那覇市,0.05110278474996579), (沖縄県,0.050697418856247733), (12月24日,0.050466135833929004)
2018-05-15 21:41:51.674  INFO RelatedWikiPageProcessor  : related wikipages: 流線形 -> (流れ,0.11173271371226531)
2018-05-15 21:42:06.699  INFO RelatedWikiPageProcessor  : related wikipages: 相対性理論 -> (時間,0.050505449840522756)
2018-05-15 21:42:07.772  INFO RelatedWikiPageProcessor  : related wikipages: 青空百景 -> (ムーンライダーズ,0.1363140503236836), (鈴木博文,0.054001764173099404), (1982年,0.05217094703759643), (MANIA MANIERA,0.05209252299043802), (鈴木慶一,0.050752580964901504)
2018-05-15 21:42:07.874  INFO RelatedWikiPageProcessor  : related wikipages: 風街ろまん -> (はっぴいえんど,0.05395896768196796)

Cymbalsなんかうまくいってますが、スピッツや安全地帯はWikipediaの元記事の選択を失敗してるっぽいですね...

オンライン

  • 一画面構成でセッション管理等も不要のため、REST APIを作成しReact + Reduxからfetchするように設計
    • こちらの記事のように、@RestControllerを付けるだけでPOJOへのマッピングまでやってもらえるので大変爽快でした
    • パスやI/Fの定義はこちらが参考になりました。(予定は無いですが)例えばフロントにネイティブアプリが増えても使えるような設計ができたと思います
    • APIの動作確認にはInsomniaを使用しました。使いやすくていい感じです
  • ユーザーからの選曲リクエストを元にWatson Discoveryを検索し、返却されたドキュメントに紐づくファイルをローカルから取得してJLayerで再生
    • 全て手で作ると大変なようですが、BasicPlayerクラスを使うと、何も考えなくても勝手に動きます。曲の順次再生も、再生終了時に呼び出されるコールバックを仕掛けておくだけで簡単に実現できました
  • 新しくリクエストが登録されたり、次の曲が再生されるタイミングで画面を更新したかったので、STOMP Over WebSocketにて適宜ソケット通信を実施
    • サーバーサイドはこちらおよびこちらでできました。例えば、新しいリクエストが投稿された際には「GET_REQUESTS」という文字列を送り、クライアントに対応するREST APIを叩かせる...という形にしています
  • バッチ機能(音楽取り込み/フィードバック学習)を起動するREST APIを提供
    • バッチをそのままControllerから呼び出すと同期的に動いてしまうので、applicationContext.xmlでSimpleAsyncTaskExecutorを管理Beanに加えておきます

DB

WWW SQL DesignerでER図ぽいものを書いてみました。ほんとに何でもブラウザでできますね...

ER図
※重要な列のみ抜粋

  • Discoveryは、既存の文書ファイルをそのまま取り込んで検索するというアプローチも選択可能ですが、今回はMp3から文書を作る必要があったので、普通にRDBを使用
  • music、music_tag、artworkに対して、Wikipedia等からtextとして取得した文字情報をtext_tagにて紐付け、music毎にdiscovery公開用のdocumentを生成
    • バッチ起動時にmissing_atに日付を入れ、走査中に見つかったもののみnull -> 見つからなかったものはdiscoveryより削除など、それなりに実運用を考えて設計しました
    • cacheは、WikipediaやVisual Recognitionへリクエストしまくって迷惑が掛かったり課金されないよう、HTTPレスポンスの生データを取っておくテーブルになります。実運用時はリクエスト後一定期間過ぎたものは消す...などしてもいいかも
  • requestは検索文を保持、trackは検索文により再生された曲を保持、feedbackは曲に対して気に入った/気に入らなかったが押された数を保持
    • 学習処理時に「検索文に対して、あるドキュメント(今回は曲)がどれだけの関連度(Discoveryでは整数値)を持つか」というデータを作る必要があるため。関連度はバッチ内で気に入った/気に入らなかったの割合で計算します
  • DDL実行は、Spring Bootの起動時にsqlファイルが呼ばれるようにして対応しました。今回のようにPoC的に開発するにはだいぶ便利に思えました

クライアントサイド

  • Pingendoでレイアウトの枠組みを作成
    • 今までHTMLとCSSは基本的に手で書いてたのですが、部品をドロップするだけでメキメキ画面が出来上がっていき大変楽でした
      • ただ、手で書いた経験がなかったら微修正などは難しいようにも思いました...
  • 枠組みができたら、Atomで開発サーバーを立ち上げて画面の部品ごとにReactに(勉強しながら)移植
    • 初めて触れるものなので読み物から入りたいと思い、React開発 現場の教科書を買いました。概要や周辺知識を広く知るにはとてもよい本と思います
  • Reactに画面を移植したら、Reduxで動きを付ける
    • Redux-Sagaも導入したのですが、理解にあたりこの投稿に大変助けられました。これがなかったら諦めてたかもしれません...
    • Node.jsの開発サーバーはlocalhost:3000、tomcatはlocalhost:8080で立ち上がるので、Charlesでリクエストのパス等を適宜変換しました
    • ステータスバーのアニメーションはReact Transition Group、ソケット通信にはSockJS-clientSTOMP.js、時刻処理にMoment.js、fetchのポリフィルにgithub/fetchなど、色々と触れて大変楽しかったです

外部サービス

Watson Discovery

  • 環境構築については、こちらの投稿を参考に、環境作成 -> コレクション作成 -> 日本語化 & エンリッチメント削除まではWebのツールから行いました
  • Retrieve and Rankと比べて料金体系が変わっており、ライトプランを使えばお金はかかりません
  • ドキュメントも日本語のちゃんとしたものが公開されているので、ご一読をお勧めします。こちらとか、結構役立つと思います
    • Apache Solrをカスタマイズした程度のものだったR&Rと比べて、機能は絞られたのかもしれませんが、全体的な親切さはだいぶ向上したように感じます
  • 一時期、全然ドキュメント入れてないのに以下の使用料の表示がぶっ壊れて容量オーバーみたいな警告が出てて、怖くなってサイズを減らしたのですが、今見たら直っててなんなんだ...と思いました

使用料

Watson Visual Recognition

  • Discovery同様、ライトプランで十分なため無料で使えます
  • Custom Classifierを作る要件でもなかったので、IBM Cloudにてサービス登録 -> 資格情報の生成だけですぐに使えるようになりました
  • 肝心の戻り値はなんとも言えないものです...自分の使い方がよくないのも分かりますが、どういう使い方をするといいでしょうか...

例:
c9f73f67.jpg

VisualRecognitionの戻り値
{
  "images" : [
    {
      "image" : "90C031B45C058E5F56A002ADEC186787",
      "classifiers" : [
        {
          "classes" : [
            {
              "score" : 0.61599999999999999,
              "type_hierarchy" : "\/\/\/大人",
              "class" : "大人"
            },
            {
              "score" : 0.85499999999999998,
              "class" : "人"
            },
            {
              "score" : 0.51500000000000001,
              "type_hierarchy" : "\/\/外交官",
              "class" : "外交官"
            },
            {
              "score" : 0.5,
              "type_hierarchy" : "\/\/官公吏",
              "class" : "官公吏"
            },
            {
              "score" : 0.81599999999999995,
              "class" : "橙赤色"
            },
            {
              "score" : 0.39900000000000002,
              "class" : "群青色"
            }
          ],
          "classifier_id" : "default",
          "name" : "default"
        }
      ]
    }
  ],
  "custom_classes" : 0,
  "images_processed" : 1
}

Google Home関連

一通りできたところで、最後に半日ほどいじったら普通に動いてびっくりしました😲
MP3を再生しているサーバーがクラウド上に乗っていたら、IFTTTから直接REST APIを叩く...でもよかったかもしれませんが、
今回はLAN(localhost)上にあるサーバーにリクエストを回していく必要がある(けど、固定IPなんて持ってないしましてやポートなんて開けたくない)ということだったので、IoTの世界で使われているらしいMQTTの仕組みを使ってリクエストを受け取るようにしました
(今書きながら調べてたら、こういうものもあるんですね...機会があれば触ってみたいです)

  • 1. Google HomeをiOSアプリからGmailのアカウントに紐付け
  • 2. BeeBotteにてXSプランでアカウント登録。watson_musicのchannelを作成

Beebotte

  • 3. 1.のアカウントでIFTTTに登録し、Googleアシスタント -> Webhooksで2.で作ったchannelにpublishするレシピを作成
    • 2.と3.はこちらの通りにやったらすぐできました
    • 「ワトソンミュージックに投稿 XXX」だと、長いせいか認識してくれなかったので、「ワトソンにXXX」にしました

IFTTT

投稿JSON
{"data":[{ "text" : "{{TextField}}", "type" : "google_assistant" }]}
気に入ったJSON
{"data":[{ "feedback" : "LIKE", "type" : "google_assistant" }]}
気に入らなかったJSON
{"data":[{ "feedback" : "DISLIKE", "type" : "google_assistant" }]}
  • 4. Webアプリ起動時に3.のチャンネルをsubscribeするMqttクラスを作成
    • 実装はpahoで行いました。この投稿がわかりやすかったです
    • ここにも記載がありますが、URLに「ssl://mqtt.beebotte.com:8883」、ユーザー名に「token:{2.のChannel Tokenの値}」を指定すると接続可能です
  • 5. Google Homeに「ワトソンに投稿 XXX」と話しかけるとリクエストが送信されます

所感

  • 1月〜3月にバッチ、4月にWeb、5月に結合&仕上げ、という感じでできました
    • 時間は測ってませんが、体感15人日〜20人日くらいかかったような気がします...疲れました
  • 前述の通り、よほど使わなければ費用はまずかからないと思います。Watsonのライトプランは一月放置するとインスタンスが削除されてしまうようですが...
  • 「なんでこの検索文でこの曲なんだろう...」というものもままありますが、とりあえずちゃんと動くものができたので満足です🍜
    • Wikipediaをでたらめに紐付けてるだけだから、当然の帰着とも言えます😞
      • 次に音楽を扱うなら、楽曲から言語情報を抽出する...くらいのことを頑張らないといけないなと思いました
    • リクエストをしないとランダム選曲しますので、とりあえずしばらく使い続けようと思います(本末転倒)

ソース

githubに公開しました:octocat:

https://github.com/yktakaha4/watson-music
https://github.com/yktakaha4/watson-music-web