1. どんなものを作るか
  2. 全体の仕組み
  3. 必要な部品
  4. 配線
  5. ESP32 側のプログラム
  6. 中継サーバーのプログラム
  7. 店舗で使うための設計ポイント

どんなものを作るか

ボタンを押して話しかけると、数秒後にスピーカーから回答が返ってくる端末です。たとえば店頭で「今日は何時まで開いていますか?」「駐車場はありますか?」と聞かれたときに、店舗の情報をもとに答えます。

スマートスピーカーとの違いは、答える内容を自分で決められることです。営業時間やメニュー、よくある質問だけを答えさせ、それ以外は「スタッフにお声がけください」と返す、といった制御ができます。

全体の仕組み

マイクとスピーカーをつないだESP32が、中継サーバーを経由して音声認識・対話・音声合成のAI APIを呼び出す構成図
図1 全体構成(イメージ)

ESP32 は「録音して送る」「返ってきた音声を流す」だけを担当し、AI とのやり取りは中継サーバーにまとめます。処理の順番は次のとおりです。

  1. ESP32 がボタンを押している間の音声を録音し、中継サーバーに送る
  2. 中継サーバーが音声認識 API で文字に起こす
  3. 店舗情報を添えて対話 API(ChatGPT)に回答を作ってもらう
  4. 回答の文章を音声合成 API で音声にする
  5. 音声データを ESP32 に返し、スピーカーで再生する
なぜ中継サーバーを挟むのか:ESP32 に API キーを書き込むと、端末を持ち出されたときにキーが漏れ、勝手に使われるおそれがあります。キーや店舗情報、1日の利用上限は中継サーバー側で管理し、端末には置かないのが安全です。Cloudflare Workers なら小規模な利用は無料枠に収まることが多く、維持費も抑えられます。

必要な部品

部品用途・選び方
ESP32-S3 開発ボード本体。録音データを一時的にためるので、PSRAM 付きのモデルがおすすめです。
INMP441I2S 接続のデジタルマイク。ノイズに強く、配線も簡単です。
MAX98357AI2S 接続のアンプ。デジタルの音声データをそのままスピーカーで鳴らせます。
小型スピーカー4Ω〜8Ω、2〜3W 程度のもの。
USB 電源・ケーブル常設するなら、抜けにくい配置と安定した電源を選びます。

部品代は合計で数千円程度です。ほかに、OpenAI の API 利用料(使った分だけの従量課金)がかかります。

配線

INMP441とMAX98357AをESP32-S3のGPIOに接続する配線例の表
図2 配線例。ピン番号はボードに合わせて変更してください。

マイクとアンプはどちらも 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 が止まったときに無反応だと故障と思われてしまいます。「ただいまご案内できません。スタッフにお声がけください」といった音声を、端末側に用意しておきます。

お店や受付への導入を相談したい方へ

音声案内のほか、来店者数のカウントや冷蔵庫の温度監視など、小さく始められるIoT・AIの導入をお手伝いしています。初回相談は無料です。

無料で相談する