
以前、「Raspberry piでChatGPTとおしゃべりしてみる」で同じような記事を投稿しましたが、技術やツールが新しくなってかなり簡単に作れるようになってきました。
Gemini3も公開されたので、今回はGeminiとおしゃべりしたいと思います。
ただ、前回と同じようにおしゃべりでは面白くないので、Geminiの回答を読み上げるときにプラモデルのハロを口パクさせて、ハロとおしゃべりしているような感じにしたいと思います。
LLM、音声認識、読み上げ、それぞれローカル単独で動かせるようにしたいところですが、それなりに動かすには現状では難しいようですので、今回はクラウドをべったり頼ります。今後の技術の進歩に期待しましょう!
今回も、全ソースをGitHubにアップしてますので参考にどうぞ。
ラズパイ準備
今回はraspberry pi 5を使用しました。
今回の内容はハードスペックがそれほど必要ではないので、4とかzeroでも動くと思います。未確認ですが。
オーディオ入出力
raspberry pi 5からオーディオジャックがなくなってしまったので、スピーカーはUSB接続を使用します。
マイクも同様にUSB接続です。
今回は以下のものを使用しました。
- サンワサプライ(Sanwa Supply) PCフラット型マイク MM-MCF02BK
- クリエイティブ・メディア Creative Sound Blaster Play! 3 USB オーディオ インターフェース 最大 24bit/96kHz ハイレゾ再生 SB-PLAY3
最近のラズパイOSは入出力をGUIで簡単に選択できます。画面最上部のマイクやスピーカーのアイコンを右クリックするとデバイスを選択できます。

ただ、マイク入力は後ほどデバイスIDを調べて指定する必要がありますが。。。
GPIO
GPIOの接続は以下のようにします。
- 赤:2番(5V)
- 茶:6番(GROUND)
- オレンジ:12番(GPIO18)

プラモデルのハロをちょっと改造して、サーボモータで口をパクパクするようにします。
時短のためサーボモータを使用し巻いたが、かなり音がうるさいので本当ならソレノイドとかを使ったほうがいいと思います。制御回路を組むのが面倒だったので。。。

プラモデルのハロの口をステンレス棒で押し上げられるようにしています。
サーボモータにプラモデルのゲートの平らな部分を切り取ったものをくっつけてステンレス棒を上下させるようにしています。
以下のサンプルでテストしました。
# kutipaku-test.py
from gpiozero import AngularServo
from time import sleep
# SG90のピン設定
SERVO_PIN = 18 # SG90-1
MIN_DEGREE = -90 # 000 : -90degree
MAX_DEGREE = 90 # 180 : +90degree
def main():
# min_pulse_width, max_pulse_width, frame_width =>SG90仕様
servo = AngularServo(SERVO_PIN, min_angle=MIN_DEGREE, max_angle=MAX_DEGREE, min_pulse_width=0.5/1000, max_pulse_width=2.4/1000, frame_width=1/50)
servo.angle = 0
sleep(0.1)
# SG90を -60度 <-> +60度で角度を変える
try:
for _ in range(30):
servo.angle = 15
sleep(0.1)
servo.angle = 0
sleep(0.1)
except KeyboardInterrupt:
print("stop")
return
if __name__ == "__main__":
main()
Geminiの利用
Geminiを利用するにはAPIキーが必要なので、Google AI Studio でプロジェクトを作成してAPIキーを発行します。
Google AI Studio にログインして、左ペインで「APIキー」をクリック、右ペインの「APIキーを作成」をクリックします。

プロジェクトが必要なので、「プロジェクトを作成」を選択します。


