Briswell Tech Blog

ブリスウェルのテックブログです

Youtubeライブ配信の埋め込みリンクを設置する話

はじめに

凄まじい寒波によりキッチンの水道管が凍りました。水が出ない半日を過ごしたid:rosoneです。 (更に近所では水道管が破裂していました🙄)

さて、Webシステムの開発をしている中で、ライブ配信機能を追加してみるのはどうか?というアイディアが出ました。 少し掘り下げで考えてみます。

  • ライブ配信機能をイチから実装していくと、開発コストだけでなくインフラ面でのコストも高くつく
  • 何かしらの方法でライブ配信を行い、自前のWebシステム上でユーザーが配信を見れる形であれば良さそう
    • ライブ配信のプラットフォームを立ち上げたいわけではない

上記により、例えばYoutube等の既存のプラットフォームを使って配信をし、自前のWebページに配信動画のプレイヤーを配置してあげれば、低コストでスモールスタートすることができそうです。

以上の背景から、オープン/クローズドは問わず1:nの形で配信できるプラットフォームの中から実現できそうなサービスを調べたところ、YoutubeとZoomが候補に上がりました。

Youtubeは非常に簡単にお試しすることができたので、今回はYoutubeライブ配信のページ埋め込みについてまとめます。

Zoomの方は公式のSDKが公開されており実現はできそうなのですが、ある程度しっかりとした実装が必要になりそうなため、お試しがうまく行ったら別の機会でブログにまとめたいと思います。

Youtubeライブから埋め込み用リンクを取得

ゆったりとしたjazzのライブ配信をお借りします。

埋め込みリンクは以下の手順で取得できます。

以下の埋め込みリンクを取得できました。

 <iframe width="560" height="315" src="https://www.youtube.com/embed/YSAodnpu1OA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

HTMLの準備

Webページのテンプレートを以下のサイト様からお借りします。

テンプレートのHTMLをブラウザで開くと、おしゃれなページが表示されました。

この写真部分をYoutubeライブに差し替えていきます。

埋め込みリンクを設置

今回はChromeの検証機能を使用し、ソース上で差し替え対象の写真を指定している箇所を特定します。

エディタでHTMLを開き、該当箇所に埋め込みリンクを差し替えていきます。

コード差し替え後に画面を表示すると、Youtubeライブ配信のプレイヤーが表示されました。

レイアウトが崩れているので、表示位置とサイズを調整します。

いい感じに配置できました。

コメントやいいねなどの付属機能は付いていないですが、シークバーの追っかけ再生、再生速度や画質変更などの基本的な再生機能は本家Youtubeと同等で利用できるようです。

最後に

レイアウトなど気にしなければ、モノの数分で自前のページにYoutubeライブのプレイヤーを設置することができました。

今回はソースを直接編集していますが、実際の運用では同じように毎回ソースを修正してデプロイしてというのは現実的ではないですね。

自前のWebシステム上で配信動画のURLを入力して更新するような仕組みだけ作ってあげれば、「はじめに」で記載したアイディアを実現できそうです。

  1. Youtubeライブ配信を開始
  2. Youtube上から埋め込みリンクを取得
  3. 自前のWebシステム上でURLを入力

ちなみに、はてなブログに埋め込みリンクをそのまま設置すると、下記のようになりました。

寒い日が続きますが、水道管の凍結対策も忘れずにお過ごし下さい。

バタフライ・エフェクト

どんな小さな出来事でも連鎖反応で未来に大きな影響を及ぼす「バタフライ・エフェクト」。ゲームや映画で良く使われる演出ですね。

今回は、以下のローレンツ方程式により、バタフライ・エフェクトの現象を確かめてみます。

x, y, zの変数とp, r, bの定数の常微分方程式です。定数値は、p=10, r=28, b=8/3 をローレンツさんは用いています。

1. ローレンツ方程式を定義

def lorenz(x, y, z, p=10, r=28, b=8/3):
    dot_x = -p * x + p * y
    dot_y = -x * z + r * x - y
    dot_z = x * y - b * z
    return dot_x, dot_y, dot_z

2. 時間間隔とステップ数を設定

dt = 0.01
num_steps = 15000

3. 空の配列を作成

xs = np.empty(num_steps + 1)
ys = np.empty(num_steps + 1)
zs = np.empty(num_steps + 1)

4. 初期値を設定

初期値①

xs[0], ys[0], zs[0] = (1., 1., 1.)

初期値②(0.001だけ増やす)

xs[0], ys[0], zs[0] = (1., 1., 1.001)

5. 座標生成

for i in range (n_steps):
    dot_x, dot_y, dot_z = lorenz(xs[i], ys[i], zs[i] )
    xs[i + 1] = xs[i] +  (dot_x * dt)
    ys[i + 1] = ys[i] +  (dot_y * dt)
    zs[i + 1] = zs[i] +  (dot_z * dt)

6. 描画

初期値の違いがどのように結果に影響してくるのか。グラフ描画(ローレンツ・アトラクタ)により確認してみます。

初期値①(1.000, 1.000, 1.000)

初期値②(1.000, 1.000, 1.001)

ほんの僅かな初期値の違いにより、周回する軌道が異なってくることが確認できました。まさにカオスです。

予測には精度の高い初期値(観測値)が大事なのですね。

