どんなものを作るか
ボタンを押して話しかけると、数秒後にスピーカーから回答が返ってくる端末です。たとえば店頭で「今日は何時まで開いていますか?」「駐車場はありますか?」と聞かれたときに、店舗の情報をもとに答えます。
スマートスピーカーとの違いは、答える内容を自分で決められることです。営業時間やメニュー、よくある質問だけを答えさせ、それ以外は「スタッフにお声がけください」と返す、といった制御ができます。
全体の仕組み
ESP32 は「録音して送る」「返ってきた音声を流す」だけを担当し、AI とのやり取りは中継サーバーにまとめます。処理の順番は次のとおりです。
- ESP32 がボタンを押している間の音声を録音し、中継サーバーに送る
- 中継サーバーが音声認識 API で文字に起こす
- 店舗情報を添えて対話 API(ChatGPT)に回答を作ってもらう
- 回答の文章を音声合成 API で音声にする
- 音声データを ESP32 に返し、スピーカーで再生する
必要な部品
| 部品 | 用途・選び方 |
|---|---|
| ESP32-S3 開発ボード | 本体。録音データを一時的にためるので、PSRAM 付きのモデルがおすすめです。 |
| INMP441 | I2S 接続のデジタルマイク。ノイズに強く、配線も簡単です。 |
| MAX98357A | I2S 接続のアンプ。デジタルの音声データをそのままスピーカーで鳴らせます。 |
| 小型スピーカー | 4Ω〜8Ω、2〜3W 程度のもの。 |
| USB 電源・ケーブル | 常設するなら、抜けにくい配置と安定した電源を選びます。 |
部品代は合計で数千円程度です。ほかに、OpenAI の API 利用料(使った分だけの従量課金)がかかります。
配線
マイクとアンプはどちらも I2S という方式でつなぎます。ESP32-S3 は I2S を2系統持っているので、録音用と再生用で分けると扱いやすくなります。
ESP32 側のプログラム
Arduino-ESP32(3.x)の ESP_I2S ライブラリには、WAV 形式で録音・再生する関数が用意されています。要点だけを抜き出すと次のようになります。
#include <ESP_I2S.h> // Arduino-ESP32 3.x の I2S ライブラリ
#include <WiFi.h>
#include <HTTPClient.h>
I2SClass mic, spk;
const char* RELAY_URL = "https://<中継サーバー>/talk";
void setup() {
WiFi.begin("SSID", "PASSWORD");
while (WiFi.status() != WL_CONNECTED) delay(200);
mic.setPins(4, 5, -1, 6); // BCLK, WS, DOUT(未使用), DIN
mic.begin(I2S_MODE_STD, 16000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO);
spk.setPins(15, 16, 7); // BCLK, LRC, DOUT
spk.begin(I2S_MODE_STD, 24000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO);
pinMode(0, INPUT_PULLUP); // BOOT ボタン
}
void loop() {
if (digitalRead(0) == HIGH) return; // ボタンが押されるまで待つ
size_t wavSize = 0;
uint8_t* wav = mic.recordWAV(4, &wavSize); // 4秒録音(WAV形式)
HTTPClient http;
http.begin(RELAY_URL);
http.addHeader("Content-Type", "audio/wav");
http.addHeader("X-Device-Token", "<端末ごとの合言葉>");
int code = http.POST(wav, wavSize);
free(wav);
if (code == 200) {
String body = http.getString(); // 返ってきた WAV を再生
spk.playWAV((uint8_t*)body.c_str(), body.length());
}
http.end();
}
実機では、録音時間をボタンの長さに合わせる、通信エラー時に短い音を鳴らす、といった調整を加えます。また INMP441 は 24 ビットのデータを出すため、音が小さい場合は 32 ビットで受け取ってから変換するなど、マイクに合わせた調整が必要です。
中継サーバーのプログラム
Cloudflare Workers で書いた中継サーバーの例です。音声を受け取り、文字起こし→回答作成→音声合成の順に API を呼び、できた音声をそのまま返します。
export default {
async fetch(req, env) {
if (req.headers.get("X-Device-Token") !== env.DEVICE_TOKEN)
return new Response("Forbidden", { status: 403 });
const auth = { Authorization: `Bearer ${env.OPENAI_API_KEY}` };
// ② 文字起こし
const form = new FormData();
form.append("file", new File([await req.arrayBuffer()], "voice.wav", { type: "audio/wav" }));
form.append("model", "gpt-4o-mini-transcribe");
const { text } = await (await fetch("https://api.openai.com/v1/audio/transcriptions",
{ method: "POST", headers: auth, body: form })).json();
// ③ 店舗情報を添えて回答を作る
const chat = await (await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST", headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({
model: "gpt-4o-mini",
max_tokens: 200,
messages: [
{ role: "system", content: env.SHOP_PROMPT }, // 営業時間・メニュー・答えてよい範囲
{ role: "user", content: text },
],
}),
})).json();
const answer = chat.choices[0].message.content;
// ④ 音声に変換して ⑤ そのまま返す
const speech = await fetch("https://api.openai.com/v1/audio/speech", {
method: "POST", headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ model: "gpt-4o-mini-tts", voice: "alloy", input: answer, response_format: "wav" }),
});
return new Response(speech.body, { headers: { "Content-Type": "audio/wav" } });
},
};
モデル名やパラメーターは執筆時点の例です。実際に使うときは OpenAI の公式ドキュメントで最新の情報を確認してください。OPENAI_API_KEY や SHOP_PROMPT は、コードに直接書かず Workers のシークレット・環境変数に登録します。
店舗で使うための設計ポイント
1. 応答の速さ
3つの API を順番に呼ぶため、回答まで数秒かかります。「お調べします」と先に短い音声を流す、回答を短めにする(max_tokens を絞る)だけでも体感はかなり変わります。
2. 騒がしい場所での聞き取り
常に聞き続ける方式より、ボタンを押している間だけ録音する方式のほうが、周りの会話を拾わず、誤反応も防げます。マイクの向きと設置場所も聞き取り精度に大きく影響します。
3. 答えてよい範囲を決める
店舗情報を渡すときに「この情報にないことは答えず、スタッフに案内する」と指示しておきます。価格や在庫など間違えると困る内容は、定期的に情報を更新する仕組みも必要です。
4. プライバシーへの配慮
音声は回答を作るためだけに使い、保存しない設計にします。端末の近くに「音声はAIによる案内のために利用します」と掲示しておくと、お客さまも安心して使えます。
5. 費用の上限
いたずらで何度も使われても費用が膨らまないよう、中継サーバーで1日あたりの回数に上限を設けます。OpenAI 側の利用上限の設定も併用すると安心です。
6. つながらないときの動き
Wi-Fi や API が止まったときに無反応だと故障と思われてしまいます。「ただいまご案内できません。スタッフにお声がけください」といった音声を、端末側に用意しておきます。