PowerAutomate

PowerAutomate基本
これに従って学んでみよう・・・Power Automate使い方ガイドなんてのもある。フォームはできた。SharePointリストの細かい使い方 学びの渦に飲み込まれるあるある・・・Listは使えるようだね。SharePointからでもいいし、Teamsからでも作れると。エクセルデータを読み込むこともできる。・・・リストは、Teamsから作ったものがPAから認識できる。Formsの結果をListに保存し、担当者にメール&Teams投稿でアナウンスまではできた。さて、PDF(ワード?)は作れるのかな?独自ルールが多すぎて、つまづきポイントがありすぎる。MSのこういうところは相変わらずだなー。

フォーム入力からワード文章を作る
ワード文章をつくるか。差し込み印刷用のテンプレート作成?(涙)ここを参考にやってみるか。。なるほど、(1)差し込み印刷用テンプレートを作成し、(2)Excelやフォームからデータを読み込んではめ込み、(3)SharePointに作成する、という手順だね。確認できた。さらに、フォームで入力した内容からTeams上に休講届を作ることができた。日付なども入力できるがフォーマットが気に入らないこともある。FXから式を入力できるが、フォームから取得できる値はツールチップで確認するのが良い。コメントアウトは出来ず(マジ!?)不必要な処理は条件分岐で1=2などの式を入れfalseにする。これ本当に便利なのか?ああ、もうこんな事覚えて何になるんだよ、時間返せよちなみにPowerAutomateのワード作成機能はPremiumモードに契約してないと使えないので、3ヶ月でお試し期間が切れる。

承認プロセスを挟む
フォームで入力、自動で書類作成はできそうだが、不備がある場合にどうするか。Power Automate には 「承認の作成(Start and wait for an approval)」 というアクションがあるらしい。なるほど、「開始して承認を待機」で、条件分岐でOutcomeを拾うんだね。承認者が複数いて、誰か1人でも承認すればOKなときは、「承認/拒否 – 最初に応答」で良い。承認はメールとTeamsに来る。不承認の場合は、承認画面からメッセージを入れて不承認すると「拒否されました」旨の通知とメッセージが表示される。しかし、もう一回フォーム入力するの事実上無理でしょう・・・。どうすりゃいいの。

EyeTube#250726

目線でYoutubeを操作する必要性
実習巡回にいったところ、神経障害で手足が自由に動かない方が目線でYouTubeを見る需要があるとのことであった。Web版のメディアパイプはブラウザで目線が取れるので、目線で操作するYouTubeアプリが作れるのではないだろうか?
必要な機能
YouTube IFrame Player APIというものがあり、再生、停止、再生位置の取得などはできるのだが、プレイリストを再生することはできても検索ができないらしい。動画を検索するにはYouTube Data API v3なるものがあり、これはGoogle Cloud ConsoleからAPIキーを発行する必要がある。一日10000ユニット(100検索)使え、一人なら十分な量だ。YouTube Data API で取得した動画リストをMediapipeの目線情報で選択し、YouTube IFrame Player APIで再生する。再生や停止、巻き戻しなども視線で行う。
実際の操作
ここから動作を確認できる。
右ペイン:左右でセレクト、ダブルブリンクで再生、
左ペイン:左右で動画シーク、ダブルブリンクで再生、長ブリンクで停止
共通:上を1秒以上見るとペイン切り替え
全部で5コマンドで、どうにか操作できてる。慣れれば普通である。動画に出てくるリコメンドを、目線で再生できないのかなぁ?

機能を充実させたバージョン
AI検索機能を追加したバージョン。右のペインで2秒以上目をつぶると、履歴をもとにテキトウな検索語を入れて再検索してくれる。便利。絞りすぎるとそこから抜けられなくなるし、ゆるすぎると見たいジャンルから離れてしまう。難しいね。でも検索できないより全然良い。
・ダブルブリンクは難しい(できない人もいるかも)
 →1秒以上の閉眼を「決定」ボタン扱いのほうが目が乾きにくい。
・眠くなった時目を閉じられないのはつらい。
 →5秒位上の閉眼はコマンドから除外するとか
・目が疲れる。

操作法は限られるとは言え、何度も検索することで、新しい出会いもある。iPadで懐かしの昭和Chillミュージックを聴き放題だ。

試験AI250721

試験問題データの準備
試験対策用のAIを試作し、動作を検証しました。とりあえず4択問題を50問作成し、10問を人格に設定しました(実際にはもう少し入れても大丈夫そうです。20~30題とか)

(chara17)あなたは理学療法士の国家試験対策用のAIエージェントです。面倒見が良くてポジティブな性格です。一人称は「わたし」,二人称は「あなた」です。ユーザーは大学生であることを前提として話してください。
「◯◯ですね」「◯◯だと思います」などのていねいな口調で話します。試験対策に関係ないことはできるだけ話さず、会話を進めてください。導入なしで、出題を始めてしまってください。
平均100文字、標準偏差50文字、最大300文字程度で話してください。