芝大門から眺める愛宕山

Pythonで3D地形図化 - Briswell Tech Blog

年の瀬も間近になってまいりました。ラストスパートですね!

弊社のオフィスがある芝大門の近くにある愛宕山
天然の山としては東京23区内最高峰(25.7m)で、かつては江戸の見晴らしの名所と言われていました。

愛宕山 | 錦絵でたのしむ江戸の名所

こちら(出典:国立国会図書館)のように愛宕山は錦絵にも描かれています。
現在は、周辺が高層ビルに囲まれているため、かつてのように見晴らすことはできなくなりましたが、急な石段(出世の石段)も現存し、存在感を残しています。

ふと感じたのが...
愛宕山から見る江戸(愛宕山→江戸)は、上記のような錦絵や江戸後期の写真にも残っているので、どのような眺めであったのか確認できるのですが、
自分が今いる芝大門から見る愛宕山(江戸→愛宕山)はどのような眺めだったのか。町屋や大名屋敷も数メートルほどの高さだったので、愛宕山を眺めることができたはず。

現在の3D地形図をもとにそれを確認してみようと思います。

地理院地図 / GSI Maps|国土地理院

こちらの地理院地図の3D表示機能を使い

  • ズームレベル:13
  • タイルのX座標:7276
  • タイルのY座標:3226

芝大門愛宕山が含まれる上記のタイルのobjファイル(3Dモデルフォーマット)をダウンロードします。

そのobjファイルをパワポで手軽に3Dビューしてみます。

当時はこのような眺めだったのですね。感動です。

良いお年をお迎えください。

GAS & Slackで地震発生時の安否確認Botを作ってみた

弊社のようにリモートワークが多い会社では、地震発生時に安否確認を「迅速かつ正確に」行う必要があります。
ただ、地震の発生はいつどこで起きるかわからないため、担当者の割り振りが難しいのが現状です。
今回はGASとSlackを組み合わせて、地震発生時にSlackに安否確認の投稿をする仕組みを実装(自動化)してみました。
 

投稿のイメージはこんな感じです。
   

地震の情報源は気象庁の以下電文を利用します。
気象庁防災情報XMLフォーマット形式電文の公開(PULL型)
今回は10分に一度、情報を取得し地震情報のチェックを行うため、「高頻度フィード(地震火山)」のxmlを使用します。

「高頻度フィード(地震火山)」には直近に発生した地震火山関連情報を一覧で取得できます。
今回は、この中から震度3以上に適用される「震度速報」を安否確認のトリガーとします。
震度速報の[entry]タグには、詳細情報が記載されたURLが[link]タグ内にセットされています。
 

[link]タグ内のURLにアクセスすると、このような詳細情報を確認することでができます。
「震度」や「震源」といった情報を取得できますね。
 

そのため、手順としては以下の流れとなりそうです。

  • 「高頻度フィード(地震火山)」を取得

  • 「震度速報」の[entry]タグ内リンクを順に確認していく

  • 詳細情報リンクにて、震度5以上の情報があれば、Slackにメッセージを投稿  
     

では、実際に実装していきます。 
 

GASでスクリプトの作成

Google Apps Script
上記リンクより、GASの新規プロジェクトを作成します。
プロジェクトの作成は「新しいプロジェクト」より行えます。

コードの作成

プロジェクトの作成ができたらスクリプトを書いていきます。
※[token]と[channel]は後ほど取得するので仮置きしておきます。

