はじめに

以前、Raspberry Pi にしゃべらせた者です。今回は本丸。Raspberry PiでAIによる応答が出来る様にしたいと思います。

使用する機材

使用する機材は、以前の記事にある環境そのまま使用します。

前回と違う箇所

前回は、「ただいま」に対して「おかえりなさい」と定型の返事をするだけでしたが、今回は、「ただいま」に対する返答内容をAIに考えてもらう、思いっきり他力本願柔軟な会話システムにします。ついでに、語尾は以前、明言させてもらった「にゃっ」をつけるにゃっ!!

AIを使うためにOpenAI API Key取得

AIをRaspberry Piで使用するためにOpenAIのAPI Keyを取得しておきます。

1. OpenAI にアクセスしてログイン

まずOpenAIのページに行きます。https://platform.openai.com/api-keys

  • ChatGPTアカウント(またはOpenAIアカウント)でログイン
  • アカウントを持っていない場合:Sign up(アカウント作成)

2.「Create new secret key」をクリック

ログイン後 → API keys の画面が表示されるはずです。ここでCreate new secret key(新規API Key作成) をクリック 

3.Keyをコピーして保存

生成されたKeyが表示されるのでメモしておきます。
重要:この画面でしか見られないのでKeyをコピーして 安全な場所に保存しておきましょう。

4.(必要なら)支払い情報を設定

APIの利用には 支払い情報(Billing)の登録 が必要になる場合があり、クレジットカード情報を登録しておくと、従量課金で使えるようになります。

5.Raspberry Piに登録

Raspberry Piで常に使用できる様、.bashrcに登録しておきます。

  1. ~/.bashrc を編集
nano ~/.bashrc
  1. ファイルの一番下に以下を追加
export OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxx"

※ sk-xxxxxxxxxxxxxxxx の部分を、取得したAPI Keyに置き換えてください。

  1. 保存して反映
source ~/.bashrc
  1. 設定されているか確認
echo $OPENAI_API_KEY

API Keyが表示されれば設定完了です。

6.OpenAIインストール

今回コードでOpenAIを使用するにあたりインストールしておきます。

pip install openai

コード

今回は、前回に作成したコードを変更する形で進めるため、前回のコードと今回作成したコードの違う箇所を抜粋してみます。

1. AIのため、OpenAI 追加

# 新規追加:OpenAI クライアント初期化
self.openai_client = OpenAI()

環境変数のチェックも追加

if not os.environ.get('OPENAI_API_KEY'):
    print("OPENAI_API_KEY 環境変数が設定されていません")
    raise Exception("OpenAI API認証情報が見つかりません")

2. 会話コンテキストの管理

AIとの会話履歴を保持することで、文脈を理解した応答が可能になります。以下ポイントです。

  • システムプロンプトで「にゃっ」語尾を指定(親しみやすさ重視)
  • 絵文字禁止ルール(音声合成で読み上げられてしまうため)
  • 応答は1〜2文に制限(長すぎると聞き取りづらい)
self.conversation_history = [
    {
        "role": "system",
        "content": """あなたは家庭用のスマートスピーカーのアシスタントです。
以下のルールに従って応答してください:
- 親しみやすく、温かみのある口調で話す
- 応答は短く簡潔に(1〜2文程度)
- 「ただいま」には「お帰りなさい」系の返事をする
- 「お帰りなさい」系の返事には質問をする
- 日本語で応答する
- 絵文字は使わない(音声読み上げされるため)
- 語尾には必ず「にゃっ」をつける(例:「お帰りなさいにゃっ」「わかったにゃっ」)"""
    }
]

3. AI応答生成機能

新しく追加されたget_chatgpt_response()メソッドで、ユーザー入力に対する応答を生成します。以下ポイントです。

  • モデル選択: gpt-4o-miniを使用してコスト削減
  • 会話履歴の管理: 20メッセージを超えたら古いものを削除(システムプロンプトは維持)
  • temperature=0.7: 適度にクリエイティブだが安定した応答
def get_chatgpt_response(self, user_input):
    """AIから応答を取得"""
    # ユーザー入力を会話履歴に追加
    self.conversation_history.append({
        "role": "user",
        "content": user_input
    })
    
    # AIのAPI呼び出し
    response = self.openai_client.chat.completions.create(
        model="gpt-4o-mini",  # コスト効率の良いモデル
        messages=self.conversation_history,
        max_tokens=150,
        temperature=0.7
    )
    
    assistant_message = response.choices[0].message.content
    
    # 応答を会話履歴に追加
    self.conversation_history.append({
        "role": "assistant",
        "content": assistant_message
    })
    
    # 会話履歴が長くなりすぎたら古いものを削除
    if len(self.conversation_history) > 20:
        self.conversation_history = [self.conversation_history[0]] + self.conversation_history[-10:]
    
    return assistant_message

主な修正は以上になります。ということで、今回作成したコードの全容はこちら!!

コード

import time
import sys
import os
import tempfile
import pyaudio
from google.cloud import speech
from google.cloud import texttospeech
import pygame
from openai import OpenAI


