2021年8月31日火曜日

卒論に向けて

学会の詳細抄録が完成し、一息つきました。実証実験も順調に進み、あとは定量評価に向けてデータを蓄積し、実証実験で出てきた課題を解決してシステムの改善をしていくばかりとなりました。

それと並行して卒業論文の準備を進めていきたいと思います。まずは先行研究の調査です。システム開発を開始する前に先行研究の論文は読みましたが、読みっぱなしでまとめてきませんでした。しかし、卒論では先行研究について関連する研究の要約と今回の研究との違いについて触れなくてはなりません。

そこで、次回から先行研究の論文を読んでスライド5枚程度にまとめ(緒論、方法、結果、考察、結論)10分ほどの発表をしてはどうでしょうか。

今回の研究のコンセプトに「家族健康記録(FHR)」があります。そこで、家族で高齢者を見守るというコンセプトの研究について調べてはどうでしょうか。例えば、「第四次産業革命時代におけるヘルスケアサービス分野のデジタルトランスフォーメーション等に関する調査研究」なんかどうでしょうか。

また、スマートデバイスで高齢者のバイタルをモニタリングする試みとしてスマートウォッチによる心電図の計測が最近話題を集めています。先進的な取り組みとして調べておくとよいかもしれません。例えば「腕時計型脈波モニタリングデバイスによる心房細動の検出の妥当性に関する研究」というのがあります。

これは、研究ではありませんがタカラトミーという玩具メーカーから「あみちゃん」という高齢者をターゲットとしたAI人形が出ています。モニタリングではありませんが、スマートスピーカーのチャットボットと通じるところがあり、将来的にはこうしたAI人形が高齢者の健康管理のユーザーインターフェースになるのではないかと思います。そこで、このAI人形について調べてみるのも面白いかもしれません。

2021年8月28日土曜日

血圧の記録にトラブルが発生!

8/24のGoogle Home班の報告にある血圧記録のトラブルについて原因を究明して解決したので報告します。

【現象】

8/24以降に入力した血圧データが壊れている。最初は収縮期血圧のみが壊れていたが、その後確認すると拡張期血圧も壊れている。

薬シェア健康記録のスプレッドシート「血圧」に記録されたデータが下記のようになっている(血圧の数値が入るべきところが Ljava.lang.Object になっている)。