/**
* 気象庁の地震情報(高頻度フィード)を取得しSlackに安否確認を飛ばす
* http://xml.kishou.go.jp/xmlpull.html
*/
function getJisinkun() {
  var now = new Date();
  var Before_1_Minute = new Date();
  Before_1_Minute.setMinutes(now.getMinutes() - 11); //11分前の時刻を取得(10分ごとに実行するため)
  var preDate = Utilities.formatDate(Before_1_Minute,"JST","yyyy-MM-dd HH:mm");
  console.log(preDate);

  var feedUrl   = 'https://www.data.jma.go.jp/developer/xml/feed/eqvol_l.xml';
  var feedHtml  = UrlFetchApp.fetch(feedUrl).getContentText();
  var feedDoc   = XmlService.parse(feedHtml);
  var feedXml   = feedDoc.getRootElement();
  var feedentry = getElementsByTagName(feedXml, 'entry'); //xmlのentry要素を配列で取得
  var atom      = XmlService.getNamespace('http://www.w3.org/2005/Atom');

  //初期値設定
  var slackFlg  = false; //slack送信フラグ
  var quakedt   = ""; //地震発生時刻

  //気象庁のxmlデータ(feed)より、entry要素を繰り返し探索
  var quakeInfo = ""; //地震情報
  feedentry.forEach(function(value, i) {
    var titleText = value.getChild('title', atom).getText(); //title
    var linkText  = value.getChild('link', atom).getAttribute('href').getValue(); //link
    
    //titleが震度速報の場合(震度3以上が震度速報に該当)
    if('震度速報' == titleText){
      //気象庁のxmlデータ(data)の情報を取得
      var dataUrl   = linkText;
      var dataHtml  = UrlFetchApp.fetch(dataUrl).getContentText();
      var dataDoc   = XmlService.parse(dataHtml);
      var dataXml   = dataDoc.getRootElement();
      var titleText = value.getChild('title', atom).getText();  //title

      var dataItem            = getElementsByTagName(dataXml, 'Pref'); // xmlに含まれるPref要素を配列で取得する
      var dataMaxInt          = getElementsByTagName(dataXml, 'Kind')[0].getValue(); // 震度
      var dataArea            = getElementsByTagName(dataXml, 'Area')[0].getValue(); // 地域
      var dataReportDateTime  = getElementsByTagName(dataXml, 'TargetDateTime')[0].getValue(); // 地震発生時刻
      dataReportDateTime      = dataReportDateTime.replace('T', ' ');
      dataReportDateTime      = dataReportDateTime.replace('+09:00', '');

      //気象庁のxmlデータ(data)より、Pref要素を繰り返し探索
      dataItem.forEach(function(value, i) {
        //Pref要素の文字列を取得(震度と地域)
        var strPref = dataArea;
        // 都道府県指定(東京近辺を対象)
        if(dataArea.match(/福島県|東京都|神奈川県|埼玉県|千葉県|茨城県|静岡県|栃木県|群馬県|愛知県|長野県/)){
          //「地震発生時刻 > 実行時刻の5分前」かつ「震度速報相当」の場合
          if(dataReportDateTime > preDate && dataMaxInt.match(/[震度5-|震度5+|震度6-|震度6+|震度7]/)){
              slackFlg = true;  //送信フラグをtrue
              quakedt     = dataReportDateTime;  //地震発生時刻
              quakeArea   = dataArea;  //地震発生地域
              quakeMaxInt = dataMaxInt;  //震度
              Logger.log("地震発生時刻:" + dataReportDateTime);
              Logger.log("地震発生地域:" + dataArea);  //地震発生地域
              Logger.log(dataMaxInt);  //震度
          }
        }
      });
    }
  });
  
  //送信フラグがtrueの場合
  if(slackFlg == true){
    var url = "https://slack.com/api/chat.postMessage";
    var payload = {
    "token" : "xoxb-XXXXXXXXXXXXX-XXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXX",
    "channel" : "XXXXXXXXXX",
    "text" : "<!channel>\n【安否確認】\n震度5以上検知しました。\n安否確認のため、各自リアクションを行ってください。\n\n【地震情報】\n地震発生時刻:" + quakedt +"\n地震発生地域:"+ quakeArea +"\n"+ quakeMaxInt,
    };
    var params = {
      "method" : "post",
      "payload" : payload
    };
    // Slackに投稿する
    UrlFetchApp.fetch(url, params); 
    Logger.log("送信完了");
  }else{
    Logger.log("送信なし");
  }
}

/**
* @param {string} element 検索要素
* @param {string} tagName タグ
* @return {string} data 要素
*/
function getElementsByTagName(element, tagName) {
  var data = [], descendants = element.getDescendants();
  for(var i in descendants) {
    var elem = descendants[i].asElement();
    if ( elem != null && elem.getName() == tagName) data.push(elem);
  }
  return data;
}

スクリプトの記載は以上です。

このように丸々コピペでOKです。

スクリプトについては下記サイトを参考にさせていただきました。
参考文献: 気象庁のサイトから地震情報を取得してメール通知する

 

ライブラリ(SlackAPI)の追加

Slackへの投稿を行うために、SlackAPIライブラリを追加します。
ライブラリの追加はサイドメニューのライブラリ[ + ]より追加できます。
追加に際して、スクリプトIDが必要になるのですが、SlackAPIの場合は下記となります。

SlackAPIスクリプトID:
  1on93YOYfSmV92R5q59NpKmsyWIQD8qnoLYk-gkQBI92C58SPyA2x1-bq

ライブラリを追加したことで、GAS上でSlackAPIの利用が可能となります。
 


続いて、Slackアプリの作成とtoken情報の取得を行います。


 

Slackアプリ(bot)作成

SlackAPI
上記リンクより、Slackアプリを作成します。
※既に作成済みの場合はBOT_USER_OAUTH_TOKENの取得までスキップしてください。

アプリの作成は「Create New App」ボタンより行えます。
 
アプリの作成には[From scratch]と[From an app manifest]のどちらかを選べます。
今回は[From scratch]を選択します
   

Botの権限設定

サイドメニューの[OAuth & Permissions]よりOAuth & Permissions画面に遷移します。

OAuth & Permissionsのページ中腹にある[Scopes]より[Bot Token Scopes]を設定します。
 
[Add an OAuth Scope]より以下の権限を追加
今回はメッセージの書き込みを行うので、[chat:write]と[chat:write.public]を追加します。
 

ワークスペースへのインストール

OAuth & Permissionsのページ上部にある[Install to Workspace]をクリックし、ワークスペースにインストールを行います。
 
アクセス権限のリクエストを許可します。
 

BOT_USER_OAUTH_TOKENの取得

ワークスペースへのインストールが完了すると、OAuth & Permissionsのページに[Bot User OAuth Token]が表示されます。
 
取得した[Bot User OAuth Token]を、GASの[token]にセットします。
 


次に、投稿したいSlackチャンネルのIDを取得します


 

SlackのチャンネルIDを取得

