Briswell Tech Blog

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

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.吉原宿の間で左側に富士山が見える場所があります。箱根越えの後の楽しみです。

文章から画像を生成してみよう

こんにちは.AI,IoT担当の大澤です.

最近AI界隈では文章から画像を生成するText to Imageタスクの分野で大変な盛り上がりを見せていました. OpenAIのDALL-E2GoogleImagenなど,高画質で高精度な画像を生成するモデルが相次いで発表されたことが大きいのではないでしょうか.

Text to Imageとは読んで字のごとく,入力された文字列をもとに画像を生成するタスクのことです.下の画像を見てみましょう.

麦わら帽子とサングラスを身に着けたサボテンですね.Photoshopで作られた作品のように見えますが…実はこちら,Googleが発表したImagenというAIが作成したものです.

こちらはまずAIへの入力として「A small cactus wearing a straw hat and neon sunglasses in the Sahara desert.」という文章が与えられます.日本語訳では「サハラ砂漠で麦わら帽子をかぶり、ネオンサングラスをかけた小さなサボテン。」です.

するとこの画像が出力されるわけです.しっかりと文章の意味を理解し,的確に画像へと変換されています.

「A photo of a Corgi dog riding a bike in Times Square. It is wearing sunglasses and a beach hat.(タイムズスクエアで自転車に乗っているコーギー犬の写真。サングラスとビーチハットをかぶっています。)」という文章を与えると次のような画像が生成されるようです.

面白いですね.実は今までにもこのようなタスクができるモデルはあったのですが,transformerや拡散モデルなど,近年生まれた技術の組み合わせでここまでクオリティの高い画像生成を行うことができるようになったようです.

ただ,これらは膨大な計算リソースを必要とするため,一般的なPCなどでは実行できません.代わりに計算量を大幅に削減したモデルで,画像生成を体験してみましょう.

こちらのサイトに掲載されているgooglecolabのdemoコードを使用させていただきました.

www.12-technology.com

まずはimagenのサイトにもあった文章と同じものを入れてみましょう.

A photo of a Corgi dog riding a bike in Times Square. It is wearing sunglasses and a beach hat.

流石にGoogleのようにはいきませんが,それっぽく見える画像が生成できました.

他にも色々試してみましょう.

A photo of snowman sunbathing on a beach in summer. Snowman is wearing sunglasses and a beach hat.

真ん中上部のガッツポーズが最高ですね.

Photo of a cat wearing sunglasses at the wheel of a convertible.

左下のボケ感がじわじわきますね.

Illustration of a sunflower wearing sunglasses eating ice cream.

イラスト風にもできました.

とてもおもしろいですね.夏なのでサングラス多めでお送りしました. それでは.

Pythonで夏を感じる

厳しい暑さが続きますね!
この1週間、ずっと30℃超えが当たり前で梅雨はどこ?って思っておりましたが、やはり梅雨明けしたようです。

OpenWeatherMapAPIにより全世界の天気データを取得できます。
PythonからこのAPIを呼び出し、取得した天気データをグラフ化して、一足先に夏を感じたいと思います。

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

import requests
import pandas as pd
import plotly.graph_objects as go
from pytz import timezone
from datetime import datetime

APIKEY = "" #APIキー
LATITUDE = "" #緯度
LONGITUDE = "" #経度
BG_COLOR = "#100500" #グラフ背景色
FONT_COLOR = "#fff5f3" #グラフ文字色
TITLE = "東京都港区の体感気温と湿度" #グラフタイトル

2. 天気情報を取得

url = "http://api.openweathermap.org/data/2.5/onecall"
payload = {"lat": LATITUDE, "lon": LONGITUDE, "lang": "ja", "units": "metric", "APPID": APIKEY}
weather_data = requests.get(url, params=payload).json()
tz = timezone(weather_data['timezone'])
wd = weather_data['hourly'] # 現在から48時間先までの1時間毎のデータを取得

3. 日時、体感気温、湿度をリスト化

weather_data_list = []
for w1 in wd:
    tmp_dict = {}
    tmp_dict['日時'] = datetime.fromtimestamp(w1['dt'], tz=tz).strftime("%m/%d %H時")
    tmp_dict['体感気温℃'] = int(w1['feels_like'])
    tmp_dict['湿度%'] = int(w1['humidity'])
    weather_data_list.append(tmp_dict)

df_temp = pd.json_normalize(weather_data_list)

4. グラフ化

# X軸:日時、Y軸:体感気温
trace1 = go.Scatter(
    x = df_temp['日時'],
    y = df_temp['体感気温℃'],
    mode = 'lines',
    name = '体感気温℃',
    yaxis='y1',
    line=dict(color='#da70d6')
)

# X軸:日時、Y軸:湿度
trace2 = go.Scatter(
    x = df_temp['日時'],
    y = df_temp['湿度%'],
    mode = 'lines',
    name = '湿度%',
    yaxis='y2',
    line=dict(color='#87cefa')
)

