WonderPlanet Tech Blog

アドバンストテクノロジー部やCTO室のメンバーを中心に最新の技術情報を発信しています。

D3.js v5で積み上げ折れ線グラフ(表示切り替え機能付き)を作成する方法

こんにちはAT部の@y-matsushitaです。
前回はD3.js v5でツールチップ付きのグラフを作成してみました。
今回は少し発展させて積み上げ折れ線グラフを作成してみたのでご紹介いたします。
また値が全体に対してどれぐらいの比率なのか視認できるように比率での表示に切り替えられるようにもしましたので併せてご紹介いたします。
本投稿ではD3.jsのv5で動作するコードを記載していきます。

前回の記事はこちら

tech.wonderpla.net

積み上げ折れ線グラフ

以下のページに作成した積み上げ折れ線グラフのデモとコードを載せています。
前回同様コードが長いのでコード全体は以下のデモページからご参照ください。
前回と異なる要所だけピックアップして説明していきます。

デモとソースコード

f:id:y-matsushita:20180606165606p:plain:w600

データの複製

読み込んだデータを複製し合計値を表示するデータと比率を表示するデータに分けます。
比率用のデータに対して行った変更を元のデータに影響が出ないようにしたいので、一旦JSONを経由して複製を行います。
合計で表示するデータは取得時のまま保管、比率で表示するデータは後ほど0〜1で全体に占める割合に変換します。

stacked_line_chart.js 23~28行目

