はじめに

以前「Raspberry Pi に顔を覚えてもらう」を執筆させていただいた者です。続きですよ続き!

前回の記事で、目標として立てた「やりたいこと」がこちら

やりたいこと

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

今回は3にあたる

「自分なら「お帰りなさい」他の人なら「どちらさまですか?」と話しかける」

こちらを実現するため今回は、Raspberry Piに喋ってもらおうと思います。

今回の実現目標

目的からすると、人感センサーが反応し、それが自分だと判別できたタイミングで動作開始するのが理想です。

ただ、今回は「喋ってもらおう」というのが目標ではあるので、以下の流れになる様に実装します。

  1. 「ただいま」という音声を受けた(Input)
  2. 「お帰りなさい」と発声(Output)
  3. 「おわり」という音声で処理終了(Input)

実現するにあたって

今回は、Google CloudのAI音声サービスを使用して、音声対話システムを構築してみようと思います。

使用するGoogle Cloud サービス

  • Cloud Speech-to-Text API(音声認識)
  • Cloud Text-to-Speech API(音声合成)

基本構成

  • Raspberry Pi 4
  • USBマイク
  • スピーカー(3.5mm イヤホンジャック端子のもの)

セットアップ

その1 Google Cloud Platform の初期設定

音声認識・音声合成を使うには、Google Cloud Platformのセットアップが必要です。

1. Google Cloud プロジェクトの作成

  1. Google Cloud Console にアクセス
  2. 画面上部の「プロジェクトを選択」→「新しいプロジェクト」をクリック
  3. プロジェクト名を入力(例:raspberry-pi-voice)して「作成」

2. 必要なAPIの有効化

プロジェクトを作成したら、2つのAPIを有効化します。

Cloud Speech-to-Text API の有効化

  1. 左メニューから「APIとサービス」→「ライブラリ」を選択
  2. 検索ボックスに「Speech-to-Text」と入力
  3. 「Cloud Speech-to-Text API」をクリック
  4. 「有効にする」ボタンをクリック

Cloud Text-to-Speech API の有効化

  1. 同じく「APIとサービス」→「ライブラリ」から
  2. 検索ボックスに「Text-to-Speech」と入力
  3. 「Cloud Text-to-Speech API」をクリック
  4. 「有効にする」ボタンをクリック

3. サービスアカウントの作成とキーのダウンロード

