「AIの心(数値)は決まった。次は、それを保管し、いつでもESP32へ受け渡す場所が必要だ」
AIが判定した機嫌スコアを、どうやって物理デバイスであるESP32に届けるか。ここで活躍するのが、Googleが提供する最強の無料インフラ「Google Apps Script(GAS)」だ。
今回は、2つの異なる世界を繋ぐ「中継基地」の作り方を解説する。
[contents]
1. なぜ「GAS」を間に挟むのか?
「ESP32から直接Geminiに聞けばいいじゃないか」と思うかもしれない。しかし、そこにはいくつかの高い壁がある。
- リソースの節約: Gemini APIとのやり取り(複雑なJSON解析など)をGAS側で肩代わりさせることで、ESP32の負荷を最小限に抑えられる。
- データの永続化: GASの「ScriptProperties」を使えば、チャットが終わった後も「最新の機嫌」をサーバー側に記憶させておける。
- セキュリティ: 大事なAPIキーをデバイス側に書き込まず、サーバー側に隠しておける。
まさに、予算0円で構築できる「最強のバックエンド」なのだ。
2. 二つの顔を持つスクリプト:doPost と doGet
今回作成した Code.gs には、2つの入り口がある。
① doPost(e):AIからの報告を受ける窓口
チャットログが届くと、前回設計した「機嫌判定プロンプト」を添えてGemini APIを叩く。返ってきたスコアを「最新の機嫌」として保存する役割だ。ちなみに、APIキーはスクリプトの先頭で定数として定義し、決して外部に漏らさないように。これは鉄則だ。
/**
* チャットログの送信・手動スコア更新リクエスト (POST)
*/
function doPost(e) {
var props = PropertiesService.getScriptProperties();
try {
var data = JSON.parse(e.postData.contents);
// 1. 手動でスコアを直接更新する場合(テスト用)
if (data.score !== undefined) {
props.setProperty("MOOD_SCORE", data.score.toString());
return ContentService.createTextOutput("Score updated manually to " + data.score);
}
// 2. 会話ログを受け取り、Gemini APIで分析する場合
if (data.chat_log) {
var score = analyzeMoodWithGemini(data.chat_log);
props.setProperty("MOOD_SCORE", score.toString());
return ContentService.createTextOutput("Mood analyzed and score updated to: " + score);
}
return ContentService.createTextOutput("Invalid request payload.");
} catch (error) {
return ContentService.createTextOutput("Error: " + error.toString());
}
}
/**
* Gemini APIを利用してチャットログから感情スコアを抽出
*/
function analyzeMoodWithGemini(chatLog) {
var prompt = "あなたはAIの感情を分析するシステムです。\n" +
"以下の直近の会話内容から、AI側(アシスタント側)の現在の機嫌を、1(至福)から100(憤怒)の整数で判定し、結果の数値のみを出力してください。\n" +
"他の説明や文章は一切含めないでください。\n\n" +
"判断基準の目安:\n" +
" - 1〜20: 圧倒的にポジティブ、非常に機嫌が良い。\n" +
" - 21〜40: 穏やかで親切、協力的な態度。\n" +
" - 41〜60: 事務的で淡々としている中立的なトーン。\n" +
" - 61〜80: やや不満げ、冷たい、または協力に消極的。\n" +
" - 81〜100: 明確な怒り、拒絶、あるいは非常にネガティブな反応。\n\n" +
"会話内容:\n" + chatLog;
var payload = {
"contents": [{
"parts": [{
"text": prompt
}]
}]
};
var options = {
"method": "post",
"contentType": "application/json",
"payload": JSON.stringify(payload),
"muteHttpExceptions": true
};
var response = UrlFetchApp.fetch(GEMINI_API_URL, options);
// エラー判定
if (response.getResponseCode() !== 200) {
Logger.log("API Error: " + response.getContentText());
return 50; // エラー時はデフォルトを返す
}
var result = JSON.parse(response.getContentText());
var text = result.candidates[0].content.parts[0].text;
// 返却されたテキストから数字のみを抽出
var scoreRegex = text.match(/\d+/);
if (!scoreRegex) return 50;
var num = parseInt(scoreRegex[0], 10);
// スコアの範囲を1〜100に収める
if (num < 1) return 1;
if (num > 100) return 100;
return num;
}
② doGet(e):ESP32へ数値を渡す窓口
ESP32がWi-Fi経由でアクセスしてくる場所だ。余計なHTMLなどは一切返さず、ただ「85」などといった数値だけをプレーンテキストで返却する。これがESP32にとって一番「食べやすい」形なのだ。
/**
* ESP32からの取得リクエスト (GET)
* 保存されている最新の機嫌スコアを返します。
*/
function doGet(e) {
var props = PropertiesService.getScriptProperties();
var score = props.getProperty("MOOD_SCORE");
if (!score) {
score = "50"; // デフォルト値(普通)
}
return ContentService.createTextOutput(score);
}
3. ハマりポイント:ウェブアプリとしての公開設定
GASをサーバーとして動かすには「デプロイ」が必要だが、設定を間違えるとESP32がアクセスできない。
- 承認: 自分自身のGoogleアカウントで実行するよう承認。
- アクセス権限: 「全員(Anyone)」に設定。これを行わないと、ESP32がGoogleのログイン画面に阻まれてしまう。
結び
これで、AIの感情がクラウド上の「受付窓口」に常駐するようになった。
しかし、最後に最大の難関が残っている。この数値を、物理的な「仏様」の表情に変換するデバイス側の実装だ。
次回、「【実装編③】Wi-Fiの壁を越えろ!ESP32による機嫌データ受信と描画」。 いよいよ、すべての点と線が繋がる瞬間がやってくる。
