はじめに
前回までに、Groq API をラズパイで動かし、音声会話システムを構築しました。
今回は、M5 Stack Core2 AWS に入出力(マイク、スピーカー)の役割をもたせます。
Arduino IDE への追加インストール
まず、PC上の Arduino IDEにM5Stack用の設定を追加します。M5Stackにプログラムを書き込むだけなので、PCはなんでも OK です。
①ボードマネージャーにM5Stack URLを追加
Arduino IDEの設定画面で追加のボードマネージャーURLに 下を追加:
https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json
②ボードマネージャーでM5Stackをインストール
ツール → ボード → ボードマネージャー で M5Stack を検索してインストール。
M5Stack by M5Stack official : ver.3.3.7 でした。
③ライブラリをインストール
ツール → ライブラリを管理 で以下をインストール:
M5Unified : v.0.2.14 で、インストール済みでした(たまたま?)。
ArduinoJson : v.7.4.3 で、インストール済みでした(たまたま?)。
ラズパイにFlaskサーバーを設定
ラズパイに Flask をインストールします。
pip install flask --break-system-packages
Flaskサーバー用コードを ~/voice_server.py として保存します。スクリプトの内容は以下です。
from flask import Flask, request, send_file
import subprocess
import os
import json
app = Flask(__name__)
VOICE = "/usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice"
DICT = "/var/lib/mecab/dic/open-jtalk/naist-jdic"
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
@app.route("/talk", methods=["POST"])
def talk():
print(f"受信データサイズ: {len(request.data)} bytes")
print(f"Content-Type: {request.content_type}")
audio_data = request.data
if len(audio_data) == 0:
return "音声データが空です", 400
with open("/tmp/input.wav", "wb") as f:
f.write(audio_data)
# Groq音声認識
result = subprocess.run([
"curl", "-s",
"https://api.groq.com/openai/v1/audio/transcriptions",
"-H", f"Authorization: Bearer {GROQ_API_KEY}",
"-F", "model=whisper-large-v3",
"-F", "file=@/tmp/input.wav",
"-F", "language=ja"
], capture_output=True, text=True)
text = json.loads(result.stdout)["text"]
print(f"あなた: {text}")
# Groq LLM
messages = [
{"role": "system", "content": "あなたは簡潔に返答するアシスタントです。返答は必ず日本語で1文以内にしてください。"},
{"role": "user", "content": text}
]
result = subprocess.run([
"curl", "-s",
"https://api.groq.com/openai/v1/chat/completions",
"-H", f"Authorization: Bearer {GROQ_API_KEY}",
"-H", "Content-Type: application/json",
"-d", json.dumps({"model": "llama-3.3-70b-versatile", "messages": messages})
], capture_output=True, text=True)
reply = json.loads(result.stdout)["choices"][0]["message"]["content"]
print(f"AI: {reply}")
# open_jtalkで音声合成(reply_raw.wavに出力)
clean_reply = reply.replace("\n", "、")
subprocess.run(
f'echo "{clean_reply}" | open_jtalk -x {DICT} -m {VOICE} -ow /tmp/reply.wav',
shell=True
)
# サイズ確認
size = os.path.getsize("/tmp/reply.wav")
print(f"送信WAVサイズ: {size} bytes")
return send_file("/tmp/reply.wav", mimetype="audio/wav")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
起動方法は以下の通りです。
python3 ~/voice_server.py
これで以下が表示されれば OK です。途中で Warning が出ますが、商用利用不可のメッセージなので私的にホビーとして使用する分には大丈夫です。
Running on http://0.0.0.0:5000
Running on http://ラズパイのIPアドレス:5000
M5Stack 用のArduino IDE スケッチ
以下をコピペして、raspi_flask の名前で保存します。コンパイルしてエラーが出ないことを確認して、M5Stack に書き込みます。SSid と password、 IP address は各自の値を入れます。
#include <M5Unified.h>
#include <WiFi.h>
#include <HTTPClient.h>
const char* ssid = "xxxxxxxxxxxxxxxxxxx";
const char* password = "xxxxxxxxxxxxxxxxxxx";
const char* serverUrl = "http://ラズパイのIPアドレス:5000/talk";
#define RECORD_SECONDS 5
#define SAMPLE_RATE 16000
#define BUFFER_SIZE (SAMPLE_RATE * RECORD_SECONDS)
int16_t recordBuffer[BUFFER_SIZE];
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
M5.Speaker.setVolume(255); // 0-255で調整
M5.Lcd.setTextSize(2);
M5.Lcd.println("AI Assistant");
WiFi.begin(ssid, password);
M5.Lcd.print("WiFi connecting");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
M5.Lcd.print(".");
}
M5.Lcd.println("\nWiFi completed!");
M5.Lcd.println("Press A button");
}
void loop() {
M5.update();
if (M5.BtnA.wasPressed()) {
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 0);
M5.Lcd.println("Recording...");
// 録音
M5.Mic.begin();
M5.Mic.record(recordBuffer, BUFFER_SIZE, SAMPLE_RATE);
M5.Mic.end();
M5.Lcd.println("Sending...");
// WAVヘッダー作成
int dataSize = BUFFER_SIZE * 2;
int wavSize = dataSize + 44;
uint8_t* wavBuffer = (uint8_t*)malloc(wavSize);
memcpy(wavBuffer, "RIFF", 4);
*(int32_t*)(wavBuffer + 4) = wavSize - 8;
memcpy(wavBuffer + 8, "WAVE", 4);
memcpy(wavBuffer + 12, "fmt ", 4);
*(int32_t*)(wavBuffer + 16) = 16;
*(int16_t*)(wavBuffer + 20) = 1;
*(int16_t*)(wavBuffer + 22) = 1;
*(int32_t*)(wavBuffer + 24) = SAMPLE_RATE;
*(int32_t*)(wavBuffer + 28) = SAMPLE_RATE * 2;
*(int16_t*)(wavBuffer + 32) = 2;
*(int16_t*)(wavBuffer + 34) = 16;
memcpy(wavBuffer + 36, "data", 4);
*(int32_t*)(wavBuffer + 40) = dataSize;
memcpy(wavBuffer + 44, recordBuffer, dataSize);
// HTTPでラズパイに送信
HTTPClient http;
http.begin(serverUrl);
http.addHeader("Content-Type", "audio/wav");
int httpCode = http.POST(wavBuffer, wavSize);
free(wavBuffer);
if (httpCode == 200) {
M5.Lcd.println("Playing...");
WiFiClient* stream = http.getStreamPtr();
int len = http.getSize();
uint8_t* replyWav = (uint8_t*)malloc(len);
stream->readBytes(replyWav, len);
M5.Speaker.begin();
M5.Speaker.playWav(replyWav, len);
while (M5.Speaker.isPlaying()) { delay(10); }
M5.Speaker.end();
free(replyWav);
} else {
M5.Lcd.println("Error: " + String(httpCode));
}
http.end();
M5.Lcd.println("Press A button");
}
}
M5 Stack の液晶画面に A の文字が表示されたら、画面左下の薄い〇印を押します。すぐに、M5 Stack 内蔵のマイクで録音が始まるので、話しかけてください。
すると、ラズパイのターミナルに話した内容のテキスト、および、AIの回答テキストが表示されます。それと同時に、M5 Stack のスピーカーからAIの回答テキストが発話されます。
AIの回答を2文以内に指定したところ、ときどき音声サイズオーバーで M5 Stack 液晶画面に -11 が表示される場合があります。そのため、1文以内に制限しています。
おわりに
わりとスムーズに構築できました。音声サイズオーバー対策が課題です。