APIを使うための認証情報を作成します。

  1. 左メニューから「IAMと管理」→「サービスアカウント」を選択
  2. 「サービスアカウントを作成」をクリック
  3. サービスアカウント名を入力(例:raspberry-pi-voice-sa
  4. 「作成して続行」をクリック
  5. ロールの選択で右を追加:Cloud Speech Client
  6. 「完了」をクリック
  7. 作成したサービスアカウントをクリック
  8. 「キー」タブを選択
  9. 「鍵を追加」→「新しい鍵を作成」
  10. キーのタイプは「JSON」を選択して「作成」
  11. JSONファイルが自動ダウンロードされます(これが service-account-key.json になります)

セキュリティ注意: このJSONファイルは外部に漏らさないよう厳重に管理してください。

コストについて知っておこう

Google Cloud の音声サービスは従量課金制ですが、無料枠がかなり充実しています。

無料枠(2025年12月時点)

Cloud Speech-to-Text API

  • 毎月 60分まで無料
  • Standard モデルの場合

Cloud Text-to-Speech API

  • Standard音声: 毎月 400万文字まで無料
  • Neural2音声: 毎月 100万文字まで無料

実際の使用量目安

今回のシステムで「ただいま」→「お帰りなさい」のやり取りを想定すると:

  • 音声認識: 1回5秒 × 1日10回 × 30日 = 約25分/月
  • 音声合成: 「お帰りなさい」7文字 × 1日10回 × 30日 = 2,100文字/月

→ 完全に無料枠内で運用可能です!

有料になる場合の料金

無料枠を超えた場合でも、比較的リーズナブルです:

  • Speech-to-Text: $0.006/15秒(約1円/15秒)
  • Text-to-Speech (Neural2): $16.00/100万文字(約2,400円/100万文字)

1日中ずっと使い続けるような用途でない限り、月額数十円〜数百円程度で収まるはずです。

その2 OSインストールとシステム更新

まずは必要なライブラリ等をRaspberry Piにインストールしておきます。既に入ってる場合は、次のステップへ。

sudo apt update && sudo apt upgrade -y
sudo apt install -y python3-pip python3-dev python3-venv
sudo apt install -y portaudio19-dev python3-pyaudio
sudo apt install -y alsa-utils pulseaudio pulseaudio-utils
sudo apt install -y libportaudio2 libportaudiocpp0

その3 Python環境構築

仮想環境作成

python3 -m venv voice_env
source voice_env/bin/activate

必要なPythonライブラリのインストール

pip install google-cloud-speech google-cloud-texttospeech
pip install pyaudio pygame

その4 音声デバイス設定

# 音声デバイス確認
arecord -l  # 入力デバイス
aplay -l    # 出力デバイス

# USBマイクのテストなので5秒ほど下のコマンド実行後喋ってみてください
arecord -D plughw:[arecordで表示されたカード番号],[arecordで表示されたデバイス番号] -d 5 test.wav

# 上記コマンドで録音したファイルを再生
aplay test.wav

# 音量調整
alsamixer

その5 Google Cloud認証設定

サービスアカウントキーを保存したファイルを参照できる位置に配置し、環境変数設定をします。

echo 'export GOOGLE_APPLICATION_CREDENTIALS="/(Raspberry Piでの保存フォルダ)/service-account-key.json"' >> ~/.bashrc
source ~/.bashrc

これで環境が整いました。

いざ実装!!

以下のコードを voice_greeting.py として保存します。

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

class ContinuousVoiceGreeting:
    def __init__(self):
        # Google Cloud認証確認
        if not os.environ.get('GOOGLE_APPLICATION_CREDENTIALS'):
            print("GOOGLE_APPLICATION_CREDENTIALS 環境変数が設定されていません")
            raise Exception("Google Cloud認証情報が見つかりません")
        
        # Google Cloud クライアント初期化
        self.speech_client = speech.SpeechClient()
        self.tts_client = texttospeech.TextToSpeechClient()
        
        # 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初期化完了!")

    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 check_tadaima(self, text):
        """「ただいま」検出"""
        if text is None:
            return False
        
        tadaima_patterns = ["ただいま", "只今", "タダイマ", "tadaima", "ただ今"]
        text_lower = text.lower()
        
        for pattern in tadaima_patterns:
            if pattern.lower() in text_lower:
                print(f"「{pattern}」を検出しました!")
                return True
        
        print("「ただいま」は含まれていませんでした")
        return False

    def speak_okaeri(self):
        """「お帰りなさい」音声出力(3.5mmジャック)"""
        if self.is_speaking:
            return
        
        self.is_speaking = True
        message = "お帰りなさい"
        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ジャック用音声再生(MP3形式)
            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ジャック向け最適化)
            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)
            
            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 = ["終わり", "やめて", "ストップ", "stop", "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):
                        print("終了コマンドを検出")
                        break
                    
                    if self.check_tadaima(recognized_text):
                        self.speak_okaeri()
                
                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():
    try:
        greeting_system = ContinuousVoiceGreeting()
        greeting_system.run()
    except Exception as e:
        print(f"Raspberry Pi初期化エラー: {e}")
        print("セットアップガイドを確認してください")
        sys.exit(1)

if __name__ == "__main__":
    main()

実行してみよう

仮想環境がアクティブになっていることを確認してから実行します。

source voice_env/bin/activate
python voice_greeting.py

動かしてみた

実際にRaspberry Pi上で実行してみました。マイクに向かって「ただいま」と話しかけると…

(音が出ます)

音声として「お帰りなさい」と言ってもらえる様になりました!

最後に

今回でRaspberry Piにお話ししてもらえる様になり、少しお友達感が増す様になってきました。
ただ、少し応答に時間がかかる感じがあるため、改善していければなおさらお友達感が増す予感がします。

ここまで話してもらえる様になったなら、次は、Raspberry Piに会話の内容を理解してもらって、粋な返しができるようにするため、次回は、AIをRaspberry Piに搭載して、粋な会話ができるようにするチャレンジにしてみようと思います。

その時、会話の語尾は「にゃ」ってつけてもらうんだ俺!!!



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