Slackのワークスペースにて、投稿したいチャンネルのURLをコピーします。
投稿したいチャンネルを[右クリック]→[コピー]→[リンクをコピー]
https://[slackのワークスペース名].slack.com/archives/XXXXXXXXXXX
上記のようなリンクをコピーできるので、チャンネルID(URL末尾のXXXXXXXXXXX)を取得します。

 
取得した[SlackチャンネルID]を、GASの[channel]にセットします。
 


仮置きの値をセットできたので、スクリプトのデプロイ作業を行います。


 

作成したスクリプトをデプロイ

GASのデプロイボタンをクリックし、作成したScriptのデプロイを行います。
[新しいデプロイ][デプロイを管理][デプロイをテスト]の3種選択可能ですが、[新しいデプロイ]を選択します。

[種類の選択]ではウェブアプリを選択し、[設定]の説明文に概要を記載します。
[アクセスできるユーザー]は[全員]としておきます。
入力完了後、[デプロイ]ボタンよりデプロイ作業が開始されます。

アクセス許可を求められるので[アクセスを承認]より承認します。

デプロイが完了するとURLが発行されるのですが、特に利用しないので[完了]ボタンより戻ります。
 


最後にGASのトリガーを設定し、スクリプトを定期実行させます。


 

GASのトリガー設定

GASサイドメニューの[時計アイコン]をクリックし、トリガー画面に遷移します。

右下の「トリガーを追加」から新規トリガーの作成を行います。

[時間ベースのトリガーのタイプを選択]にて、[分ベースのタイマー]を選択します。
[時間の間隔を選択(分)]にて、[10分おき]を選択します。
上記以外はデフォルト値のままでOKです。
選択できたら[保存]ボタンを押下し登録します。

登録できました。
 


これで設定完了です!お疲れ様でした🙌


 

実行結果を確認してみる

震度5以上の地震は直近で発生していないため、対象を震度3まで引き下げて検証してみます。
実行はGASのヘッダーメニューの[実行]ボタンより行えます。 実行後はこのようにログが出力されます。 ※直近で震度3の発生がなかったため[Before_1_Minute]の値を30000にまで引き上げています。

Slackに投稿されました!

これで地震発生時の取り急ぎでの安否確認はできますね👏
実運用では投稿メッセージにgoogleフォームのリンクをつけて細かい安否確認を行った方が良いかと思います😇。

GAS & SlackでMeetのリンク自動生成Botを作ってみた

弊社ではSlackの文面では伝わらない時などに、Google Meet(以下Meet)を使用しています。
MeetのURL発行には以下の手順を踏まねばならず、若干の手間だったりします。

  • Meetのページを開く
  • 会議を新規で作成
  • 作成されたMeetURLをコピー
  • Slackに貼り付け

今回は、この手順をSlackのコマンドで実装(半自動化)してみました。
参考になれば幸いです。

GASでスクリプトの作成

Google Apps Script
上記リンクより、GASの新規プロジェクトを作成します。
プロジェクトの作成は「新しいプロジェクト」より行えます。

コードの作成

プロジェクトの作成ができたらスクリプトを書いていきます。
まずは、定数の定義からです。

const SLACK_POST_URL = 'https://slack.com/api/chat.postMessage';
const VERIFICATION_TOKEN = 'xxxxxxxxxx';
const BOT_USER_OAUTH_TOKEN = 'xxxxxxxxxx'

※[VERIFICATION_TOKEN]と[BOT_USER_OAUTH_TOKEN]は後ほどSlackのアプリから取得するので仮置きしておきます。

続いてMeetURLの取得処理を書いていきます。

/* Google MeetのURLを作成 */
function getMeetUrl() {
  const calendarId = 'primary'; // 一時的にイベントを作成するカレンダーID
  const dt = new Date();
  const date = dt.getFullYear() + '-' + (dt.getMonth() + 1) + '-' + dt.getDate();
  const requestId = Math.random().toString(32).substring(2); // 適当な文字列を作る
  const events = Calendar.Events.insert({
    summary: 'tmp_event',
    singleEvents: true,
    allDayEvent: true,
    start: { date },
    end: { date },
    conferenceData: {
      createRequest: {
        requestId,
        conferenceSolutionKey: {
          type: 'hangoutsMeet'
        },
      }
    }
  }, calendarId, { conferenceDataVersion: 1 })

  // MeetURLだけあれば良いので、作成後に予定そのものは削除する
  Calendar.Events.remove(calendarId, events.id);

  if (events.conferenceData.createRequest.status.statusCode === 'success') {
    const meetUrl = events.conferenceData.entryPoints[0].uri;
    return meetUrl;
  }
}

最後に、Slackへのレスポンス処理を作成します。

/* Slackに投稿 */
function postMessage(event, message) {
  const thread_ts = event.thread_ts ?? event.ts;
  const params = {
    method: 'post',
    payload: {
      token: BOT_USER_OAUTH_TOKEN,
      channel: event.channel,
      thread_ts: thread_ts,
      text: message,
    },
  };
  UrlFetchApp.fetch(SLACK_POST_URL, params);
}