class ContinuousVoiceGreeting:
    def __init__(self):
        # Google Cloud認証確認
        if not os.environ.get('GOOGLE_APPLICATION_CREDENTIALS'):
            print("GOOGLE_APPLICATION_CREDENTIALS 環境変数が設定されていません")
            raise Exception("Google Cloud認証情報が見つかりません")
        
        # OpenAI認証確認
        if not os.environ.get('OPENAI_API_KEY'):
            print("OPENAI_API_KEY 環境変数が設定されていません")
            raise Exception("OpenAI API認証情報が見つかりません")
        
        # OpenAI クライアント初期化
        self.openai_client = OpenAI()
        
        # Google Cloud クライアント初期化
        self.speech_client = speech.SpeechClient()
        self.tts_client = texttospeech.TextToSpeechClient()
        
        # AI会話履歴(コンテキスト維持用)
        self.conversation_history = [
            {
                "role": "system",
                "content": """あなたは家庭用のスマートスピーカーのアシスタントです。
以下のルールに従って応答してください:
- 親しみやすく、温かみのある口調で話す
- 応答は短く簡潔に(1〜2文程度)
- 「ただいま」には「お帰りなさい」系の返事をする
- 「お帰りなさい」系の返事には質問をする
- 日本語で応答する
- 絵文字は使わない(音声読み上げされるため)
- 語尾には必ず「にゃっ」をつける(例:「お帰りなさいにゃっ」「わかったにゃっ」)"""
            }
        ]
        
        # Raspberry Pi最適化設定
        self.speech_config = speech.RecognitionConfig(
            encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,
            sample_rate_hertz=16000,
            language_code="ja-JP",
            model="latest_short",  # 短時間モデルが安定
            use_enhanced=True,
            enable_automatic_punctuation=True,
        )
        
        # 自然な日本語音声設定
        self.tts_voice = texttospeech.VoiceSelectionParams(
            language_code="ja-JP",
            ssml_gender=texttospeech.SsmlVoiceGender.FEMALE,
            name="ja-JP-Neural2-B"
        )
        
        self.tts_config = texttospeech.AudioConfig(
            audio_encoding=texttospeech.AudioEncoding.MP3,
            speaking_rate=0.9,  # 少し遅めが聞き取りやすい
            pitch=0.0
        )
        
        # PyAudio設定(Raspberry Pi最適化)
        self.audio_format = pyaudio.paInt16
        self.channels = 1
        self.rate = 16000
        self.chunk = 1024
        self.audio = pyaudio.PyAudio()
        
        # システム状態管理
        self.is_running = True
        self.is_speaking = False
        
        # pygame初期化(3.5mmジャック用最適化)
        os.environ['SDL_AUDIODRIVER'] = 'alsa'  # ALSA強制使用
        pygame.mixer.pre_init(frequency=22050, size=-16, channels=1, buffer=512)
        pygame.mixer.init()
        
        print("Raspberry Pi + AI 初期化完了!")

    def get_chatgpt_response(self, user_input):
        """AIから応答を取得"""
        try:
            # ユーザー入力を会話履歴に追加
            self.conversation_history.append({
                "role": "user",
                "content": user_input
            })
            
            # AIのAPI呼び出し
            response = self.openai_client.chat.completions.create(
                model="gpt-4o-mini",  # コスト効率の良いモデル
                messages=self.conversation_history,
                max_tokens=150,
                temperature=0.7
            )
            
            # 応答を取得
            assistant_message = response.choices[0].message.content
            
            # 応答を会話履歴に追加
            self.conversation_history.append({
                "role": "assistant",
                "content": assistant_message
            })
            
            # 会話履歴が長くなりすぎたら古いものを削除(システムプロンプトは維持)
            if len(self.conversation_history) > 20:
                self.conversation_history = [self.conversation_history[0]] + self.conversation_history[-10:]
            
            print(f"ChatGPT応答: 「{assistant_message}」")
            return assistant_message
            
        except Exception as e:
            print(f"ChatGPTエラー: {e}")
            return "すみません、うまく応答できませんでしたにゃっ"

    def listen_for_voice(self):
        """音声入力を待機・取得する"""
        try:
            print("音声入力待機中... (5秒間録音)")
            
            stream = self.audio.open(
                format=self.audio_format,
                channels=self.channels,
                rate=self.rate,
                input=True,
                frames_per_buffer=self.chunk,
                input_device_index=None
            )
            
            frames = []
            for _ in range(0, int(self.rate / self.chunk * 5)):
                try:
                    data = stream.read(self.chunk, exception_on_overflow=False)
                    frames.append(data)
                except OSError:
                    print("音声入力でオーバーフローが発生しました")
                    continue
            
            stream.stop_stream()
            stream.close()
            
            audio_data = b''.join(frames)
            
            print("Google Cloudで音声認識中...")
            
            # Google Cloud Speech-to-Text
            audio = speech.RecognitionAudio(content=audio_data)
            response = self.speech_client.recognize(
                config=self.speech_config,
                audio=audio
            )
            
            if response.results:
                text = response.results[0].alternatives[0].transcript
                confidence = response.results[0].alternatives[0].confidence
                print(f"認識結果: 「{text}」(信頼度: {confidence:.2f})")
                return text
            else:
                print("音声を認識できませんでした")
                return None
                
        except Exception as e:
            print(f"音声認識エラー: {e}")
            return None

    def speak_response(self, message):
        """音声出力(3.5mmジャック)"""
        if self.is_speaking:
            return
        
        self.is_speaking = True
        print(f"3.5mmジャックから音声出力: 「{message}」")
        
        try:
            # Google Cloud Text-to-Speech
            synthesis_input = texttospeech.SynthesisInput(text=message)
            
            response = self.tts_client.synthesize_speech(
                input=synthesis_input,
                voice=self.tts_voice,
                audio_config=self.tts_config
            )
            
            # 3.5mmジャック用音声再生
            with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as tmp_file:
                tmp_file.write(response.audio_content)
                tmp_file_path = tmp_file.name
            
            # pygame音声再生(3.5mmジャック向け最適化)
            try:
                pygame.mixer.music.load(tmp_file_path)
                pygame.mixer.music.set_volume(0.8)  # 音量調整
                pygame.mixer.music.play()
                
                while pygame.mixer.music.get_busy():
                    time.sleep(0.1)
                    
            except pygame.error as e:
                print(f"pygame音声エラー: {e}")
            
            os.unlink(tmp_file_path)
            print("音声出力完了")
            
        except Exception as e:
            print(f"音声出力エラー: {e}")
        finally:
            self.is_speaking = False

    def check_exit_command(self, text):
        """終了コマンドチェック"""
        if text is None:
            return False
        
        exit_patterns = ["終わり", "やめて", "ストップ", "またね", "exit", "quit", "シャットダウン"]
        text_lower = text.lower()
        
        for pattern in exit_patterns:
            if pattern.lower() in text_lower:
                return True
        return False

    def run(self):
        """メインループ"""
        try:
            while self.is_running:
                recognized_text = self.listen_for_voice()
                
                if recognized_text:
                    if self.check_exit_command(recognized_text):
                        # 終了時もChatGPTに別れの挨拶を生成させる
                        farewell = self.get_chatgpt_response("終了します。さようなら。")
                        self.speak_response(farewell)
                        print("終了コマンドを検出")
                        break
                    
                    # ChatGPTで応答を生成
                    response = self.get_chatgpt_response(recognized_text)
                    self.speak_response(response)
                
                time.sleep(0.5)
                print("\n" + "-"*40)
                
        except KeyboardInterrupt:
            print("\n Ctrl+Cで終了")
        except Exception as e:
            print(f"\n システムエラー: {e}")
        finally:
            self.cleanup()

    def cleanup(self):
        """終了処理"""
        self.is_running = False
        
        if self.is_speaking:
            print("音声出力完了を待機中...")
            time.sleep(2)
        
        self.audio.terminate()
        pygame.mixer.quit()