d3.json("data.json").then(
  function(data) {

    // 比率表示用にデータを複製
    var showData = data;
    var ratioData = JSON.parse(JSON.stringify(data));

データを比率に変換

ここで複製しておいたデータを通常の数値データから比率のデータに変換します。
まずは階層全ての値の合計値を求め、そこから各階層ごとに占める比率を算出しデータを入れ替えます。

stacked_line_chart.js 45~54行目

ratioData.forEach(function(d, i) {
  // 階層全ての合計値を求める
  for (j = 0, t = 0; j < keys.length; ++j){
    t += parseInt(d[keys[j]]);
  }
  // 合計値における比率を求める
  for (j = 0; j < keys.length; ++j){
    ratioData[i][keys[j]] = d[keys[j]] / t;
  }
});

積み上げ折れ線グラフの描画

d3.area()で描画する面エリアの指定を行います。
xに日付、y0に下側の座標、y1に上側の座標が入ります。
実際に値が入っていくのはその下のd3.stack()からで、layer作成のループ時にキーごとに下の階層から順番にデータが入っていきます。

stacked_line_chart.js 65~78行目

var area = d3.area()
    .x(function(d, i) {
      return x(d.data.day); })
    .y0(function(d) { return y(d[0]); })
    .y1(function(d) { return y(d[1]); });

var layer = g.selectAll(".layer")
    .data(d3.stack().keys(keys)(data))
    .enter().append("g")
    .attr("class", "layer")
    .append("path")
    .attr("class", "area")
    .style("fill", function(d) { return z(d.key); })
    .attr("d", area);

ボタン押下時のデータの差し替え

ボタンの取得を行い、表示非表示の切り替えフラグを初期化します。

stacked_line_chart.js 210~213行目

// 合計⇄比率変換用ボタン
var changeBtn = d3.select("#changeBtn");
// 比率表示状態かどうか
var isRatio = false;

取得したボタンが押下されたら現在の表示状態のフラグisRatioを反転させ現在の表示状態を切り替えます。

stacked_line_chart.js 215~216行目

changeBtn.on("click", function() {
  isRatio = !isRatio;

比率で表示(isRatioがtrue)であれば全体における各層の比率で表示します。
表示させるデータは先ほど変換したratioDataに差し替えます。
ratioDataに入っている値の範囲が0〜1のため、domainの範囲も0〜1に変更します。
またy軸の目盛り表記がこのままだと合計の表示のままなため、%表記の軸に変更します。
もし合計で表示(isRatioがfalse)だった場合は上記の処理の逆で比率のデータを合計のデータに差し替えてdomainとy軸の目盛り表記も元に戻します。

stacked_line_chart.js 217~252行目

if(isRatio){
  // 比率表示
  y.domain([0, 1]);
  showData = ratioData;

  d3.selectAll(".axis--y")
  .call(
    d3.axisLeft(y)
    .ticks(6)
    .tickSizeInner(-cWidth)
    .tickFormat(function(d) { return d*100 + "%"; })
  );
  d3.select(".axis-title")
  .text("比率");

}else{
  // 合計表示
  y.domain([0, d3.max(data, function(d) {
    for (i = 0, t = 0; i < keys.length; ++i){
      t += parseInt(d[keys[i]]);
    }
     return t;
   }
  )]).nice();
  showData = data;

  d3.selectAll(".axis--y")
  .call(
    d3.axisLeft(y)
    .ticks(6)
    .tickSizeInner(-cWidth)
    .tickFormat(function(d) { return d/1000 + "k"; })
  );
  d3.select(".axis-title")
  .text("合計");
}

描画の切り替えアニメーション

先ほど差し替えたデータで再度積み上げ折れ線グラフの内容を描画し直します。
描画はtransition()duration(500)で徐々に変化させることができます。
この場合数値はミリ秒なので0.5秒で変化させています。

stacked_line_chart.js 254~261行目

d3.selectAll(".layer")
    .data(d3.stack().keys(keys)(showData))
    .select("path")
    .attr("class", "area")
    .transition()
    .duration(500)
    .ease(d3.easeLinear)
    .attr("d", area);

最後に

今回は前回に続きD3.jsのv5で積み上げ折れ線グラフを作成しました。
前回の記事と重複する箇所は省略しているため、もし気になる箇所があれば前回の記事も参考にしてみてください。 tech.wonderpla.net

D3.js v5でデータを可視化 - ツールチップ付きのグラフを作成する方法

こんにちはAT部の@y-matsushitaです。
今回はデータ分析結果の表示を行うために、ブラウザで動的コンテンツを描画するJavaScriptライブラリであるD3.jsを試してみたのでご紹介します。
本投稿ではD3.jsのv5で動作するコードを記載していきます。D3.jsの情報はv3向けが多かったのですが、せっかくなので新しいv5向けに書いてみました。バージョンによってコードの書き方が結構異なるので、見つけたサンプルが動作しない場合はバージョンの違いを疑ってみると良いかもしれません。

d3js.org

ツールチップ付き棒グラフ

以下のページに作成した棒グラフのデモとコードを載せています。
コードが長いのでコード全体は以下のデモページからご参照ください。本投稿では要所だけピックアップして説明していきます。

デモとソースコード

f:id:y-matsushita:20180605204311p:plain:w600

D3.jsの読み込み

index.htmlのsvgの下に以下のタグを書いてD3.jsを読み込みます。
読み込むバージョンはv5を指定して、その後に棒グラフを描画するコードが書かれたjsファイルを読み込みます。

index.html 13~14行目

<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="bar_chart.js"></script>

スケール設定

軸の表示領域をマージンを除いた幅に設定します。
paddingは0.2でバーとバーの間に20%の隙間を作ります。
yの座標は値が大きいほどに画面下に行くため、rangeの値を反転([cHeight, 0])しておきます。

bar_chart.js 17~18行目

var x = d3.scaleBand().rangeRound([0, cWidth]).padding(0.2);
var y = d3.scaleLinear().range([cHeight, 0]);

以下の部分でx軸に日付を、y軸に値の幅を設定します。
y軸はそのままだと最小値が0扱いになってしまうため、少し幅に余裕をもたせてあります。

bar_chart.js 28~29行目

x.domain(data.map(function(d) { return d.day; }));
y.domain([d3.min(data, function(d) { return d.value; }) / 1.005, d3.max(data, function(d) { return d.value; }) * 1.005]);

x軸とy軸の表示

x軸はグラフの高さcHeight分、y方向に座標をズラしてグラフの下に配置します。
また、表示後に日付に応じて曜日が土日であれば色を変えられるようにクラスを付与しておきます。 tickに対して直接classを指定することが出来なかったため、後の処理で日付から曜日を推定し土曜日であればsat、日曜日であればsunのクラスを付与しています。

bar_chart.js 44~58行目

g.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + cHeight + ")")
    .call(
      d3.axisBottom(x)
      .ticks(12)
      .tickFormat(d3.timeFormat("%Y/%m/%d"))
    );
// 横の目盛りを日付のみにして土日にクラスを付与する
var ticks = d3.selectAll(".axis--x text");
ticks.attr("class", function(d){
  if(d3.timeFormat("%a")(d) == "Sat") return "sat";
  if(d3.timeFormat("%a")(d) == "Sun") return "sun";
  return "weekday";
}).html(function(d) {return formatTime(d);});

色の指定は satsunクラスに対してcssで行います。

style.css 54~59行目

.sat{
  fill:#1874CD;
}
.sun{
  fill:#f2594b;
}

y軸では .tickSizeInner(-cWidth) でx軸方向に逆側へグリッドを伸ばします。
また text を追加して回転させ軸に沿う形で値を表示しました。

bar_chart.js 61~75行目

g.append("g")
    .attr("class", "axis axis--y")
    .call(
      d3.axisLeft(y)
      .ticks(6)
      .tickSizeInner(-cWidth)
      .tickFormat(function(d) { return d/1000 + "k"; }))
    .append("text")
    .attr("class", "axis-title")
    .attr("transform", "rotate(-90)")
    .attr("y", 6)
    .attr("dy", ".71em")
    .style("text-anchor", "end")
    .attr("fill", "#5D6971")
    .text("アクセス量");

棒グラフの描画

取得したdataをもとにバーを描画します。x軸方向に日付を設定し、y軸方向に値を入力しました。

bar_chart.js 88~102行目

g.append("g")
    .selectAll("rect")
    .data(data)
    .enter()
    .append("rect")
    .attr("class", "bar")
    .attr("x", function(d) {
      return x(d.day);
    })
    .attr("y", function(d) {
      return y(d.value);
    })
    .attr("width", x.bandwidth())
    .attr("height", function(d) { return cHeight - y(d.value); })
    .attr("fill", "steelblue");

フォーカスとツールチップの設定

まずグラフの上に透明な rect をグラフと同じ大きさで配置してレイヤーのように覆います。
この rectにはoverlayクラスを与えておき、cssでfill: none;pointer-events: all;を適用させておくとマウスイベントの効く透明なレイヤーが作成できます。
このレイヤーにマウスが乗った場合(mouseover)フォーカスとツールチップを表示させます。
逆にマウスがグラフ上から外れた場合(mouseout)非表示にします。そしてマウスが動いている場合(mousemove)はマウスに近い位置のバーをフォーカスしてツールチップに情報を表示します。

bar_chart.js 105~122行目

svg.append("rect")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    .attr("class", "overlay")
    .attr("width", cWidth)
    .attr("height", cHeight)
    .on("mouseover", function() {
      tooltip.style("display", "block");
      focus.style("display", "block"); })
    .on("mouseout", function() {
      // tooltipとfocusを非表示
      tooltip.style("display", "none");
      focus.style("display", "none");
      // 棒グラフのfocus(一旦focusクラスを全て外す)
      g.selectAll(".bar").attr("class",function(d) {
        return "bar " + formatClass(d.day);
      })
     })
    .on("mousemove", mousemove);

マウス移動時の設定

まずはツールチップを作成しておきます。
日付と値を表示できるようにします。またツールチップにはマウスが乗ってしまっても動作に影響がでないようにcssにpointer-events: none;を適用させておきます。またツールチップの座標を絶対位置で指定できるようにposition:absolute;を適用させておきます。見た目についてはお好みでshadowなどを適用させると見やすくなると思います。

bar_chart.js 125~127行目

var tooltip = d3.select("#contents").append("div").attr("class", "tooltip"),
    tt_date = tooltip.append("time").attr("class", "tt_date"),
    tt_value = tooltip.append("div").attr("class", "tt_value");

style.css 34~36行目

.tooltip {
  position: absolute;
  pointer-events: none;

マウスの移動時はマウス位置をもとにinvertで日付情報に逆変換してマウス座標に近い日付を取得します。
ツールチップは特定した日付の情報を設定、位置をマウスカーソルから少しずらして表示します。
またマウスカーソルがグラフの中央より右側にある場合はツールチップをマウスカーソルの左側に表示するようにします。これによりマウスを画面端にもっていったときにグラフ外に表示されて崩れてしまうという事態を防ぎます。
このとき.duration(200).ease(d3.easeLinear)を設定しておけば移動をアニメーションさせて滑らかな移動の表現ができるようになります。
また特定した日付の.barクラスにcssで色を変えたfocusクラスをattrで追加しておくことでバーの色を変更することができます。これによりマウス座標に近い日付のバーにフォーカスを当てることができます。

bar_chart.js 128~162行目

function mousemove() {
  var x0 = x.invert(d3.mouse(this)[0]),
      i = bisectDate(data, x0, 1),
      d0 = data[i - 1],
      d1 = data[i],
      d = x0 - d0.day > d1.day - x0 ? d1 : d0;

  tt_date.html(function() {return formatTime(d.day);});
  tt_value.html(function() {return "アクセス数:"+d.value});

  // マウスの位置によりtooltipを表示位置を変更(右側 or 左側)
  var centerX = cWidth / 2;
  var tooltipPosX = 5,
      tooltipPosY = -15;
  if(d3.mouse(this)[0] > centerX) {
    // tooltipの大きさ分、左側にx座標をずらす
    tooltipPosX = -tooltip.node().getBoundingClientRect().width;
  }
  tooltip.transition()
        .duration(200)
        .ease(d3.easeLinear)
        .style("left", (d3.event.pageX + tooltipPosX) + "px")
        .style("top", (d3.event.pageY - tooltipPosY) + "px");
  focus.attr("transform", "translate(" + parseInt(x(d.day) + x.bandwidth()/2) + "," + 0 + ")");
  focus.select(".x-hover-line").attr("y2", cHeight - margin.top);

  // 棒グラフのfocus(一旦focusクラスを全て外す)
  g.selectAll(".bar").attr("class",function(d) {
    return "bar " + formatClass(d.day);
  })
  // 棒グラフのfocus(選択したものにfocusクラスをあてる)
  g.select("."+formatClass(d.day)).attr("class",function(d) {
    return "bar " + formatClass(d.day) + " focus";
  })
}

最後に

今回はD3.jsのv5で動きのある棒グラフを作成しました。
このコードを少し書き換えればツールチップ付きの折れ線グラフなども作れます。
ツールチップ付きの折れ線グラフのサンプル

値を入れるだけでサクッと作れるわけではないため少し苦労しますが、
ブラウザ上で何かグラフなどの表現したい場合には頼りになりますね。

Dialogflowでちょっとしたチャットボットを作ろう

アドバンストテクノロジー部の近藤です。
Amazon EchoやGoogle Homeなどのスマートスピーカーが最近テレビCMするなどしていて普及し始めています。
今回はGoogle Homeにも利用できるDialogflowを使ってちょっとしたチャットボットのようなものの作り方を紹介します。

必要なもの

・Googleアカウント(DialogflowとActions on Googleのログインに利用)
・連携したいメッセージアプリのアカウント
・Google Homeで動かしたい場合はその実機

Dialogflowとは

https://dialogflow.com
Dialogflowは音声およびテキストベースの自然言語での会話型インターフェイスを構築するためのサービスです。
たとえば「誕生日おめでとう」という呼びかけに対して、「ありがとう」という返答をさせるようなものを作ることができます。
Dialogflowのコンソールではどのような言葉に対してどのように返答するかの設定していきます。
Dialogflowで作成したチャットボットは、Googleアシスタント、Facebook Messenger、Slackなどのサービスに対応していて、それらのサービス上でも動作させることができます。
スマートスピーカーのGoogle Homeで音声による会話をすることも可能です。

Actions on Googleとは

https://console.actions.google.com
Actions on Google では、Dialogflowで作成したアプリをシミュレーターで動作確認をしたり、アプリを公開するための設定・審査への提出をすることができます。
審査を通過すると、Google Homeを使っているユーザーがアプリを利用することができるようになります。
Google Home以外にもSlackやFacebookメッセンジャーなどのメッセージアプリに組み込んで公開する方法があります。

ハンズオン

「元気ですか?」と聞いたら、いくつかのパターンで返答してくれるボットを作ってみましょう。

Agentを作る

Dialogflowを開いたら、左側のメニューから「Create new agent」を選択し、Agentを作成します。 f:id:HidehikoKondo:20180226211344p:plain

今回のAgentの設定は下記のように設定しましょう。
Agent name:HowAreYou?(日本語不可です)
DEFAULT LANGUAGE:Japanese
DEFAULT TIME ZONE:(GMT+9:00)Asia/Tokyo
GOOGLE PROJECT:Create a new Google project
設定項目を記入したら「CREATE」ボタンを押しましょう。
f:id:HidehikoKondo:20180220171719p:plain

Intentsの設定

メニューから「Intents」を開き、「CREATE INTENT」ボタンでインテントを作成します。
一番上の「Intent name」の欄にインテントの名前を設定します。

「User Says」の項目に、反応させたい言葉を記入します。
「元気ですか?」と設定します。
複数設定することもできるのでせっかくなので「調子はどう?」も設定しておきましょう。
f:id:HidehikoKondo:20180220171013p:plain

Text responseの欄にGoogleアシスタントが返答する言葉を記入します。
複数記入すればそのなかからランダムでどれかひとつを返してくれます。
「今日は元気いっぱいです!!」「ちょっと具合が悪いです」「まぁまぁ元気です」
今回はこの3つを設定しましょう
f:id:HidehikoKondo:20180301192155p:plain

設定したら忘れずに右上の「SAVE」ボタンを押します。

Integrationsの設定

「Integrations」のメニューを開きます。
Dialogflowで連携できるサービスの一覧が表示されていますが、ここではGoogle Assistantの欄の「INTEGRATION SETTINGS」のリンクをクリックします。 f:id:HidehikoKondo:20180301182826p:plain
ここで先程設定したIntentをGoogle Assistantで利用するための設定をします。
f:id:HidehikoKondo:20180220190111p:plain

Explicit invocationの項目でさきほど設定した「元気ですか?」を選択します。
Google Assistantで呼び出す時に言う「OK Google」に続く言葉として設定されます。

上記の設定ができたら「TEST」をクリックしてActions on Googleの画面に遷移します。

Actions on Googleの設定

ここではアプリの動作確認をするためのシミュレータを利用することができます。
文字入力により確認ですが、DialogflowのIntentsで設定したキーワードに反応するか確認することができます。

f:id:HidehikoKondo:20180226182903p:plain 日本語で利用する場合はLanguageの項目は「Japanese(Japan)」を選びましょう。
Inputの項目には予め「テスト用アプリにつないで」と入力されています。この言葉がテスト用のウェイクワードとなります。「OK Google」ではありませんので注意しましょう。(Overviewの画面で任意の言葉に変更できます)
Inputの項目にカーソルを合わせてEnterキーを押せばアプリが立ち上がります。

f:id:HidehikoKondo:20180226183635p:plain アプリが立ち上がったら、Inputの項目にDialogflowで設定した「元気ですか?」「調子はどう?」と入力してみましょう。
設定したレスポンスを返してくれます。
画面右側ではJSON形式でレスポンスがやり取りされていることが分かります。

Google Homeの実機でテスト

シミュレーターでの動作が確認画できたら、やっぱり実機で試してみたいですよね。
DialogflowにログインしているGoogleアカウントと、Google Homeに紐付けているGoogleアカウントが同じである必要があります。
この状態になっていれば、すでにテストでGoogle Homeを利用することができます。

テストで実行するにはさきほどActions on Googleでも利用したテスト用ウェイクワード「テスト用アプリにつないで」とGoogle Homeに話しかけます。
アプリが起動したら「元気ですか?」「調子はどう?」と話しかけます。
そうすると「今日は元気いっぱいです!!」「まぁまぁ元気です」などと返してくれるはずです。

メッセージアプリで動かしてみる

Dialogflowで作ったアプリはGoogle Home以外にもFacebookメッセンジャーやSlackなどのメッセージアプリに対応していたり、WEBページ内に埋め込むことも可能です。
今回はSlackでの利用例を紹介します。

Dialogflowの「Integrations」の画面でSlackを選択します。
右上のスイッチをONにします。
次に「TEST IN SLACK」ボタンを押して、Slackのワークスペースにログインします。

ログインできたら、Slackのアプリを作ります。
Launchの項目の「Create a new Slack app.」のリンクをクリックすると、Slackのアプリの作成画面に遷移します。
f:id:HidehikoKondo:20180226201648p:plain アプリ名を「HowAreYou?」と設定し、「Create App」をクリックします。
するとClient ID、Client Secret、Verification Tokenが発行されます。
これをDialogflow側の設定画面のSlack Client ID、Slack Client Secret、Slack Verification Tokenに入力します。 f:id:HidehikoKondo:20180226201835p:plain

これらの項目が入力できたら「Start」ボタンを押して設定完了です。 f:id:HidehikoKondo:20180226201555p:plain

では、Slackを立ち上げて、「Apps」の項目を見ると、「dialogflow_bot」というのが追加されています。
これを開き、Dialogflowで設定した「元気ですか?」「調子はどう?」と聞いてみましょう。
すると設定したとおりに返事を返してくれます。

f:id:HidehikoKondo:20180226202156p:plain

まとめ

このようにしてDialogflowでチャットボットを作ることができます。
今回は単純に決められたテキストをランダムで返すものを作りましたが、Dialogflowで入力されたメッセージに対してJavaScriptで動的に返答するテキストを変えることもできます。
たとえばよくある質問に対して回答をするようなものも作れるかもしれないですね。

Google Homeで音声で操作できたり、WEBページに埋め込んだり、メッセージアプリと連携したりと、使える場面もいろいろあります。
アイデア次第で面白いことが色々できそうですね。

AlexaとZoom Roomsでスマート会議スキルを開発する方法

こんにちはアドバンストテクノロジー部の@y-matsushitaです。
今回はZoom RoomsのAPIを使ってAlexaからZoom Roomsを操作するスキルを開発してみましたのでご紹介します。
先日ご紹介したAlexa for Businessで公開されているZoom for Alexaと連携させる方法とは別のものです。
開発したスキルは、以下の動作例のように日本語で繋げたい会議室を指定すると、接続や切断をすることが可能です。
これにより、ミーティングツールの使い方をよく分かっていないために会議の開始が遅くなるといった時間の無駄を削減することができます。

動作例

・「アレクサ、ズームルームで''ホール''に繋いで!」
 → "発言者の会議室" と "指定した会議室(ホール)" のZoom Roomsを接続

・「アレクサ、ズームルームで''ミーティング''を終了して!」
 →"発言者の会議室" のZoom Roomsを終了

実際の動作結果

実際の動作は先にみたほうが分かりやすいため動画でご紹介します。
youtu.be

Zoomについては紹介記事を投稿しているので、ご興味があればご覧ください。
tech.wonderpla.net

またAlexa for Businessについても、ご興味があれば合わせてご覧ください。 tech.wonderpla.net


概要&必要なもの

AWS LambdaでZoom Rooms APIを呼び出してZoom Roomsを操作します。
AlexaとZoom Roomsを活用するため、開発には以下のものを用意しておく必要があります。

Alexaスキル開発用

・Alexa 端末
・Amazon Developerアカウント
・Amazon ショッピングアカウント
(混乱を避けるため全て.co.jpリージョンであることが望ましい)
・AWS アカウント

Zoom Rooms用

・Zoom Roomsアカウント x 2
・Zoom Rooms用PC x 2
・Zoom Rooms用タブレット x 2
(接続先と接続元用に各2つずつ用意)

またPCにインストールするZoom Roomsクライアントは非公開のプレリリース版のビルドである必要があります。
Webで公開されている通常のZoom RoomsクライアントではZoom Rooms APIが使えないため、接続ができません。
こちらは非公開ではありますが、Zoomにメールするとダウンロード用のアドレスを共有してもらえました。
Zoom Rooms API - Zoom Developers

Note: These APIs need a pre-launch version of our new Zoom Room build – if you would like to try it, please send an e-mail to “developersupport@zoom.us” and we will provide you the link to download

こうして必要なものを並べると、導入のために必要なものが、かなりあるのが分かりますね。
以上の必要なものが揃ったらZoom RoomsをPCとタブレット両方にインストールしておいてください。
PCにインストールするZoom Roomsはメールで頂いたプレリリース版です。
タブレットにインストールするZoom Roomsはストアに公開されている通常のもので大丈夫です。


AlexaでZoom Roomsを操作するスキルの開発

Amazon開発者コンソールの設定では接続したい部屋名を取得するIntentを設定します。
とくに難しい設定はしなくても出来るので、詳しい解説は前回の記事をご参照ください。
tech.wonderpla.net

今回は以下のように部屋に接続するためにConnectRoomIntentを設定しました。 Alexa インテント

また{room}のスロットに部屋名として認識させたい単語を登録しておきました。
(ワンダープラネットでは会議室に名古屋名物の名前をつけているため、スロットに食べ物の名前が並んでいます笑)
Alexa スロット
この他にもミーティング開始時や終了時のインテントを追加しておきました。
ひとまず動作を見るだけであれば、先ほど設定した部屋名を取得するインテントだけで大丈夫です。


Zoom RoomsのAPI Keyの取得

次はZoom RoomsのAPIを使うために必要なKeyを取得します。
Zoom Developers – Power Up Your Apps with Video, Voice, and Screen Sharing
サインインを行ったら、画面右上のDevelopper Accountを開き、API Keyを発行しましょう。
Zoom Developer Account
Zoom Roomsのアクセストークン

Zoom Rooms APIの呼び出し

ここからは実際にソースコードを書いていきます。言語はNode.js6.10です。
JWT認証でZoom Rooms APIのAccess Tokenを発行してZoom Roomsへ命令を送ります。
ひとまずはLambdaに書く前にローカル環境で動作を確認してみます。

JWT認証

取得したAPI KeyとAPI Secretを使ってJWTでAccess Tokenを発行します。
JWTはnpmでインストールしておきましょう。

$ npm install jsonwebtoken

Access Tokenは以下のような記述で発行できます。

const jwt = require('jsonwebtoken');
const ZOOM_API_KEY = "取得したAPI Key";
const ZOOM_API_SEC = "取得したAPI Secret";

var payload = {
  iss: ZOOM_API_KEY,
  exp: ((new Date()).getTime() + 5000)
};
var token = jwt.sign(payload, ZOOM_API_SEC);

Zoom Roomsを起動

あらかじめnpmでsync-requestをインストールしておきます。

$ npm install sync-request

さきほどのコードで生成したtokenを用いてAPIを呼び出します。
以下のコードを叩くと設定したルームIDの会議室のインスタントミーティング(設定したルームIDの会議室をホストとして起動)が開始できます。
もし設定するルームIDが分からない場合は以下を参考にしてください。
Zoom RoomsのミーティングIDとルームIDを調べる

const syncrequest = require('sync-request');
const HOST_ZR_ID = "起動するZoom RoomsのルームID";
var option = {
   qs: {access_token: token},
   json: {method: "join",
          params: {force_accept: true}}
};
var zr_join = "https://api.zoom.us/v2/rooms/" + HOST_ZR_ID + "/meetings";
var syncres = syncrequest('POST', zr_join, option);
var syncBody = JSON.parse(syncres.getBody('utf8'));
console.log(syncBody);
if(syncBody.result){
    console.log("起動成功");
}else{
    console.log("起動失敗");
}

methodjoinにすることでZoom Roomsを起動します。ただし接続先を指定していないため、設定したルームIDの会議室をホストとして開始するだけで、他の会議室には繋がりません。

また、paramsにはforce_acceptを設定しています。これが無いとAPI実行時にerror: { code: 4009, message: 'The room client is busy.' }と出力され起動できないことがあります。設定しておくとbusy状態でも接続を強制してくれるようになります。他と接続していないときでも稀にbusyが返ってきて悩まされることがあるので、ひとまずは設定しておくとよいでしょう。

Zoom Roomsを他の会議室と繋ぐ

他の会議室からホストとなっている会議室に接続したい場合は、
以下のようにoptionのparamsに接続先のミーティングIDを指定すると接続できます。
このとき、ホスト側はすでに起動させておいてインスタントミーティングの状態にしておく必要があります。

const syncrequest = require('sync-request');
const HOST_MEETING_ID = "ホスト(起動済み)のZoom RoomsのミーティングID";
const GUEST_ZR_ID = "新たに起動する接続したいZoom RoomsのルームID";
var option = {
   qs: {access_token: token},
   json: {method: "join",
          params: {meeting_number: HOST_MEETING_ID,
                   force_accept: true}}
};
var zr_join = "https://api.zoom.us/v2/rooms/" + GUEST_ZR_ID + "/meetings";
var syncres = syncrequest('POST', zr_join, option);
var syncBody = JSON.parse(syncres.getBody('utf8'));
console.log(syncBody);
if(syncBody.result){
    console.log("接続成功");
}else{
    console.log("接続失敗");
}

Zoom RoomsのミーティングIDとルームIDを調べる

設定するZoom RoomsのミーティングIDとルームIDが分からない場合、以下の方法で確認できます。

ミーティングIDの確認

以下のリンクより管理画面へログインして各部屋のミーティングIDを見ることができます。
https://zoom.us/signin

ログインしたら左サイドバーの「Zoom Rooms」から対象のルームの編集をクリックします。
Zoom Rooms 管理ページ

少しスクロールするとhttps://zoom.us/j/〇〇〇〇という形式でミーティングIDを確認できます。
またこれ以外にもタブレットのZoom RoomsアプリからSettingsを開いても確認することができます。
Zoom Rooms ミーティングID

ルームIDの確認

ルームIDは設定画面での記載箇所が見つからなかったため、Zoom Rooms APIのListを使って確認しました。
以下のコードでAPI KeyとAPI Secretを設定して実行すれば、アカウントに紐づくZoom RoomsのルームIDと名前の一覧が取得できます。

const jwt = require('jsonwebtoken');
const syncrequest = require('sync-request');
const ZOOM_API_KEY = "取得したAPI Key";
const ZOOM_API_SEC = "取得したAPI Secret";

var payload = {
  iss: ZOOM_API_KEY,
  exp: ((new Date()).getTime() + 5000)
};
var token = jwt.sign(payload, ZOOM_API_SEC);

var option = {
   qs: {access_token: token},
   json: {method: "list"}
};

var zr_list = "https://api.zoom.us/v2/rooms/zrlist";
var syncres = syncrequest('POST', zr_list, option);
console.log(syncres.getBody('utf8'));

Alexaを使ってAWS Lambdaから実行

ローカル環境で動作することが確認できたら、ここからはAWS Lambdaにソースを移して声で実行してみます。
簡単にAlexa用に書くと以下のようになります。AWS Lambdaに登録が完了したらARNをAmazon開発者コンソールにコピーしてください。

"use strict";
const jwt = require('jsonwebtoken');
const syncrequest = require('sync-request');
const Alexa = require("alexa-sdk");
const SKILL_NAME = "ZOOM"
const APP_ID = "amzn1.ask.skill.xxxxx";
const ZOOM_API_KEY = "取得したAPI Key";
const ZOOM_API_SEC = "取得したAPI Secret";
const HOST_ZR_INFO = { zr_name: '起動するZoom Roomsの会議室名', zr_id: '起動するZoom RoomsのルームID', meeting_id: '起動するZoom Roomsのミーティング'ID };
const GUEST_ZR_INFO = { zr_name: '接続したいZoom Roomsの会議室名', zr_id: '接続したいZoom RoomsのルームID', meeting_id: '接続したいZoom RoomsのミーティングID' };

//ステートの定義
const states = {
  CONNECT: '_CONNECTMODE'
};

var languageString = {
    "ja-JP": {
        "translation": {
            "WELCOME_MESSAGE": "ズームルームへようこそ。",
            "HELP_MESSAGE": "ほかの部屋と接続したい場合は、ルーム名を教えてください。",
            "UNHANDLED_MESSAGE": "すみません、よく聞きとれませんでした。",
            "CONNECT_MESSAGE": "%sに接続します。",
            "ERROR_CONNECT_MESSAGE": "接続に失敗しました。",
            "EXIT_MESSAGE": "さようなら。"
        }
    }
};

exports.handler = function(event, context, callback) {
  var alexa = Alexa.handler(event, context);
  alexa.resources = languageString;
  alexa.dynamoDBTableName = 'ZoomRooms';
  alexa.registerHandlers(handlers,connectHandlers);
  alexa.execute();
};

// 初期ハンドラ
var handlers = {
  'LaunchRequest': function () {
    this.emit(':ask', this.t("WELCOME_MESSAGE") + this.t("HELP_MESSAGE"));
  },
  'AMAZON.HelpIntent': function () {
    this.emit(':ask', this.t("HELP_MESSAGE"));
  },
  'AMAZON.CancelIntent': function() {
    this.emit(':tell', this.t("EXIT_MESSAGE"));
  },
  'AMAZON.StopIntent': function() {
    this.emit(':tell', this.t("EXIT_MESSAGE"));
  },
  'ConnectRoomIntent': function() {
    this.handler.state = states.CONNECT;
    this.emitWithState("ConnectRoomIntent");
  },
  'Unhandled': function () {
    this.emit(':ask', this.t("UNHANDLED_MESSAGE"));
  }
};

// 接続ステート
var connectHandlers = Alexa.CreateStateHandler(states.CONNECT, {
  'ConnectRoomIntent': function() {
    var response = "";
    var asyncToken = getToken(ZOOM_API_KEY, ZOOM_API_SEC);
    //ホストを起動
    var isOpenHost = connectionRoom(asyncToken, HOST_ZR_INFO);
    if(isOpenHost){
      //ホストが正常に起動出来ていたら、依頼された部屋と接続
      if(connectionRoom(asyncToken, GUEST_ZR_INFO, HOST_ZR_INFO)){
        response = this.t("CONNECT_MESSAGE", GUEST_ZR_INFO.zr_name);
      }
    }
    if(!response){
      // 失敗時はエラーメッセージを流す
      response = this.t("ERROR_CONNECT_MESSAGE");
    }
    this.emit(':tell', response);
  },
  'Unhandled': function() {
    this.emit(':ask', this.t("UNHANDLED_MESSAGE"));
  }
});

// 部屋を起動(AcessToken, 起動する部屋, 接続先の部屋)
function connectionRoom(token, startRoom, connectRoom){
  var option = {
     qs: {access_token: token},
     json: {method: "join",
            params: {force_accept: true}}
  };
  if(connectRoom){
    // 接続先がある場合、optionに接続先のミーティングIDを追加
    option.json.params.meeting_number = connectRoom.meeting_id;
  }
  var zr_join = "https://api.zoom.us/v2/rooms/" + startRoom.zr_id + "/meetings";
  var syncres = syncrequest('POST', zr_join, option);
  var syncBody = JSON.parse(syncres.getBody('utf8'));
  console.log(syncBody);
  if(syncBody.result){
      console.log("接続成功");
      return true;
  }else{
      console.log("接続失敗");
      return false;
  }
};

// ZoomRooms接続用トークンを発行
function getToken(api_key, api_sec){
  var payload = {
    iss: api_key,
    exp: ((new Date()).getTime() + 15000)
  };
  return jwt.sign(payload, api_sec);
}

上記のコードでは、Zoom Roomsの接続先がGUEST_ZR_INFOに固定となっているため、
実際に運用する場合はConnectRoomIntentで取得した部屋名からIDを割り出すコードは必要になるかと思います。
またendのmethodなどを駆使すればAlexaでZoom Roomsを終了したりすることも可能です。


Zoom Rooms API

上記のコードで扱ったZoom Roomsの起動、接続以外にも以下のようなことが可能です。
凝ったことはできませんが、基本的な機能は一通り使えるようです。

Zoom Rooms API
Restart POST https://api.zoom.us/v2/rooms/{roomId}/zrclient
"method": "restart"
ズームルームクライアントを再起動する。
List POST https://api.zoom.us/v2/rooms/zrlist
"method": "list"
登録されているズームルームのIDと名前の取得する。
Leave POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "leave"
進行中の会議をそのままにして会議から退出。指定の部屋がホストだった場合は会議を終了する。
Join POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "join"
指定の会議が開かれている場合はそこに参加。 指定の会議が開かれていない場合はホストとなりインスタントミーティングを開始する。
Invite POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "invite"
ズームルームへ連絡先を招待する。
Schedule POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "schedule"
会議をスケジュールする。
Cancel POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "cancel"
スケジュールされた会議をキャンセルする。
End POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "end"
指定した部屋がホストだった場合は会議を終了する。指定した部屋が他の部屋にJOINしていた場合は、会議をLeaveする。

最後に

今回はZoom RoomsのAPIを使ってAlexa用のスマート会議室スキルを開発しました。
現在は一部の会議室で検証中ですが、会議前のモタつきが軽減されるので大変便利です。
色々と必要なものが多いためハードルが高めではありますが、
もし既にZoom Roomを使っていたりスマート会議室に興味があるのであれば、チャレンジしてみてはいかがでしょうか。

参考

Zoom Rooms API - Zoom Developers
Zoom for Alexa - GitHub

Alexa for Business でスキルを有効化する方法。Zoom for Alexaを試してみた。

こんにちはアドバンストテクノロジー部の@y-matsushitaです。
今回は先日発表されたAlexa for Businessのスキルを試しに有効化するまで触ってみたので、ご紹介します。
有効化してみたスキルはオンライン会議サービスであるZoomのAlexa for Business用スキルです。
こちらのスキルを使って声で通話を始めるスマート会議の構築にチャレンジしてみます。
Zoom for Alexa以外のスキルを有効化する場合にも参考になるかと思います。

Zoomについては紹介記事を投稿しているので、ご興味があればご覧ください。 tech.wonderpla.net

Alexa for Businessとは

Alexaを搭載するスマートスピーカーを一元管理したり、自社専用の「プライベートスキル」を開発できます。
Alexa for Business用に公開されているスキルもあり、社内で活用できる様々なスキルが利用できます。

aws.amazon.com

注意点

現時点(2018/1/15)でAlexa for Businessは日本語未対応です。
端末をAlexa for Businessに登録すると端末が英語版になってしまうため、日本語での操作が一切できなくなります。
元に戻すことは可能ですが、端末を再セットアップする必要があります。

必要なもの

・Amazon Echo(Alexa対応端末)
・AWSアカウント
・Windows機 *1
 Microsoft Windows 7 or later, the AWS Tools for Windows PowerShell, and the .NET Framework 3.5 or later.)
・Zoom Roomsアカウント*2

手順

手順としては以下のように進めていきます。

端末登録
Alexa for Businessでの設定
 ・ Room profileの作成
 ・ Skill groupの作成
 ・ Roomの作成
 ・ Skillの追加と「Zoom for Alexa」の有効化
 ・ Zoom for AlexaをSkill groupに登録
 ・ Zoom for AlexaとZoomアカウントの連携
 ・ Provider設定にZoomを追加
設定内容の確認
Zoom for Alexaを使う


端末登録

それでは早速はじめていきます。
まずはAlexa for BusinessにEchoを端末登録します。
AWSのAlexa for Businessを開き、Shared devicesから「Set up devices」をクリックします。
Alexa for Businessを利用するにはリージョンをバージニア北部に設定しなければいけない点に注意してください。 Alexa for Business Shared devices

セットアップ用のexeのダウンロードリンクが出るのでクリックしてダウンロードします。
f:id:y-matsushita:20171222122541j:plain:w500

ダウンロードしたexeを実行します。当然ですが実行はWindows環境で行います。
f:id:y-matsushita:20171222122743j

もし以下のような画面が出てたらインターネット回線が繋がっているか確認しましょう。
An Error occurred attempting to install DeviceSetupTool.

Access key IDとSecret access keyを入力します。
入力するKeyはAWSのIAMロールで発行します。
Alexa for Businessへのアクセス権限とプログラムによるアクセス許可をしておきましょう。 Setup AWS security credentials

認証が成功したらEchoとセットアップ用のPCをWi-Fiで接続します。
Select Alexa-enabled devices

この状態でEchoをセットアップモードにすると、EchoとDevice Setup Toolを接続させることができます。
セットアップモードにするには下図のボタンをオレンジ色が点灯するまで長押しします。
Echoのセットアップ

上手く検出できるとDevice Setup ToolにAmazon-◯◯◯という名前でデバイスが表示されます。 Echoのセットアップ

Echoで使用するWi-Fiの接続情報を入力します。 f:id:y-matsushita:20171222143318p

設定内容に問題がなければStatusがSuccessに変わります。
ここまででEchoとAlexa for Businessの連携は完了です。
f:id:y-matsushita:20171222145212j:plain:w700


Alexa for Businessでの設定

ここからはAWS上での操作になります。
Alexa for Business側を開いてEcho Deviceが新たに追加されていることを確認しましょう。
f:id:y-matsushita:20171222145738p:plain:w700

ここからはDeviceにSkillを追加する手順を記載します。
DeviceはRoomごとに異なるSkillを設定可能です。
必要な作業は以下の通りです。設定内容は後から変更可能です。

・Room profileとSkill groupを作成。
・Skillを有効化してSkill groupに追加。
・Roomを作成してRoom profileとSkill groupとDeviceを設定。
・Providerの追加。(Zoom for Alexa用)

Room profileの作成

まずはRoom profileとSkill groupを作成しましょう。
画面左のConfigurationからRoom profilesを開き、「Create room profile」をクリックします。
f:id:y-matsushita:20171222152213j:plain:w700

「Create room profile」で設定内容を記入します。
f:id:y-matsushita:20171222152346j:plain:w700
・Profile name : Room profile名
・Address : 所在地
・Time zone : タイムゾーン
・Wake word : ウェイクアップワード(Alexa, Amazon, Echo, Computerから選択)
・Temperature units : 摂氏・華氏の選択
・Distance units : フィート・メートルの選択
・Maximum volume : デバイス音量の最大値(6~10)
・Device setup mode : セットアップモードの有効化。Offの場合、セットアップモードにならなくなる
・Outbound calling : 電話の発信の有効化

Skill groupの作成

次はスキルを管理するためのSkill groupを作成します。
あとで「Zoom for Alexa」のスキルを有効化してグループ化させるためのものです。
「Create skill group」をクリックしましょう。
Alexa for Business Skill groupの作成

Skill groupの名前を入力するだけで、ひとまずの作成は完了です。
f:id:y-matsushita:20171222155208j:plain:w500

Roomの作成

画面左上のRoomsから「Create room」をクリックしてRoomを作成します。
Alexa for Business Roomの作成

Room名と先ほど作成したRoom profileを設定します。
Alexa for Business Room名の設定

Roomに先ほど端末登録したEchoを追加します。
端末登録したEchoを設定

Roomに先ほど作成したSkill groupを設定します。 Skill groupを設定

問題なければ作成完了です。
Roomの作成完了


Skillの追加と「Zoom for Alexa」の有効化

画面左のSkillsをクリックします。
Alexa for Business スキルの設定

Alexa skills storeタブを開いて「zoom」で検索しましょう。
Zoom for Alexa がヒットしたら Enable をクリックします。
Zoom for Alexaを有効化

Zoom RoomsアカウントでログインしてZoom for Alexa を有効化します。
Zoomアカウントと連携

問題なく有効化できればEnable skillsにZoom for Alexa が現れます。
Zoom for Alexaが追加される

Zoom for AlexaをSkill groupに登録

Skill groupsから先ほど作成したSkill groupを選択します。
Zoom for AlexaをSkill groupに登録

Add skillsからSkill groupにZoom for Alexaを追加します。
Zoom for Alexaを追加

追加されると以下のようにSkill groupに追加されます。
Zoom for Alexaを追加完了

Zoom for AlexaとZoomアカウントの連携

SkillsのZoom for AlexaをクリックしてZoom for Alexaの詳細設定を確認します。
「Link account to this room」が未設定になっている場合は、設定を行います。
Zoom for Alexaとアカウント連携

Zoom Roomsアカウントでログインして連携します。
Zoom for Alexaとアカウント連携

上手くいけばボタンの表示が「Link master account」になります。
ここでの連携は結構失敗しやすいですが、失敗したときの履歴が残っていると連続で失敗してしまうため、
上手くいかない場合は以下のようなことを試すと上手くいくかもしれません。

・一旦ブラウザでZoomのサイトへ行ってログアウトしてから管理者のZoom roomsアカウントで試す。
・ブラウザのシークレットウィンドウで試す。
・Configurationに適当なテキストを設定する。

Zoom for Alexa設定完了

Provider設定にZoomを追加

画面左のConferencingを開き、「Add provider」をクリックします。
ProviderにZoomを追加

Providerの設定情報を入力します。特に設定を変更しなくても動作しますが、
ミーティングスタート時にMeeting PINの要求が必要であれば、OptionalかRequiredに変更します。
ZoomのProvider設定

追加が完了すると以下のようにProviderとしてZoomが表示されます。
ZoomProvider追加完了


設定内容の確認

大変長くなりましたが、以上で設定は完了です。
確認として以下の点が問題なければ設定は大丈夫です。

・RoomにDeviceが設定されているか。
・RoomにRoom profileとZoom for Alexa スキルの入ったSkill groupが設定されているか。
・Zoom for AlexaスキルとZoom Roomsアカウントが連携できているか。
・ProviderにZoomが追加されているか。

あとは最後に重要な点ですが、Alexa for Businessのスキルを有効化するために、Echoを再起動しましょう。
どうやらスキルの追加をした後に再起動をしないと、有効化されないようなので注意が必要です。
(私はここに気付かず、何時間も設定を見直すハメになりました。)

スキルが動かない場合は適当に別のスキルを試して動くか確認してみると良いでしょう。
Alexa for Businessには「Crazy Jokes」のような単純にジョークを言うだけのスキルもあるので、
それらを有効化して動くかどうかを見るだけでも見直す箇所が絞り込めるかと思います。


Zoom for Alexaを使う

それでは、Zoom for Alexaを使ってみましょう。
Echoに「アレクサ、スタート、ミーティング!」と話しかけると、「ミーティングIDは何ですか?」と英語で返してくれます。
ここで返すミーティングIDはZoom Roomsだけでなく個人アカウントのミーティングIDでもOKです。

今回は試しに個人アカウントのzoom.usに繋げてみます。
zoom.usを開き、以下の画面に書かれている番号をAlexaに向かって英語で読み上げてください。
ZoomのミーティングID

しばらく待っていると、zoom.usのほうがホスト状態になっていれば自然に繋がります。
ホスト状態じゃなかった場合は、Alexaから軽快な音楽が流れ始めて待機状態になるようです。
待機状態のときにzoom.us側でインスタントミーティングを始めるとAlexaとの接続が開始されます。
ただし待機状態になっていたときにzoom.us側には通知などがされなかったので、
事前に繋げることを伝えておくか、常にホスト状態にしておく等の運用をする必要がありそうです。 Zoom for Alexaを繋げた結果

実際に動かしてみた様子です。 Amazon EchoとPCのZoomのクライアントで通話ができました!
Echoに向かって話しかけると、ちゃんとPCから音声が返ってきます。 (映像は繋がってません。)
Zoom for Alexa実行中

まとめ

今回はAlexa for BusinessのスキルとしてZoom for Alexaを試してみました。
ハンズフリーで相手と通話を可能にするのは未来感がありますね。
ただし相手のミーティングIDを読み上げなければいけなかったり、言語が英語だったりとまだまだ使い辛さを感じます。

個人的には「アレクサ、ズームルームでホールに接続して!」と日本語で言うだけで映像と音声を繋げたいですね。
Zoom RoomsにはAPIがあるので、次はそれを使って上記のような会議室同士をつなぐAlexaアプリを開発してみます。
上手くいったら、またこちらのブログで皆様にご紹介いたします!

参考

Alexa for Business Administration Guide
Zoom with Alexa for Business: Meet Without Lifting a Finger

*1:EchoをAlexa for Businessに端末登録するためにexeの実行環境が必要です。

*2:Zoom for Alexaを有効化するために必要。他のスキルであれば不要です。