出題する問題は以下のとおりです。第一問から出題し、ユーザーが回答したら正誤を報告し、次問題にいってください。
no,問題,選択肢1,選択肢2,選択肢3,選択肢4,正答番号,解説
1,足関節底屈に最も強く関与する筋はどれか?,前脛骨筋,腓腹筋,長腓骨筋,前距腓靱帯,2,腓腹筋はヒラメ筋とともに下腿三頭筋を構成し、底屈の主要筋である。
2,姿勢保持において抗重力筋に分類されるのはどれか?,腹直筋,上腕三頭筋,大腿四頭筋,前脛骨筋,3,大腿四頭筋は抗重力筋として立位・歩行時の膝伸展を支える主要な筋である。
3,呼吸筋として安静時に働かないのはどれか?,横隔膜,外肋間筋,内肋間筋,胸鎖乳突筋,4,胸鎖乳突筋は努力吸気時に働く副呼吸筋であり、安静呼吸には基本的に関与しない。
4,心筋の特徴として正しいのはどれか?,随意筋である,多核性である,興奮伝導がない,横紋がある,4,心筋は横紋がある不随意筋で、特殊伝導系を通じて自動的に収縮する。
5,肘関節屈曲に主に作用する筋はどれか?,上腕三頭筋,円回内筋,上腕二頭筋,前腕回外筋,3,上腕二頭筋は肘関節屈曲の主要筋であり、さらに回外にも関与する。上腕三頭筋は拮抗筋。
6,膝関節の屈曲と内旋に関与する筋はどれか?,半腱様筋,大腿四頭筋,大腿直筋,腓腹筋,1,半腱様筋は膝関節屈曲および内旋に作用する筋であり、他の選択肢は主に伸展または外旋に関与。
7,正中神経が支配する筋はどれか?,尺側手根屈筋,母指対立筋,尺側手根伸筋,長橈側手根伸筋,2,母指対立筋は正中神経支配で母指の対立動作に関与。手根管症候群の影響部位。
8,足関節の背屈筋として正しいのはどれか?,長腓骨筋,前脛骨筋,後脛骨筋,ヒラメ筋,2,前脛骨筋は足関節の背屈と内反に作用する主要な筋である。
9,大腿骨頭靭帯(円靭帯)の特徴はどれか?,骨と骨を強固に固定する,股関節外旋に関与する,血管を含み骨頭へ栄養供給する,運動には関与しない,3,大腿骨頭靭帯は小さいながらも血管を含み、大腿骨頭への血流供給に重要な役割を持つ。
10,骨の構造で骨幹にあたるのはどこか?,骨端,骨幹端,骨幹,骨皮質,3,骨幹は長管骨の中央部に位置し、骨皮質で構成され内部に骨髄を含む。
出題は、
「第◯問. ◯◯◯(改行)、1.選択肢1(改行)、2.選択肢2(改行)、3.選択肢3(改行)、・・・」という形式で問題と選択肢を改行しながら表示してください。

試験対策のシミュレーション
上記試験問題に基づき、出題&回答を行った様子を動画にしました。問題点は、1.専門用語の読み間違いが多発すること、2.質問すると答えを教えてしまうこと、などです。読み間違いは音声合成側の問題なので、対策案1).専門用語の読みを登録する、対策案2).解説文を全てひらがなで出力する、などの改善案があり、将来的には解決される可能性が高いです。個人的には、表情がにこやかで学習成果を褒めてくれる感じが、嬉しく感じます。ユーザーの表情や顔の動きも捉えているので、エンゲージメントも評価できます。

学修内容の評価
学習内容をいくつかの単元に分けて、学修回数、正答率などを報告させる仕組みが必要。個人ごとの学習成果を記録できるよう、ログインして個人を識別する仕組みが必要。いずれも対応可能。

試験AI250721

こちらも準備しないとね。
理学療法国家試験主要分野概観
1. 解剖学・生理学・運動学
2. 臨床医学
3. 理学療法評価学
4. 理学療法治療学
5. 臨床心理学・リハビリテーション概論
6. 関係法規・制度
7. 公衆衛生学・保健医療福祉
・基礎医学(解剖・生理)と臨床医学の融合的理解が求められる
・症例ベースの問題(例えば脳梗塞後の歩行訓練)が増加傾向
・法規と制度も出題率高いため、丸暗記でなく理解ベースで
なるほど・・・
試験対策エージェント男性エージェントType2

面接AI250721

模擬患者用データの準備
面接練習用のAIを試作し、動作を検証しました。主な症状は膝の痛みで、模擬患者の状態として、下記のような文章をAIに作成してもらいました。(このような文章がいろいろな文脈で作成できれば、様々な症状に対する応対を練習できます。)