2021/08/27 20:04:14,[Ljava.lang.Object;@2570d431,[Ljava.lang.Object;@3e48722a

【原因調査】

Action consoleでテストしてREQUESTデータを入手した(下記)。

{
  "responseId": "17678248-26ed-4c5c-a581-65b716413a8f-b9889856",
  "queryResult": {
    "queryText": "上が129で、下が70です",
    "parameters": {
      "systole": [
        129
      ],
      "diastolic": [
        70
      ],
      "ketsuatsu": [
        "最高血圧",
        "最低血圧"
      ]
    },
    "allRequiredParamsPresent": true,
    "fulfillmentMessages": [
      {
        "text": {
          "text": [
            ""
          ]
        }
      }
    ],
    "outputContexts": [
      {
        "name": "projects/kusuri-141ad/agent/sessions/ABwppHGn1M9qnMcRD7Nut4wAt7mBfE0xTyUfQAHnGuhxNzWnq5nemI-Hb1w8H9PwoR0SK9SvaUIc5fFP/contexts/actions_capability_media_response_audio",
        "parameters": {
          "systole": [
            129
          ],
          "systole.original": [
            "129"
          ],
          "diastolic": [
            70
          ],
          "diastolic.original": [
            "70"
          ],
          "ketsuatsu": [
            "最高血圧",
            "最低血圧"
          ],
          "ketsuatsu.original": [
            "上",
            "下"
          ]
        }
      },
      {
        "name": "projects/kusuri-141ad/agent/sessions/ABwppHGn1M9qnMcRD7Nut4wAt7mBfE0xTyUfQAHnGuhxNzWnq5nemI-Hb1w8H9PwoR0SK9SvaUIc5fFP/contexts/actions_capability_screen_output",
        "parameters": {
          "systole": [
            129
          ],
          "systole.original": [
            "129"
          ],
          "diastolic": [
            70
          ],
          "diastolic.original": [
            "70"
          ],
          "ketsuatsu": [
            "最高血圧",
            "最低血圧"
          ],
          "ketsuatsu.original": [
            "上",
            "下"
          ]
        }
      },
      {
        "name": "projects/kusuri-141ad/agent/sessions/ABwppHGn1M9qnMcRD7Nut4wAt7mBfE0xTyUfQAHnGuhxNzWnq5nemI-Hb1w8H9PwoR0SK9SvaUIc5fFP/contexts/actions_capability_account_linking",
        "parameters": {
          "systole": [
            129
          ],
          "systole.original": [
            "129"
          ],
          "diastolic": [
            70
          ],
          "diastolic.original": [
            "70"
          ],
          "ketsuatsu": [
            "最高血圧",
            "最低血圧"
          ],
          "ketsuatsu.original": [
            "上",
            "下"
          ]
        }
      },
      {
        "name": "projects/kusuri-141ad/agent/sessions/ABwppHGn1M9qnMcRD7Nut4wAt7mBfE0xTyUfQAHnGuhxNzWnq5nemI-Hb1w8H9PwoR0SK9SvaUIc5fFP/contexts/actions_capability_audio_output",
        "parameters": {
          "systole": [
            129
          ],
          "systole.original": [
            "129"
          ],
          "diastolic": [
            70
          ],
          "diastolic.original": [
            "70"
          ],
          "ketsuatsu": [
            "最高血圧",
            "最低血圧"
          ],
          "ketsuatsu.original": [
            "上",
            "下"
          ]
        }
      },
      {
        "name": "projects/kusuri-141ad/agent/sessions/ABwppHGn1M9qnMcRD7Nut4wAt7mBfE0xTyUfQAHnGuhxNzWnq5nemI-Hb1w8H9PwoR0SK9SvaUIc5fFP/contexts/google_assistant_input_type_voice",
        "parameters": {
          "systole": [
            129
          ],
          "systole.original": [
            "129"
          ],
          "diastolic": [
            70
          ],
          "diastolic.original": [
            "70"
          ],
          "ketsuatsu": [
            "最高血圧",
            "最低血圧"
          ],
          "ketsuatsu.original": [
            "上",
            "下"
          ]
        }
      },
      {
        "name": "projects/kusuri-141ad/agent/sessions/ABwppHGn1M9qnMcRD7Nut4wAt7mBfE0xTyUfQAHnGuhxNzWnq5nemI-Hb1w8H9PwoR0SK9SvaUIc5fFP/contexts/__system_counters__",
        "parameters": {
          "no-input": 0,
          "no-match": 0,
          "systole": [
            129
          ],
          "systole.original": [
            "129"
          ],
          "diastolic": [
            70
          ],
          "diastolic.original": [
            "70"
          ],
          "ketsuatsu": [
            "最高血圧",
            "最低血圧"
          ],
          "ketsuatsu.original": [
            "上",
            "下"
          ]
        }
      }
    ],
    "intent": {
      "name": "projects/kusuri-141ad/agent/intents/a27fc1b7-5ddc-40c7-8c38-cbe1c79bafe4",
      "displayName": "ketsuatsu"
    },
    "intentDetectionConfidence": 1,
    "languageCode": "ja"
  },
  "originalDetectIntentRequest": {
    "source": "google",
    "version": "2",
    "payload": {
      "user": {
        "locale": "ja-JP",
        "lastSeen": "2021-08-27T23:23:51Z",
        "userVerificationStatus": "VERIFIED"
      },
      "conversation": {
        "conversationId": "ABwppHGn1M9qnMcRD7Nut4wAt7mBfE0xTyUfQAHnGuhxNzWnq5nemI-Hb1w8H9PwoR0SK9SvaUIc5fFP",
        "type": "ACTIVE",
        "conversationToken": "[]"
      },
      "inputs": [
        {
          "intent": "actions.intent.TEXT",
          "rawInputs": [
            {
              "inputType": "VOICE",
              "query": "上が129で、下が70です"
            }
          ],
          "arguments": [
            {
              "name": "text",
              "rawText": "上が129で、下が70です",
              "textValue": "上が129で、下が70です"
            }
          ]
        }
      ],
      "surface": {
        "capabilities": [
          {
            "name": "actions.capability.MEDIA_RESPONSE_AUDIO"
          },
          {
            "name": "actions.capability.SCREEN_OUTPUT"
          },
          {
            "name": "actions.capability.ACCOUNT_LINKING"
          },
          {
            "name": "actions.capability.AUDIO_OUTPUT"
          }
        ]
      },
      "isInSandbox": true,
      "availableSurfaces": [
        {
          "capabilities": [
            {
              "name": "actions.capability.AUDIO_OUTPUT"
            },
            {
              "name": "actions.capability.SCREEN_OUTPUT"
            },
            {
              "name": "actions.capability.WEB_BROWSER"
            }
          ]
        }
      ],
      "requestType": "SIMULATOR"
    }
  },
  "session": "projects/kusuri-141ad/agent/sessions/ABwppHGn1M9qnMcRD7Nut4wAt7mBfE0xTyUfQAHnGuhxNzWnq5nemI-Hb1w8H9PwoR0SK9SvaUIc5fFP"
}

リスト1.Action consoleで取得したリクエストデータ

これをDialogflowとGoogle Assistantの統合のリスト2と比較すると、outputTexts[0].parameters内のパラメタが、以前はスカラーだったのに対して今はベクトル(配列)になっている(下記へ抜粋)。

"parameters": {
  "systole": [
    129
  ],
  "systole.original": [
    "129"
  ],
  "diastolic": [
    70
  ],
  "diastolic.original": [
    "70"
  ],
  "ketsuatsu": [
    "最高血圧",
    "最低血圧"
  ],
  "ketsuatsu.original": [
    "上",
    "下"
  ]
}

そのため、下記プログラムでスプレッドシートに血圧を書き込むとLjava.lang.Objectとなるようだ。

function handleWebhook(req) {
  const parameters = req.queryResult.outputContexts[0].parameters;
  const intent = req.queryResult.intent;
  var text = '';
  if (req.queryResult.intent.displayName == 'ketsuatsu'){
      text =  '今日の最高血圧' + parameters['systole.original'] + '、最低血圧' + parameters['diastolic.original'] + 'を記録しました。';
      recordBloodPressure(parameters['systole.original'], parameters['diastolic.original']);
  }
  ...(中略)...
  return res;
}

リスト2.関数handleWebhookの抜粋(血圧処理部分)

この中でparameters['systole.original']などが配列になっているために起きるトラブルである。

この問題を解決するために、以下のように、パラメタの配列要素のうち先頭要素のみを取り出すように修正した。

function handleWebhook(req) {
  debug('handleWebhook:req=\n' + JSON.stringify(req, null, ' '));
  const parameters = req.queryResult.outputContexts[0].parameters;
  const intent = req.queryResult.intent;
  var text = '';
  if (req.queryResult.intent.displayName == 'ketsuatsu'){
    const systole = parameters['systole.original'][0]; // ←修正!
    const diastolic = parameters['diastolic.original'][0]; //  ←修正!
    text =  '今日の最高血圧' + systole + '、最低血圧' + diastolic + 'を記録しました。';
    recordBloodPressure(systole, diastolic);
  }
  ...(中略)...
  return res;
}

リスト3.修正したプログラム

すなわち、最高血圧は parameters['systole.original'] を取り出してスプレッドシートへ書き込んでいたが、parameters['systole.original'] は配列になっていたので parameters['systole.original'][0] のように[0]を付けて先頭要素を取り出した。この修正で血圧データが記録されないという問題は解消した。

2021年8月25日水曜日

2021/08/24

【全体】

本日は作成途中の詳細抄録に関するお話をしました。

未完成の部分をゼミメンバーで分担し、作業を行います。

次回のゼミは 27日(金)です。それまでに抄録の8割方完成を目指します。

 

【Google Home班】

今日は、まず#2 脈拍の記録機能 を追加しようとしました。

スピーカーに記録する際は、最高血圧、最低血圧の後に続けて伝えます。

ですが、色々しているうちに最高血圧がスプレッドシートに記録されずLjava.lang.Object;

が表示されます。調べてみてもどこを改善すればいいのか分かりません。

次回はここから進めたいと思います。

2021年8月18日水曜日

2021/08/17

 【Google Home班】

お盆が明けて、久しぶりの活動でした。

今日は、To Doリストの把握をしました。”#4 血圧のグラフを折れ線グラフに”を来週進めていくことにしました。また、140と90に赤い線を引くことも同時に進めていきたいと思います。

また、”#7 血圧の削除時の確認”で削除対象記録の日時・最高血圧・最低血圧を読み上げるようにすることに取り組む予定です。

 

【LINE班】

現在、学会用の詳細抄録を作成中です。

先生によるシステムの検証により、様々な課題も見つかりました。優先順位を決め、1つずつ課題をこなしていきたいです。

2021年8月14日土曜日

TODOリスト(Vol.2)

 TODOリストの第2弾です。今回は、実証実験も始まって具体的な課題が見えてきたました。

#1 音声ヘルプ機能

スピーカに向かって何を言えばよいか戸惑うことがある。そこで、音声ヘルプ機能を作成したらどうか。

#2 脈拍の記録機能

今使っている血圧計は脈拍も測定できる。日々記録する情報は身長よりも脈拍の方が重要度が高いと思われるので、脈拍を記録できる機能を付けたらどうか。

#3 音声入力に要する時間を計測

「OKグーグル、あゆみにつないで」と話しかけてすべての記録が完了するまでの過程をログ(Debug)に書いて、音声入力に要する時間を自動で計測できるようにしたらどうか。

#4 血圧のグラフを折れ線グラフに

現在、血圧のグラフは曲線で補正したグラフになっているが、データ密集点では返って不自然で見にくいグラフになっている。曲線補正はやめて直線で結んではどうか。

図 血圧のグラフ

また、縦軸の目盛りが小さくて見えないので、140と90に赤い線を引いてはどうか?

#5 手入力で薬剤を追加したい

現在、処方情報はQRコードで入力しているが、それに対して薬剤を手入力で追加・修正する手段があるといい。

#6 スマートスピーカ同士の音声通話

Google Duoを使ってスマートスピーカ同士で会話ができないだろうか?

#7 血圧削除時の確認

血圧削除時に削除データを読み上げたらどうか(何も言わずに「削除」となると、何が消されたのか不安になる)。読み上げる項目は入力日時と上下の血圧。

#8 薬の情報

LINEの「薬の情報」で表示されるカルーセルの「バイアスピリン」が川崎病になっている。これは「くすりのしおり」の検索結果の先頭がこれになっているためだと思われる。正しい薬剤情報を出すにはどうすればよいか(何が必要か)を検討してはどうか。

#9 YakuShareクライアント

コントローラ上で稼働しているYakuShareサーバへ各種Webリクエスト(config, reload, help, google-home-notifier)を送るクライアントを作ってはどうか。

8月10日から実証実験始めました

 2021年8月10日より、コントローラを我が家に設置し、実証実験を始めました。実証実験の記録はGoogleドライブ内に作った専用フォルダ内にあるExcelファイル「実証実験.xlsx」に記録していってます。

また、このフォルダには次のフォルダを作成し、様々な記録を保存しています。

Debug

これは、GASが出力したログのバックアップです。ログはGoogleドキュメントDebugに出力していますが、定期的にバックアップを取って切り詰めないと(ログを削除しないと)膨れ上がって遅くなります。バックアップを取った日付をファイル名としてこのフォルダにバックアップファイルを溜めています。

YakuShare

これは、コントローラ側で動くNodejsのサーバプロセス YakuShare.js のログや、障害記録を保存しているフォルダです。現在はログは手作業で保存しています。これも日付をファイル名にしています。


障害事例

ここで、今日(2021/8/14)発生した障害とその対応を書き記しておきます。

8/14に発生した障害とその対応

6:30にスピーカが朝食後の薬の服用を促したが、他の部屋にいて気付かず、6:45の4回目の督促後の6:46に「OKグーグル、お薬あとで」と話しかけた。この段階で、服薬イベントの開始時刻が6:30から6:40に書き換わったが、何事もなかったかのように約5分後の6:50に5回目の督促が発話された。その後、カレンダーの予定は赤色(未)から黄色(応答無)に変わった。

この不可解な動きの原因は、最初にスピーカが服用を促してから10分を経過したあとに「あとで」を指示したときのプログラムの挙動にある。常識的にはその時刻から10分後に開始時刻を延期するだろうと考えるが、なぜかプログラムは、開始時刻(つまり6:30)を10分延期してしまった(その結果、開始時刻が6:40と過去の時刻になった)。

そこで、開始時刻を現在時刻から10分後にするために、プログラムを修正した。修正対象のプログラムは「服薬管理システムA2」の関数delayStartTimeである。

/**
 * 現在時刻の予定を指定した時間だけ遅らせる
 * @param  {Integer}   delay     遅らせる時間(分)
 */
function delayStartTime(delay) {
  debug('delayStartTime:delay=' + delay);
  // 当日の服薬予定(未)をすべて取得する
  var events = CalendarApp.getDefaultCalendar().getEventsForDay(
    new Date(),
    {search: '(未)'}
  );  

  // 取得した予定から現在時刻が開始~終了時刻に含まれる予定を探して
  // delay(分)だけ遅らせる
  var current_time = new Date();
  for(const event of events){
    //遅らせる時
    if(delay > 0){
      var start_time = event.getStartTime();
      var end_time = event.getEndTime();
      if (start_time <= current_time && current_time <= end_time) {
        // 2021.8.14 by M.Tanaka
        delay += getDiffSec(start_time, current_time) / 60;
        event.setTime(delayTime(start_time, delay), 
          delayTime(end_time, delay));
        break;
      }
    //早める時
    }else if(delay < 0){
      var start_time = event.getStartTime();
      var end_time = event.getEndTime();
      if (start_time >= current_time) {
        event.setTime(delayTime(start_time, delay), 
          delayTime(end_time, delay));
        break;
      }
    }
  }
}

/**
 * 2つの時間の差を取得する(秒単位)
 * 2021.8.14 by M.Tanaka
 */
function getDiffSec(date1, date2) {
  const diff = date2.getTime() - date1.getTime();
  return diff / 1000;
}

リスト1 修正した服薬管理システムA2

修正内容は関数delayStartTimeの「遅らせる時」の処理で、遅延時間deleyに開始時刻から現在時刻の時間差を足している箇所である。

delay += getDiffSec(start_time, current_time) / 60;

これによって、現在時刻の10分後に開始時刻が設定される。

プログラムを修正して新規デプロイして、新しいWebURLをIFTTTの「お薬待って」のWebhook URLに上書きした。

【補足】2021.8.15追記

翌日(8/15)、テストがてらに同じことをやってみた(4回目の督促後に「あとで」と話しかけた)。その結果、カレンダーの服薬予定は現在時刻の10分後に無事変更されたが、5回目の督促後に予定が赤色(未)から黄色(応答無)になった。

プログラムを確認すると、コントローラ側のプログラムYakuShare.jsでは予定延期時にカウンタをリセットしておらず、そのため、督促が5回目に達すると(未)から(応答無)に変更されていた。

そこで、YakuShare.jsの456行目あたりにカウンタをリセットするコードを追加した。

counter = 1;

リスト2 カウンタのリセット(YakuShare.js)

この修正を反映させるためにYakuShare.jsを再起動した。なお、YakuShare.jsはNodejsのforeverというツールで常駐化させているので、foreverコマンドを使って一旦停止し、再度、開始する必要がある。

また、YakuShare.jsはngrokを使って公開URLを作成しているので、起動時に配慮すべき事項がある。そのあたりに関しては別途マニュアルを作成したので注意されたい。


その他の問題点

8/10から実証実験を始めていろいろな問題に遭遇しました。ここでは、その主だったものを書き記しておきます。

①「あゆみ」に繋がらない!(8/10)

血圧を記録しようと「OKグーグル、あゆみにつないで」と話しかけると「わかりません」とスピーカーが応答しました。これはこれまでも何度も経験していることです。しょっちゅうというわけではありませんが、数週間に一度くらい遭遇します。原因はわかっていないのですが、ネットで検索すると同じような現象に出くわした人がいて、Action on googleでDisplay nameをリセット(削除してからまた設定)すると復旧することがわかりました。

Display nameというのはAction Consoleの [Develop] -> [Invocation] -> [Japanese] で設定する項目で、「OKグーグル〇〇につないで」というときの「〇〇」に該当する呼び出し名のことです。数週間に一度、これが認識されなくなるのです。

②最低血圧を誤認識!(8/11)

最低血圧を尋ねられて「59」と言ったつもりが「19」と誤認識されました。データの削除のやり方がわからず、スプレッドシートを手修正しました。あとで、DialogflowのIntentsを確認すると、「血圧を削除」と言えばよいことがわかったのですが、後の祭りです。利用者にはDialogflowを見ることはできないので、ヘルプが欲しいところです。

例えば「助けて」と言えば、音声ガイドで操作方法(話しかけ方)を教えてもらうと嬉しいです。IntentsにHelpを追加し、user expressionに「助けて」「ヘルプ」「教えて」などを追加しておくのはどうだろうか。そして、このIntentsが呼び出されたらText Responseに操作方法を設定しておけばよい。

③「血圧」と言ったのに!(8/12, 8/13)

あゆみにつないで「血圧」と言ったのに、聞き取ってもらえず言い直すことがありました。8/11は1回言い直しただけですが、8/12は2回も言い直しました。8/13はこれだけでなく最低血圧77を「7」と誤認識されたり、「血圧を消して」と言うと「応答がありません」と強制終了されたり、最終的に入力を終えるまでに数分間を要しました。こうなるともううんざりです。

④「お薬10分早く」と言ったのに!(8/13)

食事が早く終わったので、予定の時刻(6:30)になる前に「お薬10分早く」と言うと、「かしこまりました。10分早くします」と応答して、カレンダーも6:20開始に変更されたのに、6:20になっても「お薬飲んで」の督促がありません。

それもそのはず、コントローラ側のプログラムは毎朝午前1時にその日の予定を読み込んでタイマをセットします。したがって、それ以降に変更された予定には対応していません。これに対応するにはもう一度強制的にその日の予定を読み込ませる必要があります。それを行うのがreloadです。reloadを行うにはYakuShareサーバにGETメソッドで次のようなWebリクエストを行う必要があります。

https://XXXXXXXX.ngrok.io/reload

リスト3 reloadリクエスト

ここで、「XXXXXXXX.ngrok.io」はngrokが払い出したURLで、スプレッドシートpill-reminderに書き込まれています。あるいは、ngrokのEndpointsのStatusでも確認できます。

いずれにしても、予定を変更したら必ずこのWebリクエストをしなければなりません。今回は、リスト1の「早める時」の処理にこれを加える必要があります。

Webリクエストは関数GOOGLE_HOME_notifyのやり方を真似て作ればよいでしょう。

2021年8月4日水曜日

2021/08/03

  【Google Home班】

前回の問題点を解決しようと試みましたが、原因がわからず、解決できませんでした。

見たいグラフを変更するとき、3回目以降(例:血圧→身長→血圧)から表示が古いものになってしまいます。

利用者は身長・体重はあまり見ないだろうということで、このままでいくことに...。

そろそろ実際にスピーカーで記録していかねば。

 

【LINE班】

作成していた「使い方ガイド」ページの作成を進め、実際に公開してみました。また、LINEのリッチメニューにリンクを張り付けて、スマートフォンでの表示も確認しました。未完成のページも随時更新していきたいです。

来週からは、詳細抄録を中心に進めていきます。