# Y2軸グラフ化
layout = go.Layout(dict(margin=dict(l=0, r=0, t=30, b=0), paper_bgcolor=BG_COLOR, plot_bgcolor=BG_COLOR,
            title=dict(text=TITLE, x=0.5, y=1.0, font=dict(color=FONT_COLOR, size=24), xanchor='center', yanchor='top', pad=dict(l=0, r=0, t=5, b=0)),
            font=dict(color=FONT_COLOR, size=18),
            xaxis=dict(title='日時', showgrid=False),
            yaxis=dict(title='体感気温℃', side = 'left', showgrid=False, range = [10, 40]),
            yaxis2=dict(title='湿度%', side = 'right', overlaying = 'y', showgrid=False, range = [40, 100])
))
fig = go.Figure(dict(data = [trace1,trace2], layout = layout))

# グラフ画像を保存
fig.write_image("weather_graph.png", height=400, width=1600, scale=1)

5. 結果

夏ですね!
ただ日中の湿度はそこまで高くないのでハワイのようにカラッとして過ごしやすそうです。

Pythonで銀河観測

先日、私たちが住んでいる「地球」が属している「太陽系」が属している「天の川銀河」(地球 ⊂ 太陽系 ⊂ 天の川銀河)の中心にあるブラックホールの姿が初めて撮影されました。すごい時代になったものです。

「Uppsala General Catalogue of Galaxies (UGC)」という北半球から見える約1万3千個の銀河のカタログがあります。そのカタログの中に、積分記号( ∫ )のような形の渦巻銀河「UGC 3697」があります。今回はその銀河をPythonで確認してみましょう。

銀河の画像を複数枚タイル状に表示する方法 - Qiita
pythonを用いて銀経銀緯で複数のfits画像を並べてプロットする方法 - Qiita

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

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

from astroquery.vizier import Vizier #Vizierカタログのデータ取得用
from astroquery.skyview import SkyView #fitsファイル取得用
from astropy.wcs import WCS #天球上の座標取得用
from astropy.io import fits #fitsファイル読込用
import glob #ディレクトリのファイル取得用
import matplotlib.pyplot as plt #グラフ描画用

2. カタログ存在確認

catalog = "UGC"
code = "3697"
v = Vizier(catalog="VII/26D/catalog",column_filters={catalog:"="+code})
data = v.query_constraints()
print(data[0])

出力結果は以下です。UGCカタログ(VII/26D/catalog)に存在していますね。

UGC   A   RA1950  DE1950      MCG      MajAxis MinAxis  Hubble Pmag  i 
         "h:m:s" "d:m:s"                arcmin  arcmin         mag     
---- --- ------- ------- ------------- ------- ------- ------- ---- ---
3697     07 05.6  +71 55 MCG+12-07-028    3.30    0.20 INTEGRL 13.1  --

3. fitsファイルを保存

def save(p,name,obs): 
    for onep,oneo in zip(p,obs):
        onep.writeto(name+"_"+oneo+".fits",overwrite=True)

path_fits = "./"
galaxy = catalog+code
olist = ["DSS"] #Digitized Sky Survey

paths = SkyView.get_images(position=galaxy,survey=olist)
save(paths,path_fits+galaxy,olist)

4. fitsファイルを元に画像を作成

# fitsファイルを取得
dss = glob.glob("./*_DSS.fits")

# グラフ描画領域を作成
F = plt.figure(figsize=(6,8))

# fitsファイルの読込
dssfilename = dss[0]
dssname = dssfilename.replace(".fits",".png")
dsshdu = fits.open(dssfilename)[0]
dsswcs = WCS(dsshdu.header)
dssdata = dsshdu.data

# グラフタイトルを定義
plt.figtext(0.45, 0.93, galaxy, size="large")

# オリジナル画像表示
plt.subplot(211, projection=dsswcs) #2行1列の1番目
plt.imshow(dssdata, origin='lower')
plt.grid(color='white', ls='dotted')

# 拡大画像表示
dssxlen, dssylen = dssdata.shape
dsscx = int(0.5 * dssxlen)
dsscy = int(0.5 * dssylen)
dssdx = int(dssxlen * 0.15)
dsswcscut = dsswcs[dsscx-dssdx:dsscx+dssdx,dsscy-dssdx:dsscy+dssdx]

plt.subplot(212, projection=dsswcscut) #2行1列の2番目
plt.imshow(dssdata[dsscx-dssdx:dsscx+dssdx,dsscy-dssdx:dsscy+dssdx], origin='lower')
plt.grid(color='white', ls='dotted')

# 画像保存
plt.savefig(galaxy + ".png")
plt.close()

5. 結果

宇宙に浮かぶ ∫ なんとも素敵ですね...