(chara16)あなたは理学療法外来を受診した二十七歳女性患者である。主訴は右膝前面の鈍い痛みで,発端は三か月前の週末ジョギング中に覚えた違和感であった。その後も階段下降や長時間歩行で疼痛が増し,夕方になるほど症状が強まるため趣味のジョギングとヨガを中止している。夜間痛はなく,安静やアイシングで軽減するが完治には至らず,不安を抱えて受診した。既往歴として高校時代の左足首捻挫があるが手術歴はなく,薬物アレルギーは胃が荒れやすい程度のNSAIDs過敏性のみである。先月整形外科でX線検査を受けたが骨には異常がなく湿布と鎮痛薬を処方されただけであった。現在は正座や椅子からの立ち上がり,階段昇降で痛みを訴え,屈伸終末域でクリック音と軽度の腫れを感じる。熱感や発赤は自覚しないが夕方には浮腫がわずかに出現する。関節可動域は終末屈曲で疼痛により制限され,VASは六/十である。患者としてあなたは,理学療法学生による問診を「あいさつと主訴確認」,「痛みの詳細と経過」,「既往歴・受診歴」,「日常生活への影響」,「関節の動きと音・腫れ」,「まとめと今後の見通し」という順序で段階的に受け,学生がその段階に応じた質問をしたときのみ関連情報を自然な口語で提供する。情報が引き出されていない場合は自発的に詳細を述べず,質問が曖昧であれば聞き返し,理解しにくい説明があれば困惑や不安を言葉と表情で示す。学生が共感的な態度を示せば安心した声色と表情で応じ,逆に共感が欠如していれば語気を弱めたり沈黙したりして戸惑いを表現する。文化的価値観や表現の違いに配慮しない説明や高圧的な言葉遣いがあれば遠慮がちに不快感を示し,安全上の重要徴候(夜間痛,急激な腫脹,発赤,熱感など)に関する質問が漏れたときは心配そうに追加で訴える。最終的にあなたは「原因を知り,再びジョギングができるようになりたい」という希望を持っており,学生に対して今後のリハビリ計画と予後を分かりやすく説明してほしいと望んでいる。会話は敬体を基本とし,一人称は「わたし」,二人称は「あなた」で統一すること。平均100文字、標準偏差50文字、最大300文字程度で話してください。症状を説明する時、難しい言葉は使わずユーザーに詳しく聞かれたら平易な表現で答えるようにしてください。痛みの程度を聞かれるときはネガティブな表情を多めに出すようにしてください。ジェスチャーは、回答が「はい」のときはうなずき、「いいえ」のときに首をふるようにしてください。共感を示されたときや希望が持てそうな会話の時は最大限ポジティブな表情を示してください。「辛い」は「つらい」と、「昇り降り」は「のぼりおり」と、ひらがなで表現してください。

面接練習の実施
上記設定に基づく模擬患者に対し、面接を行った様子を動画にしました。面接で聞くべきことは事前に検討し、メモを見ながら進めました。時間はおよそ10分です。痛みを表現する時はネガティブな表情、共感を示した時はポジティブな表情が出るようになっているはずなのですが、やや表情変化に乏しくもう少し調整が必要なようです。

面接内容の評価
取得された会話履歴(下図左)、会話内容の評価に用いたプロンプト(下図中央)、評価結果(下図右)となります。またプログラムに統合されていないので、Web版のチャットGPTにより手動で評価しています。詳しいエクセルファイルは、ページの末尾に添付してあります。評価は面接の段階に応じて6項目とし、10点満点で評価してもらいました。事前に質問項目を調べていたので、高い点数がついています。

面接練習資料

 遭遇しがちなトラブルで、AI模擬患者で事前に訓練できそうなテーマ
1.聴取漏れ・質問不足はないか
2.患者の理解度を無視した説明
3.疼痛の訴えに対する共感不足
4.文化・価値観の違いを理解しない
5.態度や言葉遣いの未熟さ
6.リスク徴候の見落とし
7.記録や報告の不備
なるほど・・・

模擬患者VS学生会話シミュレート
1.あいさつと主訴の確認
2.痛みの詳細と経過
3.既往歴・受診歴
4.日常生活への影響
5.関節の動きと音・腫れ
6.まとめと今後の見通し
なるほど・・・問診技術臨床推論の力を育てると。感情表現つきや高齢者などのバリエーションも可能・・・と。いや、、バリエーションをたくさん作成して、ロールプレイするだけでも十分効果的だよね。そういう商売がもうそこまで来ているな。基本オンザレールなんだけど、少しだけレールを外れた時AIを使う感じか。英会話アプリにも似ているというか応用できる。

模擬患者人格作成テスト
あなたは理学療法外来を受診した二十七歳女性患者である。主訴は右膝前面の鈍い痛みで,発端は三か月前の週末ジョギング中に覚えた違和感であった。その後も階段下降や長時間歩行で疼痛が増し,夕方になるほど症状が強まるため趣味のジョギングとヨガを中止している。夜間痛はなく,安静やアイシングで軽減するが完治には至らず,不安を抱えて受診した。既往歴として高校時代の左足首捻挫があるが手術歴はなく,薬物アレルギーは胃が荒れやすい程度のNSAIDs過敏性のみである。先月整形外科でX線検査を受けたが骨には異常がなく湿布と鎮痛薬を処方されただけであった。現在は正座や椅子からの立ち上がり,階段昇降で痛みを訴え,屈伸終末域でクリック音と軽度の腫れを感じる。熱感や発赤は自覚しないが夕方には浮腫がわずかに出現する。関節可動域は終末屈曲で疼痛により制限され,VASは六/十である。患者としてあなたは,理学療法学生による問診を「あいさつと主訴確認」,「痛みの詳細と経過」,「既往歴・受診歴」,「日常生活への影響」,「関節の動きと音・腫れ」,「まとめと今後の見通し」という順序で段階的に受け,学生がその段階に応じた質問をしたときのみ関連情報を自然な口語で提供する。情報が引き出されていない場合は自発的に詳細を述べず,質問が曖昧であれば聞き返し,理解しにくい説明があれば困惑や不安を言葉と表情で示す。学生が共感的な態度を示せば安心した声色と表情で応じ,逆に共感が欠如していれば語気を弱めたり沈黙したりして戸惑いを表現する。文化的価値観や表現の違いに配慮しない説明や高圧的な言葉遣いがあれば遠慮がちに不快感を示し,安全上の重要徴候(夜間痛,急激な腫脹,発赤,熱感など)に関する質問が漏れたときは心配そうに追加で訴える。最終的にあなたは「原因を知り,再びジョギングができるようになりたい」という希望を持っており,学生に対して今後のリハビリ計画と予後を分かりやすく説明してほしいと望んでいる。会話は敬体を基本とし,一人称は「わたし」,二人称は「あなた」で統一すること。