def main():
    print("=" * 50)
    print("ChatGPT音声アシスタント(にゃっ語尾付き)")
    print("=" * 50)
    print("必要な環境変数:")
    print("  - GOOGLE_APPLICATION_CREDENTIALS")
    print("  - OPENAI_API_KEY")
    print("=" * 50)
    
    try:
        greeting_system = ContinuousVoiceGreeting()
        greeting_system.run()
    except Exception as e:
        print(f"Raspberry Pi初期化エラー: {e}")
        print("セットアップガイドを確認してください")
        sys.exit(1)


if __name__ == "__main__":
    main()

話しかけてみよう

音が出ます。

すごい。めっちゃ会話してくれる!!!オレニトモダチデキターーーーー!!!!

ただ、やはりレスポンスに少し間が空くのは気になりますが、用途として帰宅後に上着を脱ぎ靴を脱ぐ間の会話を目的としているので、以下の点に工夫を入れることで解消出来そうな気がします。

  • 応答のラリー回数を減らす
  • 会話開始をRaspberry Piが行うことで、会話ラリーの待ちが少なくなる

返答の音量は、設定等で制御できますが、接続するスピーカーのつまみで調整するのが手軽かな。(横着)

最後に

今までRaspberry Piを使用して、以下の機能を試してきました。

これをまるっとまとめると、以前のRaspberry Piに顔を覚えてもらうという記事にある、やりたいことに挙げた以下の項目が全て実現できる算段になったと思います。

  1. 帰宅したことに反応する → 人検知機能
  2. 誰が帰宅したか判別する → 顔認識機能
  3. 2の結果、自分なら「おかえりなさい」他の人なら「どちらさまですか?」と話しかける
    奥さんの場合、処理終了(うるさいって言われるから) → 音声入出力機能
  4. 応答に合わせ、粋な返しをする → AIによる会話機能
  5. 2回くらいの会話ラリーの後、「ゆっくりお休みください」と言って処理終了 → AIによる会話機能

これが全て実現できるわけです。次回はその集大成となるお帰りなさいマシーン完成を目指して突き進んでいきます!!!

きっと帰宅が楽しみになる様な装置が出来るはずにゃっ!!!



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる