diff --git a/doc/basic_usage.md b/doc/basic_usage.md index eb2bdf6..bd89f9a 100644 --- a/doc/basic_usage.md +++ b/doc/basic_usage.md @@ -11,8 +11,9 @@ robo8080さんの[AIスタックチャン](https://github.com/robo8080/AI_StackC - [2. 設定、ビルド手順](#2-設定ビルド手順) - [2.1. YAMLによる初期設定](#21-yamlによる初期設定) - [2.2. ビルド&書き込み](#22-ビルド書き込み) -- [3. パーソナライズ](#3-パーソナライズ) - - [3.1. メモリー(長期記憶)について](#31-メモリー長期記憶について) +- [3. 使い方](#3-使い方) + - [3.1. 会話](#31-会話) + - [3.2. パーソナライズ](#32-パーソナライズ) ## 1. 利用可能なAIサービス 会話に必要な各種AIサービスの対応状況を示します。 @@ -207,7 +208,15 @@ git clone https://github.com/ronron-gh/AI_StackChan_Ex.git ![](../images/build_and_flash.png) -## 3. パーソナライズ +## 3. 使い方 +### 3.1. 会話 +M5Coreを起動してアバターが表示された後、アバターの額のあたりをタッチすると録音が開始するので話しかけてください。録音時間は約7秒です。 + +> AtomS3Rは画面自体が物理ボタンになっているため、画面中央を少し強めに押し込んでください。または、次の動画のようにボディーのどこかをダブルタップすることでも録音開始できます(ダブルタップは初期状態は無効になっています。platformio.iniの[env:m5stack-atoms3r]セクション内の-DENABLE_TAP_DETECTのコメントアウトを解除してビルドすることで有効化できます)。 +> ![](../images/double_tap.gif) + + +### 3.2. パーソナライズ カスタム指示(いわゆるロール)、及びメモリー(長期記憶)により、AI会話機能をユーザーの属性に合わせてカスタマイズすることができます。 PCやスマートフォンのWebブラウザで http://(スタックチャンのIPアドレス) にアクセスすると次のような設定画面が開きます。(IPアドレスは起動時の画面に表示されます。また、Core2/CoreS3はCボタンまたはLCD右端をタッチするとアクセス用のQRコードが表示されます。) @@ -215,7 +224,7 @@ PCやスマートフォンのWebブラウザで http://(スタックチャンの ![](../images/Personalize.png) -### 3.1. メモリー(長期記憶)について +**〇 メモリー(長期記憶)について** メモリーを有効にするには SDカードの/app/AiStackChanEx/SC_ExConfig.yaml で enableMemory を true に設定してください。 > 現在、メモリーに対応しているLLMは、ChatGPT(Realtime API含む)、Gemini Liveです。 diff --git a/firmware/platformio.ini b/firmware/platformio.ini index 201e834..5cf910c 100644 --- a/firmware/platformio.ini +++ b/firmware/platformio.ini @@ -59,6 +59,7 @@ build_flags = -DENABLE_WAKEWORD -DARDUINOJSON_DEFAULT_NESTING_LIMIT=100 ;-DCORE_DEBUG_LEVEL=5 ;Verbose + ;-DENABLE_TAP_DETECT ; 加速度センサによるダブルタップの検出 lib_deps = m5stack/M5Unified @ 0.1.17 @@ -119,6 +120,7 @@ build_flags = -DARDUINO_M5STACK_CORES3 -DENABLE_WAKEWORD -DARDUINOJSON_DEFAULT_NESTING_LIMIT=100 + ;-DENABLE_TAP_DETECT ; 加速度センサによるダブルタップの検出 (※CoreS3-SEは加速度センサ非搭載) monitor_speed = 115200 upload_speed = 1500000 lib_deps = @@ -180,6 +182,7 @@ build_flags = ;-DENABLE_WAKEWORD ; ウェイクワードは未対応 -DARDUINOJSON_DEFAULT_NESTING_LIMIT=100 ;-DCAT_FACE + ;-DENABLE_TAP_DETECT ; 加速度センサによるダブルタップの検出 lib_deps = m5stack/M5Unified @ 0.2.7 diff --git a/firmware/src/driver/TapDetect.cpp b/firmware/src/driver/TapDetect.cpp new file mode 100644 index 0000000..c6cb5a6 --- /dev/null +++ b/firmware/src/driver/TapDetect.cpp @@ -0,0 +1,177 @@ +#if defined(ENABLE_TAP_DETECT) +#include +#include +#include "TapDetect.h" + + +#define ACCEL_DIM (3) +TaskHandle_t taskHandle_doubleTapDetect; +bool doubleTapDetected = false; +bool isDoubleTapDetectionRunning = true; +float firstAcc[ACCEL_DIM] = {0.0, 0.0, 0.0}; +float detectedAcc[ACCEL_DIM] = {0.0, 0.0, 0.0}; +float firstNorm = 0.0; + +// --------------------------- +// 感度等の調整パラメータ +// --------------------------- +// タップを検出する加速度の閾値 +const float TAP_DELTA = 0.4f; +// タップの回数(ダブルタップを検出したい場合は2とする) +const float TAP_COUNT = 2; +// ハイパスフィルタが安定するまでのカウント数 +const float FIRST_NOISE_COUNT = 10; +// 2回目のタップが同じ方向からのタップかどうかを判定するためのコサイン類似度の閾値 +//(1.0から-1.0の間で設定。1.0に近づくほど類似度が高い(判定は厳しくなる)。-1.0に近づくと真逆の方向) +const float COS_SIMILAR = 0.9; + + +// --------------------------- +// ダブルタップ検出タスク +// --------------------------- +void doubleTapDetectTask(void *arg) { + Serial.println("Double tap detection task created"); + + // 注意: M5.IMU.getAccel の API は環境により差があるため、 + // ビルドエラーが出た場合は M5.Imu.getAccel 等に置き換えてください。 + + unsigned long first_tap_time = 0; + int tap_count = 0; + float accel[ACCEL_DIM] = {0.0, 0.0, 0.0}; + float gravity[ACCEL_DIM] = {0.0, 0.0, 0.0}; + float norm = 0.0; + bool ok = false; + int firstNoiseCount = 0; + + while(1){ + // try common IMU API (adjust if your M5 library uses a different name) + #if defined(M5_IMU) + ok = M5.IMU.getAccel(&ax, &ay, &az); + #else + // Fallback attempt (some versions use Imu) + ok = M5.Imu.getAccel(&accel[0], &accel[1], &accel[2]); + #endif + + if (ok) { + unsigned long now = millis(); + if(now - first_tap_time > 700){ + tap_count = 0; + first_tap_time = 0; + } + + // ハイパスフィルタで重力を除去 + // + const float ALPHA = 0.8; + // ローパスフィルタで重力値を抽出 + gravity[0] = ALPHA * gravity[0] + (1 - ALPHA) * accel[0]; + gravity[1] = ALPHA * gravity[1] + (1 - ALPHA) * accel[1]; + gravity[2] = ALPHA * gravity[2] + (1 - ALPHA) * accel[2]; + // 重力を除去 + accel[0] = accel[0] - gravity[0]; + accel[1] = accel[1] - gravity[1]; + accel[2] = accel[2] - gravity[2]; + // ベクトルの大きさを計算 + norm = sqrt(accel[0] * accel[0] + accel[1] * accel[1] + accel[2] * accel[2]); + + // ハイパスフィルタが安定するまで計算結果を破棄 + if(firstNoiseCount < FIRST_NOISE_COUNT){ + firstNoiseCount ++; + delay(10); // [ms] + continue; + } + + // 定期的に加速度値を出力(デバッグ用) +#if 0 + static unsigned long last_print = 0; + if(now - last_print > 100) { + Serial.printf("IMU ax=%.3f ay=%.3f az=%.3f norm=%.3f\n", accel[0], accel[1], accel[2], norm); + last_print = now; + } +#endif + + if (norm > TAP_DELTA) { + // タップあり + float cos = 1.0; + + if(tap_count == 0){ + // 1回目のタップ + for(int i = 0; i < ACCEL_DIM; i++){ + firstAcc[i] = accel[i]; + firstNorm = norm; + first_tap_time = now; + } + }else{ + // 2回目以降は同じ方向のタップのみカウントするためにコサイン類似度を計算 + float innPro = firstAcc[0] * accel[0] + firstAcc[1] * accel[1] + firstAcc[2] * accel[2]; + cos = innPro / (firstNorm * norm); + } + + Serial.printf("Tap detected. ax=%.3f ay=%.3f az=%.3f norm=%.3f cos=%.3f\n", accel[0], accel[1], accel[2], norm, cos); + if(now - first_tap_time <= 700) { // 700ms以内なら連続タップ + if(cos > COS_SIMILAR){ // コサイン類似度で同じ方向かを判定 + tap_count ++; + } + Serial.printf("tap_count=%d\n", tap_count); + } + } + + if (tap_count >= TAP_COUNT) { + // ダブルタップ検出 + Serial.println("Double tap detected"); + tap_count = 0; + doubleTapDetected = true; + for(int i = 0; i < ACCEL_DIM; i++){ + detectedAcc[i] = firstAcc[i]; + } + isDoubleTapDetectionRunning = false; + } + } else { + Serial.println("IMU getAccel() returned false or not available"); + } + + // loopタスクから再開要求の通知が来るまで待機 + if(!isDoubleTapDetectionRunning){ + Serial.println("Waiting notify..."); + ulTaskNotifyTake( pdTRUE, portMAX_DELAY ); + isDoubleTapDetectionRunning = true; + firstNoiseCount = 0; + } + + delay(10); // [ms] + } +} + +// --------------------------- +// ダブルタップ検出タスクの起動 +// --------------------------- +void invokeDoubleTapDetectTask(void) +{ + xTaskCreate(doubleTapDetectTask, /* Function to implement the task */ + "doubleTapDetectTask", /* Name of the task */ + 3*1024, /* Stack size in words */ + NULL, /* Task input parameter */ + 2, /* Priority of the task */ + &taskHandle_doubleTapDetect); /* Task handle. */ +} + +// --------------------------- +// ダブルタップ検出タスクの停止 +// --------------------------- +void stopDoubleTapDetectTask(void) +{ + if(isDoubleTapDetectionRunning){ + isDoubleTapDetectionRunning = false; + } +} + +// --------------------------- +// ダブルタップ検出タスクの再開 +// --------------------------- +void resumeDoubleTapDetectTask(void) +{ + if(!isDoubleTapDetectionRunning){ + xTaskNotifyGive(taskHandle_doubleTapDetect); + } +} + +#endif //ENABLE_TAP_DETECT \ No newline at end of file diff --git a/firmware/src/driver/TapDetect.h b/firmware/src/driver/TapDetect.h new file mode 100644 index 0000000..84df22d --- /dev/null +++ b/firmware/src/driver/TapDetect.h @@ -0,0 +1,16 @@ +#if defined(ENABLE_TAP_DETECT) + +#ifndef _TAP_DETECT_H +#define _TAP_DETECT_H + + +extern bool doubleTapDetected; +//extern bool isDoubleTapDetectionRunning; +extern float detectedAcc[]; + +void invokeDoubleTapDetectTask(void); +void stopDoubleTapDetectTask(void); +void resumeDoubleTapDetectTask(void); + +#endif //_TAP_DETECT_H +#endif //ENABLE_TAP_DETECT \ No newline at end of file diff --git a/firmware/src/llm/LLMBase.h b/firmware/src/llm/LLMBase.h index 19b1c31..c2641e2 100644 --- a/firmware/src/llm/LLMBase.h +++ b/firmware/src/llm/LLMBase.h @@ -67,6 +67,7 @@ class LLMBase{ String getOutputText(); int getOutputTextQueueSize(); void setSpeaking(bool _speaking){ speaking = _speaking; }; + bool isSpeaking(void){ return speaking; }; int search_delimiter(String& text); }; diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 2283526..aa67a25 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -19,6 +19,7 @@ #include "mod/VolumeSetting/VolumeSettingMod.h" #include "driver/PlayMP3.h" //lipSync +#include "driver/TapDetect.h" #include #include @@ -168,6 +169,7 @@ void battery_check(void *args) { } } + //void Wifi_setup() { bool Wifi_connection_check() { unsigned long start_millis = millis(); @@ -273,7 +275,7 @@ void sw_tone() { M5.Mic.end(); M5.Speaker.begin(); - + delay(300); // AtomS3Rはこのdelayがないと鳴らないときがある M5.Speaker.tone(1000, 100); delay(500); @@ -445,6 +447,10 @@ void setup() avatar.set_isSubWindowEnable(true); #endif +#if defined(ENABLE_TAP_DETECT) + invokeDoubleTapDetectTask(); +#endif + //init_watchdog(); //ヒープメモリ残量確認(デバッグ用) @@ -518,6 +524,21 @@ void loop() } #endif +#if defined(ENABLE_TAP_DETECT) + if(doubleTapDetected){ + Serial.println("loop(): Double tap detected"); + mod->doubleTapped(detectedAcc[0], detectedAcc[1], detectedAcc[2]); + doubleTapDetected = false; + } + + // Modで重い処理をしている場合はダブルタップ検出を停止する + if(mod->isBusy()){ + stopDoubleTapDetectTask(); + }else{ + resumeDoubleTapDetectTask(); + } +#endif + if(!isOffline){ web_server_handle_client(); ftpSrv.handleFTP(); diff --git a/firmware/src/mod/AiStackChan/AiStackChanMod.cpp b/firmware/src/mod/AiStackChan/AiStackChanMod.cpp index 94f48da..83a9936 100644 --- a/firmware/src/mod/AiStackChan/AiStackChanMod.cpp +++ b/firmware/src/mod/AiStackChan/AiStackChanMod.cpp @@ -281,6 +281,15 @@ void AiStackChanMod::display_touched(int16_t x, int16_t y) } +void AiStackChanMod::doubleTapped(float ax, float ay, float az) +{ + Serial.printf("Mod double tapped. ax=%.3f ay=%.3f az=%.3f\n", ax, ay, az); +#if defined(ARDUINO_M5STACK_ATOMS3R) + sw_tone(); + STT_ChatGPT(); +#endif +} + void AiStackChanMod::idle(void) { diff --git a/firmware/src/mod/AiStackChan/AiStackChanMod.h b/firmware/src/mod/AiStackChan/AiStackChanMod.h index ae1be3e..89ea9a7 100644 --- a/firmware/src/mod/AiStackChan/AiStackChanMod.h +++ b/firmware/src/mod/AiStackChan/AiStackChanMod.h @@ -25,6 +25,7 @@ class AiStackChanMod: public ModBase{ void btnB_longPressed(void); void btnC_pressed(void); void display_touched(int16_t x, int16_t y); + void doubleTapped(float ax, float ay, float az); // 加速度センサによるダブルタップ検出のコールバック。platformio.iniで-DENABLE_TAP_DETECTを有効にしてください void idle(void); }; diff --git a/firmware/src/mod/AiStackChan/RealtimeAiMod.cpp b/firmware/src/mod/AiStackChan/RealtimeAiMod.cpp index 308f732..e34e565 100644 --- a/firmware/src/mod/AiStackChan/RealtimeAiMod.cpp +++ b/firmware/src/mod/AiStackChan/RealtimeAiMod.cpp @@ -69,11 +69,7 @@ void RealtimeAiMod::btnA_pressed(void) #if defined(ARDUINO_M5STACK_ATOMS3R) Serial.println("Btn A pressed"); sw_tone(); - if(pRtLLM->isRealtimeRecording()){ - pRtLLM->stopRealtimeRecord(); - }else{ - pRtLLM->startRealtimeRecord(); - } + toggleRealtimeRecord(); #endif } @@ -102,11 +98,7 @@ void RealtimeAiMod::display_touched(int16_t x, int16_t y) if (box_stt.contain(x, y)) { sw_tone(); - if(pRtLLM->isRealtimeRecording()){ - pRtLLM->stopRealtimeRecord(); - }else{ - pRtLLM->startRealtimeRecord(); - } + toggleRealtimeRecord(); } #ifdef USE_SERVO if (box_servo.contain(x, y)) @@ -126,6 +118,16 @@ void RealtimeAiMod::display_touched(int16_t x, int16_t y) } +void RealtimeAiMod::doubleTapped(float ax, float ay, float az) +{ + Serial.printf("Mod double tapped. ax=%.3f ay=%.3f az=%.3f\n", ax, ay, az); +#if defined(ARDUINO_M5STACK_ATOMS3R) + sw_tone(); + toggleRealtimeRecord(); +#endif +} + + void RealtimeAiMod::idle(void) { pRtLLM->webSocketProcess(); @@ -184,5 +186,22 @@ void RealtimeAiMod::alarmEventHandler() } +bool RealtimeAiMod::isBusy(void) +{ + if(pRtLLM->isRealtimeRecording() || pRtLLM->isSpeaking()){ + return true; + }else{ + return false; + } +} + +void RealtimeAiMod::toggleRealtimeRecord(void) +{ + if(pRtLLM->isRealtimeRecording()){ + pRtLLM->stopRealtimeRecord(); + }else{ + pRtLLM->startRealtimeRecord(); + } +} #endif //REALTIME_API \ No newline at end of file diff --git a/firmware/src/mod/AiStackChan/RealtimeAiMod.h b/firmware/src/mod/AiStackChan/RealtimeAiMod.h index 0a6bfa9..08f05f5 100644 --- a/firmware/src/mod/AiStackChan/RealtimeAiMod.h +++ b/firmware/src/mod/AiStackChan/RealtimeAiMod.h @@ -35,7 +35,11 @@ class RealtimeAiMod: public ModBase{ void btnB_longPressed(void); void btnC_pressed(void); void display_touched(int16_t x, int16_t y); + void doubleTapped(float ax, float ay, float az); // 加速度センサによるダブルタップ検出のコールバック。platformio.iniで-DENABLE_TAP_DETECTを有効にしてください void idle(void); + bool isBusy(void); + + void toggleRealtimeRecord(void); }; diff --git a/firmware/src/mod/ModBase.h b/firmware/src/mod/ModBase.h index 527babb..a729bab 100644 --- a/firmware/src/mod/ModBase.h +++ b/firmware/src/mod/ModBase.h @@ -41,7 +41,9 @@ class ModBase{ virtual void btnC_pressed(void) {}; virtual void btnC_longPressed(void) {}; virtual void display_touched(int16_t x, int16_t y) {}; + virtual void doubleTapped(float ax, float ay, float az) {}; // 加速度センサによるダブルタップ検出のコールバック。platformio.iniで-DENABLE_TAP_DETECTを有効にしてください virtual void idle(void) {}; + virtual bool isBusy(void) {return false;}; }; diff --git a/images/double_tap.gif b/images/double_tap.gif new file mode 100644 index 0000000..9a03ed4 Binary files /dev/null and b/images/double_tap.gif differ