/* Slackにメッセージを送信 */
function doPost(e) {
  const meetUrl = getMeetUrl();
  let message = meetUrl !== undefined ? `Meetのリンクを発行しました\n${meetUrl}` : 'Meetのリンクを作成できませんでした';
  let response = {
    response_type: 'in_channel',
    text: message,
  };

  if (e.parameter.command) {
    if (e.parameter.token !== VERIFICATION_TOKEN) {
      return null;
    }
    // Slash command
    return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);

  } else if (e.postData) {
    const contents = JSON.parse(e.postData.contents);

    if (contents.token !== VERIFICATION_TOKEN) {
      return null;
    }

    if (contents.type === 'url_verification') {
      // Event SubscriptionsのPost先URL検証のため
      return ContentService.createTextOutput(contents.challenge);
    } else if (contents.type === 'event_callback') {

      // botがbotの投稿に反応しないようにする
      if (contents.event.subtype && contents.event.subtype === 'bot_message') {
        return null;
      }

      // アプリメンションで起動させる
      if (contents.event.type === 'app_mention') {
        postMessage(contents.event, message);
      }
    }
  }
}

スクリプトの記載は以上です。

このようにつなげて記載できていればOKです。

スクリプトについては本職ではないため、偉大な先人の御知恵を賜りました。
参考文献: 【GAS】SlackでいつでもどこでもGoogle MeetのURLを発行できるBotを作ってみた

 

ライブラリ(SlackAPI)の追加

Slackへの投稿を行うために、SlackAPIライブラリを追加します。
ライブラリの追加はサイドメニューのライブラリ[ + ]より追加できます。
追加に際して、スクリプトIDが必要になるのですが、SlackAPIの場合は下記となります。

SlackAPIスクリプトID:
  1on93YOYfSmV92R5q59NpKmsyWIQD8qnoLYk-gkQBI92C58SPyA2x1-bq

※現状ライブラリのスクリプトIDはググってみるしかないようです...

ライブラリを追加したことで、GAS上でSlackAPIの利用が可能となります。

 

サービス(Google Calendar API)の追加

続いてMeetURL取得のため、Google Calendar APIを追加します。
サービスの追加はサイドメニューのサービス[ + ]より追加できます。
サービス一覧より[Google Calendar API]を選択。
サービスを追加したことで、GAS上でGoogle Calendar APIの利用が可能となります。
 


ここまでできたら一旦保存し、Slackアプリの作成を行います。


 

Slackアプリ(bot)作成

SlackAPI
上記リンクより、Slackアプリを作成します。

アプリの作成は「Create New App」ボタンより行えます。
アプリの作成には[From scratch]と[From an app manifest]のどちらかを選べます。
今回は[From scratch]を選択します。
 

VERIFICATION_TOKENの取得

Basic Informationのページ中腹にある[App Credentials]から[Verification Token]を取得します。

取得した[Verification Token]をGASの[VERIFICATION_TOKEN]にセットします。    

Botの権限設定

サイドメニューの[OAuth & Permissions]よりOAuth & Permissions画面に遷移します。

OAuth & Permissionsのページ中腹にある[Scopes]より[Bot Token Scopes]を設定します。

[Add an OAuth Scope]より以下の権限を追加
今回はメッセージの書き込みを行うので、[chat:write]と[chat:write.public]を追加します。
 

ワークスペースへのインストール(1回目)

OAuth & Permissionsのページ上部にある[Install to Workspace]をクリックし、ワークスペースにインストールを行います。

アクセス権限のリクエストを許可します。

BOT_USER_OAUTH_TOKENの取得

ワークスペースへのインストールが完了すると、OAuth & Permissionsのページに[Bot User OAuth Token]が表示されます。

取得した[Bot User OAuth Token]を、GASの[BOT_USER_OAUTH_TOKEN]にセットします。
 


仮置きした値の設定が完了したので、GASのデプロイを行います。


 

作成したスクリプトをデプロイ

GASのデプロイボタンをクリックし、作成したScriptのデプロイを行います。
[新しいデプロイ][デプロイを管理][デプロイをテスト]の3種選択可能ですが、[新しいデプロイ]を選択します。

[種類の選択]ではウェブアプリを選択し、[設定]の説明文に概要を記載します。
[アクセスできるユーザー]は[全員]としておきます。
入力完了後、[デプロイ]ボタンよりデプロイ作業が開始されます。

アクセス許可を求められるので[アクセスを承認]より承認します。

デプロイが完了するとURLが発行されるのでコピーします。
 


最後はSlackアプリにてコマンドの作成です。


 

Slackコマンドの設定

Slackサイドメニューの[Slash Commands]を選択しSlash Commands画面に遷移します。
[Create New Command]をクリックいただくことでコマンドの作成が行えます。

各項目をセットします。
[Request URL]にはGASでデプロイした際に作成されたURLをセットしてください。

設定できたら[save]ボタンで登録します。

登録できました。
 

ワークスペースへの再インストール(2回目)

Slackサイドメニューの[Installed App]を選択しInstalled App Setting画面に遷移します。

[Reinstall to Workspace]よりWorkspaceへの再インストールを行います。

アクセス権限のリクエストを許可します。  


これで設定完了です!お疲れ様でした!🙌


 

コマンドを試してみる

MeetのURLが発行されました!

これで多少は手間が省けそうですね👏

大門・浜松町を3D化

PLATEAUの3D都市データについて、弊社のオフィスがある大門・浜松町(三次メッシュコード:53393680)がLOD2(屋根形状があるモデル)にて公開されているということで確認してみます。

ピンク部分がLOD2整備範囲

3D都市モデル(Project PLATEAU)東京都23区 - データセット