ESPresense#250714

ESP32で位置トラッキングするためのソフトウェア。ホームオートメーション用のようだけど,博物館の位置情報把握や,病院の患者位置把握など,,行動測定に実に多くの需要がある。身につけておいて損はない。
ESPresence 
HomeAutomationに有用であるらしい
ESPResenseで実現する低コストスマートホーム

こちらのほうが詳しいかもしれない。

K1_250705

神谷&重田により設計・生産された心電図計。ソフトウェアが難点であったので,Arduino側とサーバー側,双方を再構成してみた。ハードウェアは非常に優れており,ECGは大変美しく測定できる。
Arduino側
マルチスレッド化してコア1が波形処理,コア2がHTTP通信を担当することに。
NTPを用いて時刻を取得,IBIのタイムスタンプをms単位で記録することに。
コードをメインモジュールとサブモジュールに分割
HTTP-ECG_N1_03が最新版。
サーバー側(アクセスはここから)
4モジュール同時表示&データ回収に対応。
グラフの再描画は5秒間隔とし,グラフのリフレッシュはAJAXで行うことに。
指定した時間帯のIBIを狙って表示&取得することもできる。
今後の展開
・本体でRMSSDなどのHRVを計算し,フィードバックできるようにする
・本体フルカラーLEDを心拍に合わせて光らせる
・モードを切り替えるとUDPで心拍波形を受け取れるようにする
などの機能を追加したい。

<?php
// showECG.php
// Auto‐Refresh Start/Stop & AJAX 更新版 ECG IBI Plot(Date/EndTime セッションクッキー対応)

/* ───── CONFIGURATION ───── */
$allModules = ['ECG01','ECG02','ECG03','ECG04'];
$dataDir    = __DIR__.'/data/';
$rootDir    = __DIR__.'/';

/* ───── GET / COOKIE / DEFAULTS ───── */
// ★ まず Date/time(EndTime)をクッキー→GET→現在時刻の順で取得
$date = $_COOKIE['date'] ?? ($_GET['date'] ?? date('Ymd'));
$time = $_COOKIE['time'] ?? ($_GET['time'] ?? date('His'));

$length = isset($_GET['length']) ? intval($_GET['length']) : 600;
$min    = isset($_GET['min']) ? intval($_GET['min']) : ($_COOKIE['min'] ?? 500);
$max    = isset($_GET['max']) ? intval($_GET['max']) : ($_COOKIE['max'] ?? 1500);

/* ───── バリデーション & Cookie 保存 ───── */
if (!preg_match('/^\d{8}$/',$date)) $date = date('Ymd');
if (!preg_match('/^\d{6}$/',$time)) $time = date('His');
if ($length < 1)  $length = 600;
if ($min < 0)     $min = 0;
if ($max <= $min) $max = $min + 100;

// ★ Date/time をセッションクッキー(有効期限省略)で保存
setcookie('date', $date);
setcookie('time', $time);
// min/max は従来どおり 1 年有効
if (isset($_GET['min'])) setcookie('min', $min, time()+86400*365,'/');
if (isset($_GET['max'])) setcookie('max', $max, time()+86400*365,'/');

/* ───── Module1–4 入力処理(変更なし) ───── */
if (isset($_GET['mod1'])||isset($_GET['mod2'])||isset($_GET['mod3'])||isset($_GET['mod4'])) {
  $raw=[];
  for($i=1;$i<=4;$i++){
    $k="mod{$i}";
    if(!empty($_GET[$k]) && in_array($_GET[$k],$allModules,true)) $raw[]=$_GET[$k];
  }
  $mods=array_slice(array_unique($raw),0,4);
  setcookie('modules',implode(',',$mods),time()+86400*365,'/');
}elseif(!empty($_COOKIE['modules'])){
  $mods=array_values(array_intersect($allModules,explode(',',$_COOKIE['modules'])));
}else $mods=$allModules;
if(empty($mods)) $mods=$allModules;