適当にプロジェクトを作成して、選択し、APIの名称を付けて作成をクリックします。
作成したAPIキーはメモしておきましょう。
Geminiを利用するサンプルは公式サイトで公開されています。
GPIOを動かすにはライブラリが必要なのでインストールします。アップデートとアップグレードもついでにやります。
sudo apt update
sudo apt upgrade -y
sudo apt install -y python3-rpi.gpio
pip install gpiozero
今回は以下のサンプルで動作確認しました。
from google import genai
client = genai.Client()
chat = client.chats.create(model="gemini-3-flash-preview")
response = chat.send_message_stream("I have 2 dogs in my house.")
for chunk in response:
print(chunk.text, end="")
response = chat.send_message_stream("How many paws are in my house?")
for chunk in response:
print(chunk.text, end="")
for message in chat.get_history():
print(f'role - {message.role}', end=": ")
print(message.parts[0].text)
音声認識
音声認識は今回、SpeechRecognitionを使用します。
SpeechRecognitionを動かすにはOSライブラリが必要なのでインストールします。
sudo apt install -y portaudio19-dev
sudo apt install -y flac
マイクのデバイスIDを以下のコードで調べます。
import speech_recognition as sr
# 使用可能なすべてのマイクデバイスのリストを表示
for index, name in enumerate(sr.Microphone.list_microphone_names()):
print("Microphone with name \"{0}\" found for Microphone(device_index={1})".format(name, index))
対象のデバイスのdevice_indexを、以下のソースのdevice_indexに設定して動作確認します。
device_index = 0
SAMPLE_RATE = 44100 # 44100 # サンプリングレート
recognizer = sr.Recognizer()
# デフォルトだと True だが、音声が長めになりがちなので False にしている
recognizer.dynamic_energy_threshold = False
# minimum audio energy to consider for recording(300)
# 録音時に考慮すべき最小のオーディオエネルギー
recognizer.energy_threshold = 100
# minimum seconds of speaking audio before we consider the speaking audio a phrase - values below this are ignored (for filtering out clicks and pops) (0.3)
# 話し声をフレーズとみなすまでの話し声の最小秒数 - この値未満の値は無視されます(クリック音やポップ音を除外するため)
recognizer.phrase_threshold = 0.3
# seconds of non-speaking audio before a phrase is considered complete (0.8)
# フレーズが完了したとみなされるまでの、話されていない音声の秒数
recognizer.pause_threshold = 0.5
# seconds of non-speaking audio to keep on both sides of the recording (0.5)
# 録音の両側に残しておく、話されていない音声の秒数
recognizer.non_speaking_duration = 0.5
def mike_to_textise():
print('初期化中。。。')
with sr.Microphone(sample_rate=SAMPLE_RATE, device_index=device_index) as source:
while True:
recognizer.adjust_for_ambient_noise(source)
# while True:
try:
print('しゃべってください')
audio_data = recognizer.listen(source=source, timeout=5)
print('聞き取れました')
sprec_text = recognizer.recognize_google(audio_data , language='ja-JP')
# os.system('cls')
print(sprec_text)
# print('----------------------')
if sprec_text == '終了':
return 0
except sr.UnknownValueError:
print('エラー:UnknownValueError')
except sr.RequestError:
print('エラー:RequestError')
except sr.WaitTimeoutError:
print('エラー:WaitTimeoutError')
# time.sleep(1)
mike_to_textise()
なかなか言葉を認識しなかったのでいろいろ調整しました。
サンプリングレートを指定しないと雑音がいっぱいで認識してくれません。
それ以外の設定もいろいろ調整しました。
基本的にサンプリングレートを44100か48000のどちらかを設定すれば認識してくれるみたいです。
たまに認識にめちゃめちゃ時間がかかるときがありますが、何とか動きました。
読み上げ
今回の読み上げライブラリはedge-ttsを使用しました。
他にもgTTSとかpyttsxとかOpenJTalkとかを検討しましたが、もっとも簡単で品質が良かったです。
gTTSを使いたかったのですが、Geminiと併用するとアクセスが多くなりすぎてエラーになっちゃうので諦めました。
MicrosoftのedgeのTTS(Text-to-Speech)機能を呼び出すpythonライブラリとのことです。(MSオフィシャルではないです。サンプルが中国語なので作者はおそらく中国の方です。)
以下のサンプルで動作確認しました。
# edge_tts-test.py
import asyncio
import io
import pygame
from edge_tts import Communicate
# --- 内部的な非同期処理(ここはライブラリの仕様上必要) ---
async def _generate_audio(text, voice):
communicate = Communicate(text, voice)
audio_buffer = io.BytesIO()
async for chunk in communicate.stream():
if chunk["type"] == "audio":
audio_buffer.write(chunk["data"])
audio_buffer.seek(0)
return audio_buffer
# --- 同期的に呼び出せるラッパー関数 ---
def speak(text, voice="ja-JP-NanamiNeural"):
"""非同期処理を隠蔽して、普通の関数として呼び出せるようにする"""
# asyncio.run() で非同期の世界を一時的に起動して結果を待つ
buffer = asyncio.run(_generate_audio(text, voice))
# pygameの再生(ここは元々同期処理)
pygame.mixer.init()
try:
pygame.mixer.music.load(buffer)
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
pygame.time.Clock().tick(10) # CPU負荷を抑えつつ待機
finally:
pygame.mixer.quit()
buffer.close()
# --- メイン処理(asyncなし!) ---
def main():
print("音声生成を開始します...")
# 普通の関数として呼び出し
speak("こんにちは。これは、非同期処理を隠して同期的に実行するテストです。")
print("再生が終了しました。")
if __name__ == "__main__":
main()
実装
以上の内容を一つにまとめてハロとおしゃべりするデバイスを作ります。
必要なライブラリがほかにもあるのでrequirements.txtにまとめてあります。
python-dotenv
gpiozero
pygame
# 音声認識ライブラリ
SpeechRecognition
pyaudio
# 読み上げライブラリ
edge-tts
# Gemini
google-generativeai
google-genai
APIキーを.envファイルに保存して読み込むようにしています。
GOOGLE_API_KEY=<Google AI Studio で作成したAPIキー>
サーボモータの制御は読み上げと並列に動かしたいのでスレッドにしました。
# haro-talk.py
import io
import pygame
import os
import time
import threading
import asyncio
from dotenv import load_dotenv
from gpiozero import AngularServo
from google import genai
from google.genai.types import GenerateContentConfig
import speech_recognition as sr
from edge_tts import Communicate
# サーボモータ関連設定
SERVO_PIN = 18
MIN_DEGREE = -90
MAX_DEGREE = 90
# 音声認識設定
SAMPLE_RATE = 48000 # 44100/48000
mic_device_index = 0
mic_listen_timeout = None # long/None
recognizer = sr.Recognizer()
recognizer.dynamic_energy_threshold = False
recognizer.energy_threshold = 100
recognizer.phrase_threshold = 0.3
recognizer.pause_threshold = 0.5
recognizer.non_speaking_duration = 0.5
# システムプロンプト(System Instruction)の定義
system_prompt = """
あなたの返答は音声としてスピーカーから出力されるので、読み上げられる文字で出力してください。
ハッシュタグなどは出力しないでください。
英文字はアルファベットで読み上げられてしまうのでカタカナで出力してください。
かっこが気の読み仮名についても出力しないでください。
クオータを節約するため、100字以内を目標にできるだけ簡潔に答えてください。
"""
# Geminiの設定
load_dotenv()
GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
MODEL_CODE='gemini-2.5-flash' # gemini-3-flash-preview/gemini-2.5-flash
client = genai.Client(api_key=GOOGLE_API_KEY)
chat = client.chats.create(
model=MODEL_CODE,
config=GenerateContentConfig(
system_instruction=system_prompt
)
)
def kuchipaku(stop_event):
"""
口パク
サーボモータを制御して口パクします
:param stop_event: スレッド停止イベント
"""
servo = AngularServo(SERVO_PIN, min_angle=MIN_DEGREE, max_angle=MAX_DEGREE, min_pulse_width=0.5/1000, max_pulse_width=2.4/1000, frame_width=1/50)
servo.angle = 0
time.sleep(0.1)
# SG90を 0度 <-> +15度で角度を変える
try:
while not stop_event.is_set():
servo.angle = 15
time.sleep(0.1)
servo.angle = 0
time.sleep(0.1)
except KeyboardInterrupt:
print("kuchipaku error")
def create_kuchipaku_thread():
"""
口パクスレッド生成
"""
stop_event = threading.Event()
thead_hundle = threading.Thread(target=kuchipaku, args=(stop_event,))
return thead_hundle, stop_event
async def _generate_audio(text, voice):
"""
音声バイナリ生成
:param text: 出力するテキスト
:param voice: 音声
"""
communicate = Communicate(text, voice)
audio_buffer = io.BytesIO()
async for chunk in communicate.stream():
if chunk["type"] == "audio":
audio_buffer.write(chunk["data"])
audio_buffer.seek(0)
return audio_buffer
def speak(text, voice="ja-JP-NanamiNeural"):
"""
読み上げ処理
:param text: 出力するテキスト
:param voice: 音声
"""
buffer = asyncio.run(_generate_audio(text, voice))
pygame.mixer.init()
try:
pygame.mixer.music.load(buffer)
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
pygame.time.Clock().tick(10)
finally:
pygame.mixer.quit()
buffer.close()
def do_chat():
"""
メイン処理
"""
print('=== 初期化中。。。 ===')
with sr.Microphone(sample_rate=SAMPLE_RATE, device_index=mic_device_index) as source:
while True:
recognizer.adjust_for_ambient_noise(source)
try:
print('=== しゃべってください ===')
audio_data = recognizer.listen(source=source, timeout=mic_listen_timeout)
print('=== 聞き取れました ===')
sprec_text = recognizer.recognize_google(audio_data , language='ja-JP')
print(sprec_text)
if sprec_text == '終了':
return 0
# Gemini呼び出し
response = chat.send_message_stream(sprec_text)
response_text = ''
for chunk in response:
response_text += chunk.text
print('=== Geminiからの回答 ===')
print(response_text)
# Gemini返答を読み上げ
thead_hundle, stop_event = create_kuchipaku_thread()
thead_hundle.start()
speak(response_text)
stop_event.set() # ループを停止させる
thead_hundle.join() # スレッドが完全に終わるのを待つ
except sr.UnknownValueError:
print('=== エラー:UnknownValueError ===')
except sr.RequestError:
print('=== エラー:RequestError ===')
except sr.WaitTimeoutError:
print('=== エラー:WaitTimeoutError ===')
do_chat()
動かしてみるとこんな感じ。
まあ、レスポンスは悪いわモーター音はうるさいわ。残念な結果ですが、一応動きました。
今回はLLMとの音声チャットのアップデート版ということで目的は達成したというこで。。。
いろいろ改善すればもっといい感じにできると思いますが、ここまででも結構時間をかけてしまったので今回はここまでにしておこうと思います。
余裕があったらMCPを使って、音声指示で照明やテレビのON/OFFを行うようにしたところですが、また気が向いたらやりたいと思います。