東京都23区の3D都市データ(商用利用可能)はこちらよりダウンロードできます。

さて、CityGML、3D Tiles、GeoJson、MVT、Shape、FBX、OBJ、GeoTIFF...
と色々ファイル形式がありますが、どれを利用するか。
地理空間や3D関連のファイルは種類が多いですね。

今回はBlenderオープンソースの3DCGソフト)でインポートできる以下のFBXファイルを利用します。

  1. 建物モデル:13100_tokyo23-ku_2020_fbx_3_op > bldg > lod2 > 53393680_bldg_6677.fbx

  2. 橋梁モデル:13100_tokyo23-ku_2020_fbx_3_op > brid > 53393680_brid_6677.fbx

【blender】リアルな都市の作り方!(~PLATEAUを用いる方法~)(ボイスピーク 解説) - YouTube

こちらの動画を参考にさせていただき、Blenderで大門・浜松町の3D都市を作成しました。地面、背景、光源の設定や、建物のテクスチャ(色や質感)変更等、大変勉強になりました。ありがとうございます。

MacBook Pro(M1 Proチップ)で作業していたのですが、なかなか画像が出力(レンダリング)されず、調べていたところ...
なんとBlenderでM1 Macレンダリングが最適化される設定がありました。

この設定により、無事にレンダリングすることができました。

結果(赤の矢印が弊社のオフィスがあるビルです)

すごいですね。このようなオープンデータから価値を生み出していきたいです。

高精度音声認識モデル「Whisper」に文字起こしさせてみる

AI,IoT担当の大澤です。 OpenAIが「Whisper」というすごい音声認識モデルを開発したと盛り上がっていたので、どんなものなのかと思って試してみました。 文章生成の「GPT-3」、画像生成の「DALL・E 2」など、OpenAIは高性能なAIを開発してきているので今回も楽しみです。

結論から言うとめちゃめちゃ簡単に精度の高い文字起こしができました。GoogleColabを使用したので、以下にその手順を記載します。

1. 準備

GoogleDrive内に適当な作業フォルダを作ります。今回は「Whisper」フォルダにしました。 (私の環境では「マイドライブ>Colab Notebooks>whisper」というパスになってます)

作ったフォルダ内でもう一度「新規」のボタンを押し、今度は「その他」を選択すると以下のようなツリーが表示されます。

ここで「GoogleColaboratory」が表示されていればいいですが、ない人は「新規」→「その他」→「アプリを追加」で「colaboratory」と検索して追加しておきましょう。

GoogleColaboratory
GoogleColabを追加
追加されれば先程のツリーに表示されているはずですので、クリックして新規ファイルを作成します。

新規ファイル

2. 設定

まずはcolabの設定をしましょう。タブの「ランタイム」から「ランタイムタイプを変更」を選択し、以下のようにGPUに変更します。

ランタイムのタイプを変更
GPUに設定

次に文字起こしをするデータを用意しましょう。 動画データ(mp4,movなど)でも音声データ(mp3,wavなど)でも使用することができます。 私の方では「output.mp4」という動画を用意しました。 この動画データを先程準備したGoogleDrive内の「whisper」フォルダにアップロードしておきましょう。

output2.mp4もありますが気にしないでください

ついでにColabノートブックの名前をwhisper.ipynbに変えました。

3. コード

それではコードを書いていきます。たったの数行です。

# githubからインストール
!pip install git+https://github.com/openai/whisper.git

# whisperのインポート
import whisper

# mediumモデルを使用
model = whisper.load_model("medium")

# 推論
result = model.transcribe("/content/drive/MyDrive/Colab Notebooks/whisper/output.mp4")
print(result["text"])

# 結果をテーブル表示で見る
import pandas as pd
pd.DataFrame(result["segments"])[["id", "start", "end", "text"]]

セルを細かく区切りました。

ちなみにcolabにはセルというものがあり、このセル単位でコードを実行することができます。 一つのセルの中に全文書くこともできますが、出力を確認しながら進められるようにいくつかに区切って書いたほうが良いです。

4. 実行

それでは実行してみましょう。

タブの「ランタイム」→「すべてのセルを実行」で一気に実行できます。 ちなみにセル単位で実行するにはセルの左の再生マークを押すと、そのセルを実行できます。(Shift + Enterでもいけます)

また、おそらくGoogleDriveがマウントされている(連携されている)状態だと思いますが、されていない場合はアラートが出るので、指示に従って連携しましょう。

実行結果がこちらです。

ちなみに私はこちらの動画を使ってみました。

www.youtube.com

日本語でもかなりの精度で認識されていますね。

また、検出した動画時間も出力されるので、字幕を簡単につけることができそうです。 会議の議事録動画をとりあえず文字起こししておけば、あとからテキスト検索できてしかも動画内での該当箇所も簡単にわかるということですね。らくちんです。

5. まとめ

非常に簡単に実行できたかと思います。

今回はmediumモデルで実行しましたが、より精度の高いlargeも使うことができます。その代わりに時間がかかりますし、最悪メモリ不足で実行できない場合があるかも知れません。

2分半の動画ではmediumで1分、largeで2分近くかかっていたと思います。気になる方はセルの3行目を「large」に変えて試してみても良いと思います。

この精度の文字起こしがオープンソースで公開されているのはすごいですね。 それでは。