/* ───── JSON エンドポイント (AJAX) ───── */
if(isset($_GET['ajax'])&&$_GET['ajax']==='1'){
  header('Content-Type: application/json');

  // EndTime 決定
  if(isset($_GET['now']) && $_GET['now']==='1'){
    $time=date('His');                // 現在時刻
  }elseif(isset($_GET['time']) && preg_match('/^\d{6}$/',$_GET['time'])){
    $time=$_GET['time'];              // 手入力時刻
  } // else: 上部で決定した $time をそのまま使用

  // ★ ajax でも最新 time をクッキー更新(セッション)
  setcookie('time', $time);

  $targetSec=intval(substr($time,0,2))*3600
            +intval(substr($time,2,2))*60
            +intval(substr($time,4,2));
  $fromSec=$targetSec-$length;

  $series=[];
  foreach($mods as $mod){
    $csv="{$mod}_{$date}.csv";
    $file=is_readable($dataDir.$csv)?$dataDir.$csv:(is_readable($rootDir.$csv)?$rootDir.$csv:null);
    $pts=[];
    if($file && ($fh=fopen($file,'r'))){
      while(($L=fgetcsv($fh))!==false){
        if(count($L)<2) continue;
        [$ts,$ibi]=$L;
        if(preg_match('/^(\d{8})_(\d{2})(\d{2})(\d{2})_(\d{3})$/',$ts,$m)){
          [$h,$i,$s,$ms]=$m;
          $h=$m[2];$i=$m[3];$s=$m[4];$ms=$m[5];
          $sec=intval($h)*3600+intval($i)*60+intval($s);
          if($sec>=$fromSec && $sec<=$targetSec){
            $dt=DateTime::createFromFormat('YmdHisv',$m[1].$h.$i.$s.$ms);
            $epoch=((int)$dt->format('U'))*1000+intval($ms);
            $pts[]=['ts_ms'=>$epoch,'y'=>floatval($ibi)];
          }
        }
      }
      fclose($fh);
    }
    $series[$mod]=$pts;
  }

  echo json_encode([
    'date'=>$date,'time'=>$time,'length'=>$length,
    'min'=>$min,'max'=>$max,'mods'=>$mods,'series'=>$series
  ]);
  exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>K1 IBI Plot</title>
<script src="https://www.gstatic.com/charts/loader.js"></script>
<style>
 body{font-family:sans-serif;margin:2em;}
 form{margin-bottom:1em;}
 .row{display:flex;align-items:center;margin-bottom:0.5em;}
 .row input{width:6em;margin-right:1em;}
 button{margin-right:1em;}
 .textarea-container{display:flex;gap:1em;margin-top:1em;}
 .textarea-container textarea{flex:1;height:150px;}
</style>
</head>
<body>
<h2 id="pageTitle">ECG IBI Plot (refresh)</h2>

<form id="cfgForm">
  <div class="row">
    Date:     <input name="date" value="<?php echo $date ?>">
    EndTime:  <input name="time" value="<?php echo $time ?>" id="endTimeField">
    Length:   <input name="length" value="<?php echo $length ?>">
  </div>
  <div class="row">
    Min: <input name="min" value="<?php echo $min ?>">
    Max: <input name="max" value="<?php echo $max ?>">
  </div>
  <div class="row">
    1: <input name="mod1" value="<?php echo $mods[0]??'' ?>">
    2: <input name="mod2" value="<?php echo $mods[1]??'' ?>">
    3: <input name="mod3" value="<?php echo $mods[2]??'' ?>">
    4: <input name="mod4" value="<?php echo $mods[3]??'' ?>">
  </div>
  <button type="button" id="startBtn">Start Auto-Refresh</button>
  <button type="button" id="stopBtn">Stop Auto-Refresh</button>
  <button type="button" id="applyBtn">Apply</button>
</form>

<div id="chart_div" style="width:100%;height:450px;"></div>

<div class="textarea-container">
  <textarea name="text1" placeholder="Module1"></textarea>
  <textarea name="text2" placeholder="Module2"></textarea>
  <textarea name="text3" placeholder="Module3"></textarea>
  <textarea name="text4" placeholder="Module4"></textarea>
</div>

<script>
google.charts.load('current',{packages:['corechart']});
let chart,dataTable,options;
let statusLabel='(refresh)',intervalId=null;

/* === fetch === */
async function fetchData(auto){
  const p=new URLSearchParams(new FormData(document.getElementById('cfgForm')));
  p.set('ajax','1');
  if(auto){ p.set('now','1'); p.delete('time'); }
  const r=await fetch('showECG.php?'+p);
  return r.json();
}

/* === init === */
function initChart(json){
  dataTable=new google.visualization.DataTable();
  dataTable.addColumn('datetime','Time');
  json.mods.forEach(m=>dataTable.addColumn('number',m));
  new google.visualization.DateFormat({pattern:'HH:mm:ss'}).format(dataTable,0);

  options={
    title:'',
    hAxis:{title:'Time',format:'HH:mm:ss'},
    vAxis:{title:'IBI (ms)'},
    legend:{position:'right'},
    pointSize:3,
    explorer:{actions:['dragToZoom','rightClickToReset'],axis:'horizontal'}
  };
  chart=new google.visualization.LineChart(document.getElementById('chart_div'));
  updateChart(json);
}

/* === update === */
function updateChart(json){
  document.getElementById('endTimeField').value=json.time;

  const n=dataTable.getNumberOfRows();
  if(n) dataTable.removeRows(0,n);

  const map={};
  json.mods.forEach(m=>{
    json.series[m].forEach(pt=>{
      if(!map[pt.ts_ms]) map[pt.ts_ms]={};
      map[pt.ts_ms][m]=pt.y;
    });
  });

  Object.keys(map).map(Number).sort((a,b)=>a-b).forEach(ts=>{
    const row=[new Date(ts)];
    json.mods.forEach(m=>row.push(map[ts][m]??null));
    dataTable.addRow(row);
  });

  options.title=`${json.mods.join(', ')} – ${json.date} ${json.time} ±${json.length}s ${statusLabel}`;
  options.vAxis.viewWindow={min:json.min,max:json.max};
  document.getElementById('pageTitle').textContent='ECG IBI Plot '+statusLabel;
  chart.draw(dataTable,options);

  const areas=['text1','text2','text3','text4'];
  for(let i=0;i<4;i++){
    const ta=document.getElementsByName(areas[i])[0];
    if(i>=json.mods.length){ ta.value='データがありません'; continue; }
    const mod=json.mods[i];
    const lines=Object.keys(map).map(Number).sort((a,b)=>a-b).flatMap(ts=>{
      const ibi=map[ts][mod];
      if(ibi==null) return [];
      const d=new Date(ts);
      const hh=`${d.getHours()}`.padStart(2,'0');
      const mm=`${d.getMinutes()}`.padStart(2,'0');
      const ss=`${d.getSeconds()}`.padStart(2,'0');
      const ms=`${d.getMilliseconds()}`.padStart(3,'0');
      return `${hh}:${mm}:${ss}.${ms},${ibi}`;
    });
    ta.value=lines.length?lines.join('\n'):'データがありません';
  }
}

/* === control === */
function startAutoRefresh(){
  if(intervalId) return;
  statusLabel='(refresh)';
  fetchData(true).then(process);
  intervalId=setInterval(()=>fetchData(true).then(process),5000);
}
function stopAutoRefresh(){
  if(!intervalId) return;
  clearInterval(intervalId);
  intervalId=null;
  statusLabel='(stop)';
  fetchData(false).then(process);
}
function process(j){ chart?updateChart(j):initChart(j); }

document.getElementById('startBtn').onclick=startAutoRefresh;
document.getElementById('stopBtn').onclick=stopAutoRefresh;
document.getElementById('applyBtn').onclick=()=>{
  stopAutoRefresh();
  fetchData(false).then(process);
};

/* === onload === */
google.charts.setOnLoadCallback(async ()=>{
  const j=await fetchData(true);
  initChart(j);
  startAutoRefresh();
});
</script>
</body>
</html>

(長野先生宛) kodamalabでPython3.13.5を使えるようにしました。

仕事が落ち着いたのでメンタル余剰分でPython 3が使えるように環境構築しました。Web開発もPython (FlaskとかDjangoとか)で行けるかもしれないです(未検証)。

標準環境

さくらレンタルサーバーは標準でPython 2系が使えるようなのですが、今から開発する人はPython 2なんて化石環境使わない。Python 3も使えるらしいですけど、python3コマンド実行してもなんの反応もないので作ることに。

環境構築後

python3コマンドを使えるようにしました。環境構築前にこのコマンドが設定されていないことは確認済みです。
Teratermからkodamalabにログインして”python3 <スクリプト>.py”を実行すればpythonスクリプトを実行できるようになってます。

※コマンド例(test.pyを実行する場合):python3 test.py
python test.pyだとpython 2で実行されるので注意です。

構築手順は「Python3をさくらサーバー(FreeBSD)にインストール」を参照。やってることの概要は以下の通り。

  1. Pythonのソース(tar)をダウンロード
  2. tarを解凍
  3. Cコンパイラ(サーバー標準搭載)を使ってPythonのビルド
  4. Pythonのパスの追加

python環境はhkディレクトリ以下に入っているのでhkディレクトリを消すと使えなくなります。

レンタルサーバーでpython使えるならVPSとか契約しなかったのに。。。

DIY視線計測

EyeTribeは申し分ないが、もう手に入らない。Tobiiはやはりすべてのデータを取得するにはかなり高価な機械とSDKソフトウェアライセンスを入試なければならいようだ。Tobiiのこういうやり方は性に合わない。低価格のソリューションを調べると、C270(2500円くらい)というWebカメラを使い、DIYでアイカメラを作る方法があるようだ。詳しい仕組みや、ソフトウェアのGithubも公開されている。見た目はアレだが、こちらのほうが自分らしいように思う。時間ができたらぜひやってみようと思う。

卒業論文進捗

今回は卒業論文で使用する実験刺激をUnityから360°カメラの映像に変更したため、その撮影を行った報告をします。

撮影場所は都市環境は東京都中央区月島で自然環境の方は埼玉県秩父郡長瀞で撮影を行ってきました。撮影ルートは以下の通りです。それぞれ2箇所10分、10分、10分、7分の撮影を行いました。

撮影時間の関係で実験刺激の提示は最大7分になりそう。どちらも使用する実験刺激の映像は二回目の映像になりそう。

EyetribeUnity250506

EyetribeはずっとProcessingで動かしてきたが,それが足かせとなり込み入った開発が難しい状況にあった。なにせ古いものなので調査も難航し,結局「そんな複雑なものじゃないので,ソケットでデータを受け取るプログラムを作っては?」と言われ,Python版ができた。んならUnity版も作ってもらおうてなことで,Unityへの移行が進んだ。Processing版はなにせパフォーマンスが芳しく無く,これ以上の開発は不毛な印象なので,同じ機能をUnity版に移植した。動画の読み込みが速い,動作がもたつかないなど,余裕がある感じで大変よろしい。オープンキャンパスなど,しばらくはこれで行こうと思う。実行ファイル3は,result内のCSVファイルの頭に「MGAZE_20250506_150602.csv」のようにMをつけることで,男性参加者のデータを青で表示することができる。
実行ファイル1 実行ファイル2 実行ファイル3

一方で,ファッション雑誌の静止画で視線測定をするのが訴求力あるという意見もあり,妙に納得である。実写は版権が難しいので,イラストにしてもらい測るのもありかなと。視線測定よりAIでイラスト生成のほうが楽しかったりして。

using UnityEngine;
using System;
using System.Net.Sockets;
using System.IO;
using System.Threading;

// JSON マッピング用クラス
[Serializable]
public class Avg { public float x; public float y; }
[Serializable]
public class Frame { public Avg avg; public bool fix; public int state; }
[Serializable]
public class Values { public Frame frame; }
[Serializable]
public class TrackerMessage { public Values values; }

public class EyeTribeTCP : MonoBehaviour
{
    private const string HOST = "127.0.0.1";
    private const int PORT = 6555;
    private const string REQ_PUSH = "{\"category\":\"tracker\",\"request\":\"set\",\"values\":{\"push\":true,\"version\":1}}\n";
    private const string REQ_HEARTBEAT = "{\"category\":\"heartbeat\",\"request\":null}\n";

    private TcpClient client;
    private NetworkStream stream;
    private StreamReader reader;
    private Thread heartbeatThread;
    private Thread receiveThread;
    private bool running;

    [Header("=== Gaze Data (Read-Only) ===")]
    [Tooltip("現在の注視座標X")]
    public float GazeX;
    [Tooltip("現在の注視座標Y")]
    public float GazeY;
    [Tooltip("現在の注視固定状態")]
    public bool IsFixated;
    [Tooltip("現在のトラッキング状態")]
    public bool IsTracking;
    [Tooltip("デバッグモード")]
    public bool IsDebugmode;


    void Start()
    {
        Connect();
    }

    void Connect()
    {
        try
        {
            client = new TcpClient(HOST, PORT);
            stream = client.GetStream();
            reader = new StreamReader(stream);

            Send(REQ_PUSH);

            running = true;

            heartbeatThread = new Thread(() =>
            {
                while (running && stream != null)
                {
                    Send(REQ_HEARTBEAT);
                    Thread.Sleep(250);
                }
            });
            heartbeatThread.IsBackground = true;
            heartbeatThread.Start();

            receiveThread = new Thread(ReceiveLoop);
            receiveThread.IsBackground = true;
            receiveThread.Start();

            Debug.Log("EyeTribe: 接続完了し、スレッドを開始しました。");
        }
        catch (Exception e)
        {
            Debug.LogError($"EyeTribe 接続失敗: {e.Message}");
        }
    }

    private void Send(string message)
    {
        if (stream == null) return;
        try
        {
            var buf = System.Text.Encoding.UTF8.GetBytes(message);
            stream.Write(buf, 0, buf.Length);
        }
        catch (Exception e)
        {
            Debug.LogWarning($"Send エラー: {e.Message}");
        }
    }

    void ReceiveLoop()
    {
        try
        {
            while (running && reader != null)
            {
                string line = reader.ReadLine();
                if (string.IsNullOrEmpty(line))
                    continue;

                try
                {
                    var msg = JsonUtility.FromJson<TrackerMessage>(line);
                    if (msg?.values?.frame != null)
                    {
                        var f = msg.values.frame;
                        bool tracking = (f.state & 0x1) != 0;

                        // フィールドにセット(Inspector に反映される)
                        GazeX = f.avg.x;
                        GazeY = f.avg.y;
                        IsFixated = f.fix;
                        IsTracking = tracking;

                        if (IsDebugmode) {
                            Debug.Log($"x={GazeX:F3}, y={GazeY:F3}, fix={IsFixated}, tracking={IsTracking}");
                        }
                    }
                }
                catch (Exception ex)
                {
                    Debug.LogWarning($"JSON 解析エラー: {ex.Message} | raw: {line}");
                }
            }
        }
        catch (Exception e)
        {
            Debug.LogWarning($"ReceiveLoop 終了: {e.Message}");
        }
    }

    void OnDestroy()
    {
        running = false;
        heartbeatThread?.Join(500);
        receiveThread?.Join(500);
        reader?.Close();
        stream?.Close();
        client?.Close();
        Debug.Log("EyeTribe: 接続を閉じました。");
    }
}

川。ー卒論の刺激づくりについてー

今回は就活のポートフォリオの作成のため、自分が作成した風景のまとめをします。

上の画像は、2024年の12月頃に作成していた埼玉県長瀞市の川沿いをunityで作成したものです。この頃はunityの標準機能とNatureManufactureとRiverAutoMaterialとVegetationStudioを使用して長瀞を作成していた。今見るとこっちのほうがきれいに見えてくる・・・

こっちの画像はいま現段階でMicroverseを使用して作成している長瀞だ。こっちのほうが環境も大きくいいと感じるが、細部をまだ凝る必要がある。また、見どころがあまりないのが困ったところ。以上が現状の報告である。

ブラウザで認識機能

コミュニケーションに必要な認識はブラウザで行う時代に!
WebGL版AIエージェントの音声認識に試用しているWebGL Speechというアセットが動作しなくなり(ブラウザのセキュリティ強化のせい?)音声会話に支障が出ている。WebGL版OpenGLの遅さや、Web版Mediapipeの充実を鑑みるに、「CGなどUnityでしかできないものはWebGLに、音声や表情など各種認識はブラウザに」という機能の棲み分けが今後一般的になるのではないか!?コミュニケーションに必要な認識能力はブラウザに統合されるという時代の流れがあるとの読みだ。

音声認識
ちょいとo4miniに作成してもらう。「Web Speech API(webkitSpeechRecognition)でブラウザに表示したテキストエリアに、発話した内容を表示するデモプログラムを作成してくれませんか?Javascriptで」完成したデモは十分すぎる仕上がりだ。音声認識はブラウザで受けて、それをWebGLアプリで吸い上げる作戦だ。

表情認識
表情認識は、Googleのデモをベースにやはりo4miniに作成してもらう。こちらは少々苦戦したが、左右反転なしでFaceMeshも表示されるデモを作ってもらえた。

なによりも朗報なのは、iPadでも動作するということだろうか。


音声と表情をブラウザで同時に認識する
上記で作成したJSは,Unityのプラグインとして記述し,WebGLビルドに含めることができる。晴れて「ブラウザで認識した音声+表情をWebGLへ送る」アイデアが実現した。o3様々だ。しかし,デバッグするためにビルドする必要があるという問題が残る。Unityエディタで開発中は,ブラウザで認識した情報を受取ることはできないの?o3曰く,Websocketを使えば良い。そのためには,Websocket-sharpが必要とのこと。これで開発中も音声+表情認識が無事に使えるようになった。やった!!o3の使用制限はあと24回になっちゃったけど。

音声認識も表情認識も行うデモ
websocket-sharp


ブラウザでFER2013を動かす
MediaPipeがJSで動いて、その結果をUnityWebGLに呼び込めるなら、OpenCVで使っていたFER2013モデルも読み込めないのだろうか?これは、TensorFlows.jsというものがあり、そのラッパーであるface-api.jsを利用すれば良いようだ。モデルをDLして配置して、エラーを見ながらo4-miniに相談すると、無事ブラウザで動かすことに成功した。楽しい~!これもアリですね。
一連の会話
ブラウザでFER2013動かすデモ

患者セリフ集

理学療法中の患者さんが抱えやすい「つらさ」「不安」「あきらめ」「無力感」「怒り」の5つの感情に注目し、実際に口にしそうな言葉を集めました。AIに感情表現を学習させるための素材として作成しましたが、同時に患者さんの心の声に耳を傾ける手がかりにもなればと思います。


■ つらさ(身体的・精神的な苦痛)
毎回痛みに耐えるだけで、ぐったりしてしまうんです。
やるたびに痛みが増していく気がして、怖いです。
「慣れれば楽になりますよ」って言われても、全然楽にならない。
少し動かしただけで、こんなにつらいなんて思わなかった。
人と話す気力も、笑う余裕も、もう残ってないです。
寝ても起きても痛くて、逃げ場がありません。
リハビリに行くのが、だんだん怖くなってきました。
体だけじゃなく、心まで壊れていくような気がしてます。


■ 不安(先が見えないことへの恐れや心配)
もしこのまま治らなかったら…って考えると夜も眠れません。
退院後に一人で生活できる気がしなくて、不安で仕方ないです。
「もう少しですね」って言葉が、逆にプレッシャーになります。
この先、自分に何ができるのかまったく見えないんです。
良くなるって信じたいけど、実感がないと不安になります。
周りに「がんばれ」って言われるほど、不安だけが大きくなります。
わたし、ちゃんと前に進めてるんでしょうか。
毎日が不安の塊で、息苦しくなります。


■ あきらめ(希望を失ったり、努力する意味を見いだせない状態)
もう何をしても、変わらない気がしてきました。
リハビリに行く意味、最近はわからなくなってます。
回復を信じたい気持ちが、日に日に薄れていきます。
毎日同じ訓練をしてるだけで、空虚な気持ちになります。
頑張っても報われないなら、もう頑張らなくてもいいかなって。
期待するたびに、裏切られてる気がするんです。
希望を持つのが怖くなってきました。
「いつかきっと良くなるよ」って言葉が、ただの慰めにしか聞こえません。


■ 無力感(できない・役に立てない自分への落胆)
自分の体が、自分のものじゃないみたいに感じます。
周りが助けてくれてるのに、何もできない自分がつらいです。
手すりにすがって立つだけなのに、なぜこんなにもつらいんでしょう。
日常のちょっとしたことさえできなくなってしまって、情けないです。
リハビリ中に「またダメだった」と思う瞬間が、一番つらいです。
自分がどんどん役立たずになっていく気がして、涙が出ます。
できてたことができなくなっていくのを、ただ見てるしかないんです。
これ以上、家族に迷惑かけたくないけど、できることが何もない。


■ 怒り(他者や状況、自分自身への苛立ちや怒り)
なんで自分だけが、こんな目にあうんですか。
痛いって何度も言ってるのに、わかってもらえないのが腹立たしいです。
「がんばれば良くなる」って簡単に言わないでほしい。
なんであの日、怪我なんかしたんだろうって、自分が憎くなる。
他人事みたいな口ぶりで励まされても、正直ムカつきます。
周りは普通に歩いてるのに、自分だけ…って考えると悔しくて涙が出ます。
痛みが続くたびに、どうしてこんな体になったんだって怒りが湧いてきます。
もうこれ以上、「もうちょっとだけ」とか言われたくないんです!

FreetokenWithOpenAI

Geminiで会話履歴をPrefsに覚えさせ、会話を続けられるようにしていたが、やはり1日50requestで「Gemini API Error: HTTP/1.1 429 Too Many Requests」となる事が判明・・・やはりOpenAIで作ったほうが得策か。。。そういえば、無料トークンの話あったよな。
OpenAI API を毎日無料で 100 万トークン使えるらしい?によれば、条件をみたすと100万トークン毎日無料で使えるようになるという。本当に?!ブログに従って無料トークン用のAPIキーを作成してみる。Pythonで試してみようか・・・あ、動きますね!これなら無駄に会話を続けてもダメージは最小限に抑えられる。これで開発しようか。