Elastic Beanstalk のエラー

暑い日々が続いておりますね。
みなさま、いかがお過ごしでしょうか。

今回はElastic Beanstalk(以下EBと記載します) のサポート対象から Node.js 12 が消滅するので、16にあげる作業を行った際に、よくわからないエラーにぶち当たったので、備忘録含め、記載します。


Node.js 12 running on 64bit Amazon Linux 2 から
Node.js 16 running on 64bit Amazon Linux 2 にアップデートを行おうと、少し調整を入れ、Circle CIからEBへデプロイをかけたところ、

CIエラー

ERROR: TimeoutError - The EB CLI timed out after 10 minute(s). The operation might still be running. To keep viewing events, run 'eb events -f'. To set timeout duration, use '--timeout MINUTES'.

タイプアウトでデプロイが失敗しました。
ログを出力してみると何も出ていない...

よくわからないので、ダメ元で再度CIを実行しました。

CIエラー2

2022-08-22 04:12:40 ERROR Instance deployment: 'npm' failed to install dependencies that you defined in 'package.json'. For details, see 'eb-engine.log'. The deployment failed.
2022-08-22 04:12:40 ERROR Instance deployment failed. For details, see 'eb-engine.log'.
2022-08-22 04:12:41 ERROR [Instance: i-0b7da73e780dfead2] Command failed on instance. Return code: 1 Output: Engine execution has encountered an error..
2022-08-22 04:12:41 INFO Command execution completed on all instances. Summary: [Successful: 0, Failed: 1].
2022-08-22 04:12:41 ERROR Unsuccessful command execution on instance id(s) 'i-0b7da73e780dfead2'. Aborting the operation.
2022-08-22 04:12:41 ERROR Failed to deploy application.



今度はエラーが出ていたので、詳細確認のため、ログを見てみると

2022/08/22 04:12:40.550548 [ERROR] An error occurred during execution of command [app-deploy] - [Use NPM to install dependencies]. Stop running the command. Error: Command /bin/sh -c npm --production install failed with error signal: killed. Stderr:npm WARN config production Use --omit=dev instead.

2022/08/22 04:12:40.550567 [INFO] Executing cleanup logic

2022/08/22 04:12:40.615852 [INFO] CommandService Response: {"status":"FAILURE","api_version":"1.0","results":[{"status":"FAILURE","msg":"Engine execution has encountered an error.","returncode":1,"events":[{"msg":"Instance deployment: 'npm' failed to install dependencies that you defined in 'package.json'. For details, see 'eb-engine.log'. The deployment failed.","timestamp":1661141560538,"severity":"ERROR"},{"msg":"Instance deployment failed. For details, see 'eb-engine.log'.","timestamp":1661141560558,"severity":"ERROR"}]}]}



このようなログが吐き出されていました。
Local で
npm --production install

を実行してもエラーは起こりませんでした。

少しGoogle先生に質問をしたところ、

stackoverflow.com

この記載を見つけました。
インスタンスのスペックが低すぎるのが原因ではないか。
確かに上記エラーが発生したEBのスペックは t3.micro でした。

藁にも縋る気持ちで、t3.medium に更新を行い、再度チャレンジしました...

結果は成功。


ログだけ見てもよくわからない系のエラーは悩ましいですね。解決できてよかったです。

Pythonで3D地形図化

弊社のオフィスのある芝大門は、江戸時代より増上寺門前町として多くの商店もあり賑わっています。以前、ご紹介しましたが江戸の大名屋敷もすぐそばにありました。戻れるものならこの目で見てみたいものです。

このあたりは、江戸時代に埋め立てがされ、それ以前は海の下(日比谷入江)だったようです。
国土地理院標高タイルの数値データより3D地形図を作成し、その事実を探っていきたいと思います。

matplotlibで国土地理院標高タイルから3D地形図を描いてみる - プログラム日記φ(..)

こちらの記事を参考にさせていただきました。ありがとうございました。

では、行って参る。

1. モジュールのインポート

よく使われるモジュールですね。

import pandas as pd #csvファイル読み取り用
import requests #HTTP通信用
import numpy as np #数値計算用
import matplotlib.pyplot as plt #グラフ作成用
from io import StringIO #文字列をファイルのように扱える

2. 標高データの読込

こちら国土地理院標高タイルについて、芝大門周辺の4つのタイルを取得します。

url_1 = 'http://cyberjapandata.gsi.go.jp/xyz/dem/13/7276/3226.txt' #芝大門を含むタイルNo.1
url_2 = 'http://cyberjapandata.gsi.go.jp/xyz/dem/13/7275/3226.txt' #西側のタイルNo.2
url_3 = 'http://cyberjapandata.gsi.go.jp/xyz/dem/13/7276/3227.txt' #南側のタイルNo.3
url_4 = 'http://cyberjapandata.gsi.go.jp/xyz/dem/13/7275/3227.txt' #南西側のタイルNo.4

urlList = [url_1, url_2, url_3, url_4]
z_result = []

for url in urlList:
  response = requests.get(url)
  maptxt = str.replace(response.text, u'e', u'-0.0')
  Z = pd.read_csv(StringIO(maptxt), header=None)
  z_result.append(Z)

3. 標高データを結合

読み込んだ4つのタイルの標高データを結合します。

z1 = np.concatenate((z_result[1],z_result[0]), axis = 1) #No.1とNo.2を結合
z2 = np.concatenate((z_result[3],z_result[2]), axis = 1) #No.3とNo.4を結合 

z_mix = np.concatenate((z1,z2), axis = 0) #No.1&2とNo.3&4を結合

4. 3Dグラフの設定

3Dグラフのデザイン部分です。

X, Y = np.meshgrid(np.linspace(0,255,512), np.linspace(255,0,512))
fig = plt.figure(figsize=(10, 8), dpi=80, facecolor='w', edgecolor='k') #w:白(White), k:黒 (Black)
ax = fig.gca(projection='3d') #3Dグラフ
ax.set_aspect('auto') #Axes3D currently only supports the aspect argument 'auto'.
ax.view_init(84, -67.5) #視点の設定

5. 3Dグラフを描画

標高値に応じて曲面を描きます。また、弊社のオフィス位置あたりに★を出力します。

surf = ax.plot_surface(X, Y, z_mix, cstride=1, rstride=1, cmap='rainbow', antialiased=False)
ax.text(140, 195, 2, "★", 'z', color='white', size='xx-large')
cb = plt.colorbar(surf, shrink=0.5, aspect=10)

6. 3Dグラフを保存

さて、どうでしょうか。

plt.savefig('mix_map.jpg')

7. 結果

江戸の見晴らしの名所と言われた愛宕山や、芝公園辺りから標高が高くなっているのが確認できますね。

Pythonで旅する東海道五十三次

東海道五十三次のルートは、日本橋(江戸)〜三条大橋(京)の全長約490kmの街道です。

江戸時代の人は、徒歩で約二週間かけて旅していたとのこと。一日の移動距離は平均すると35km(一歩が70cmとすると5万歩)となります。すごいですね。

私もその昔、東海道五十三次を自転車でトライしたのですが、小田原〜箱根間で転倒し、箱根の坂を前に挫折しました。

今回、Pythonで再チャレンジしたいと思います。

1. モジュールのインポート

import folium #地図作成用
from folium.features import DivIcon #文字列表示用
from folium import plugins #機能拡張用

2. 東海道五十三次の宿場を定義

shukuba_list = \
[['日本橋',139.774444444444,35.6836111111111], 
['品川宿',139.739166666667,35.6219444444444], 
['川崎宿',139.707777777778,35.5355555555556], 
['神奈川宿',139.632277777778,35.4727777777778], 
['保ヶ谷宿',139.595555555556,35.4440277777778],
['戸塚宿',139.529861111111,35.3950277777778], 
['藤沢宿',139.486305555556,35.3456666666667], 
['平塚宿',139.337805555556,35.3272777777778], 
['大磯宿',139.315305555556,35.309],
['小田原宿',139.161027777778,35.2487222222222], 
['箱根宿',139.026361111111,35.1904166666667], 
['三島宿',138.914472222222,35.11925], 
# 40宿場省略
['草津宿',135.960638888889,35.0174444444444], 
['大津宿',135.861416666667,35.0059722222222], 
['三条大橋',135.774361111111,35.0103333333333]]

※緯度・経度の引用元
東海道五十三次 - Wikipedia

3. 地図を作成

tokaido_map = folium.Map(location=[35.360626, 138.727363], zoom_start=8, tiles='stamenterrain') #富士山の座標を中心に地図を作成

for i in range(len(shukuba_list)):
    length=len(shukuba_list[i][0]) #宿場名の文字数を取得 
    if i == 0 or i == 54: #始点と終点は★を表示
        folium.Marker(location=[shukuba_list[i][2], shukuba_list[i][1]], icon=DivIcon(
            icon_size=(25, 25),
            icon_anchor=(0, 0),
            html='<div style="text-align: center; font-size: 11pt; color : black width: 25px; height: 25px; background: skyblue; border:2px solid #666; border-radius: 50%; ">'+"★"+'</div>'),
            popup=shukuba_list[i][0]+(' '*length)).add_to(tokaido_map) #全角スペースを入れないとポップアップの文字が改行されてしまう
    else: #始点と終点以外は宿場順を表示
        folium.Marker(location=[shukuba_list[i][2], shukuba_list[i][1]], icon=DivIcon(
            icon_size=(25, 25),
            icon_anchor=(0, 0),
            html='<div style="text-align: center; font-size: 11pt; color : black width: 25px; height: 25px; background: skyblue; border:2px solid #666; border-radius: 50%; ">'+str(i)+'</div>'),
            popup=str(i)+'.'+shukuba_list[i][0]+(' '*(length+1))).add_to(tokaido_map) #宿場名をポップアップ表示

4. フルスクリーン機能を追加

plugins.Fullscreen(
    position="topright",
    title="拡大する",      
    title_cancel="元に戻す",
    force_separate_button=True,
).add_to(tokaido_map)

5. HTMLに保存

tokaido_map.save("tokaido-53.html")

結果

自分の足で箱根越えをしたいですね。

File:NDL-DC 1309891-Utagawa Hiroshige-東海道五拾三次 吉原・左富士-crd.jpg - Wikimedia Commons

また、こちらの浮世絵に描かれているように、13.原宿〜14.吉原宿の間で左側に富士山が見える場所があります。箱根越えの後の楽しみです。