メインコンテンツへスキップ
見出し画像

【しゃべるAI】おまけーAIにデバイスを操作させる

    先ほどのコードを拡張して、ラズパイのGPIOを制御させるモジュールを追加してみました。

    1. コード

    "
    Raspberry Pi向け TTS-APIサーバ接続と GPIO/サーボモーター制御
    """
    
    import os
    import re
    import io
    import time
    import tempfile
    import requests
    import platform
    import subprocess
    import threading
    from pydub import AudioSegment
    from pydub.playback import play
    import json
    
    # Raspberry Pi環境でのみ必要なモジュールをインポート
    try:
        import RPi.GPIO as GPIO
        import pigpio
        IS_RASPBERRY_PI = True
        print("Raspberry Piモードで実行中")
    except ImportError:
        IS_RASPBERRY_PI = False
        print("通常モードで実行中(GPIO機能は無効)")
    
    # ユーザー設定 - ここで初期設定を変更できます
    TTS_API_URL = "http://192.168.xxx.xxx:5000/voice"  # TTSサーバーのアドレス
    MODEL_NAME = "【モデル】"                              # 使用するモデル
    MAX_CHUNK_LENGTH = 100                             # テキスト分割の最大長
    AUDIO_PARAMS = {
        "length": 0.86,                                # 音声の長さ調整
        "split_interval": 0,                           # スプリット間隔
        "style_weight": 1                              # スタイルの重み
    }
    
    # GPIO設定
    LED_PIN = 17                        # LEDのGPIOピン番号
    SERVO_PIN = 18                      # サーボモーターのGPIOピン番号
    
    # AIレスポンスのキーワードとアクション設定
    KEYWORD_ACTIONS = {
        "喜び": {"led": True, "servo": 90},      # 喜びの表現時、LEDを点灯しサーボを90度に
        "悲しみ": {"led": False, "servo": 0},    # 悲しみの表現時、LEDを消灯しサーボを0度に
        "驚き": {"led": "blink", "servo": 180},  # 驚きの表現時、LEDを点滅しサーボを180度に
        "質問": {"led": "pulse", "servo": 45},   # 質問の表現時、LEDをパルスさせサーボを45度に
    }
    
    try:
        import simpleaudio as sa
        SIMPLEAUDIO_AVAILABLE = True
    except ImportError:
        SIMPLEAUDIO_AVAILABLE = False
        print("注意: simpleaudioがインストールされていません。代替の再生方法を使用します。")
    
    # 基本設定
    temp_dir = tempfile.gettempdir()
    output_file = os.path.join(temp_dir, "tts_output.wav")
    
    # GPIO初期化(Raspberry Piで実行されている場合のみ)
    if IS_RASPBERRY_PI:
        # GPIO設定
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(LED_PIN, GPIO.OUT)
        GPIO.output(LED_PIN, GPIO.LOW)
        
        # PWM設定(LED用)
        led_pwm = GPIO.PWM(LED_PIN, 100)  # 100Hz
        led_pwm.start(0)
        
        # pigpioサーボ用初期化
        pi = pigpio.pi()
        if not pi.connected:
            print("警告: pigpioデーモンに接続できません。サーボ制御は無効です。")
            print("pigpioを開始するには: 'sudo pigpiod'を実行してください。")
        else:
            # サーボ用のGPIOをセットアップ(パルス幅: 500-2500μs = 0-180度)
            pi.set_servo_pulsewidth(SERVO_PIN, 1500)  # 中央位置(90度)
    
    def cleanup_gpio():
        """GPIO関連のクリーンアップ処理"""
        if IS_RASPBERRY_PI:
            try:
                led_pwm.stop()
                GPIO.cleanup()
                pi.stop()
            except:
                pass
    
    def control_led(state, duration=None):
        """LEDを制御する関数
        
        Args:
            state: Trueで点灯、Falseで消灯、"blink"で点滅、"pulse"でパルス
            duration: 効果の持続時間(秒)、Noneの場合は永続的
        """
        if not IS_RASPBERRY_PI:
            print(f"LED制御シミュレーション: {state}")
            return
            
        if state == True:
            GPIO.output(LED_PIN, GPIO.HIGH)
            print("LEDをONにしました")
        elif state == False:
            GPIO.output(LED_PIN, GPIO.LOW)
            print("LEDをOFFにしました")
        elif state == "blink":
            def blink_thread():
                end_time = time.time() + (duration if duration else 3)
                while time.time() < end_time:
                    GPIO.output(LED_PIN, GPIO.HIGH)
                    time.sleep(0.2)
                    GPIO.output(LED_PIN, GPIO.LOW)
                    time.sleep(0.2)
                GPIO.output(LED_PIN, GPIO.LOW)
                
            print("LEDを点滅させています")
            t = threading.Thread(target=blink_thread)
            t.daemon = True
            t.start()
        elif state == "pulse":
            def pulse_thread():
                end_time = time.time() + (duration if duration else 3)
                while time.time() < end_time:
                    for dc in range(0, 101, 5):
                        led_pwm.ChangeDutyCycle(dc)
                        time.sleep(0.05)
                    for dc in range(100, -1, -5):
                        led_pwm.ChangeDutyCycle(dc)
                        time.sleep(0.05)
                led_pwm.ChangeDutyCycle(0)
                
            print("LEDをパルスさせています")
            t = threading.Thread(target=pulse_thread)
            t.daemon = True
            t.start()
    
    def control_servo(angle):
        """サーボモーターを制御する関数
        
        Args:
            angle: 0-180度の角度
        """
        if not IS_RASPBERRY_PI:
            print(f"サーボモーターシミュレーション: {angle}度")
            return
            
        if not pi.connected:
            print(f"サーボ制御スキップ: pigpioが接続されていません")
            return
            
        # 角度を範囲内に制限
        angle = max(0, min(180, angle))
        
        # 角度をパルス幅に変換(500μs=0度、2500μs=180度)
        pulsewidth = 500 + (angle * 2000 / 180)
        
        pi.set_servo_pulsewidth(SERVO_PIN, pulsewidth)
        print(f"サーボを{angle}度に設定しました(パルス幅: {pulsewidth}μs)")
    
    def analyze_text_for_actions(text):
        """テキストを分析してアクションを決定する関数
        
        Returns:
            dict: LEDとサーボモーターのアクション
        """
        text = text.lower()
        action = {"led": False, "servo": None}  # デフォルトアクション
        
        # テキスト内のキーワードを検索
        for keyword, actions in KEYWORD_ACTIONS.items():
            if keyword in text:
                action = actions
                print(f"キーワード「{keyword}」を検出: {json.dumps(actions, ensure_ascii=False)}")
                break
                
        # テキストに「?」「?」があれば質問として扱う
        if "?" in text or "?" in text:
            action = KEYWORD_ACTIONS.get("質問", action)
            print("質問を検出しました")
            
        return action
    
    def split_text_custom(text, max_length=MAX_CHUNK_LENGTH):
        """テキストを文末や絵文字に基づいてセグメントに分割"""
        pattern = r'(?<=[。!?\U0001F300-\U0001F64F\U0001F680-\U0001F6FF\U00002728])'
        sentences = re.split(pattern, text)
        segments = []
        for sentence in sentences:
            sentence = sentence.strip()
            if not sentence:
                continue
            if len(sentence) > max_length:
                for i in range(0, len(sentence), max_length):
                    segments.append(sentence[i:i+max_length])
            else:
                segments.append(sentence)
        return segments
    
    def generate_tts_audio(text, tts_api_url=TTS_API_URL, model_name=MODEL_NAME):
        """テキストからTTS音声を生成するためにTTS APIを呼び出す"""
        if not text.strip():
            print("テキストが空です。")
            return None
            
        print(f"テキスト「{text}」を音声に変換中...")
        
        # テキストを分析してアクションを決定
        action = analyze_text_for_actions(text)
        
        # LEDとサーボを制御
        if action["led"] is not None:
            control_led(action["led"])
        if action["servo"] is not None:
            control_servo(action["servo"])
        
        segments = split_text_custom(text)
        combined_audio = None
        
        for i, chunk in enumerate(segments, 1):
            if len(chunk.strip()) <= 1:
                continue
                
            print(f"  チャンク {i}/{len(segments)}: 「{chunk}」")
            params = {
                "text": chunk,
                "model_name": model_name,
                "length": AUDIO_PARAMS["length"],
                "split_interval": AUDIO_PARAMS["split_interval"],
                "style_weight": AUDIO_PARAMS["style_weight"]
            }
            
            try:
                response = requests.get(tts_api_url, params=params)
                if response.status_code == 200:
                    audio_segment = AudioSegment.from_file(io.BytesIO(response.content), format="wav")
                    if combined_audio is None:
                        combined_audio = audio_segment
                    else:
                        combined_audio += audio_segment
                    print(f"  チャンク {i} 処理完了")
                else:
                    print(f"  TTS APIエラー(ステータス: {response.status_code})")
            except Exception as e:
                print(f"  TTS生成エラー: {e}")
        
        if combined_audio:
            print("音声生成完了")
            out_buffer = io.BytesIO()
            combined_audio.export(out_buffer, format="wav")
            return out_buffer.getvalue()
        
        print("音声生成に失敗しました")
        return None
    
    def play_audio(audio_data, output_file="tts_output.wav", autoplay=True, delete_after=True):
        """音声データをファイルに保存し、必要に応じて自動再生"""
        if not audio_data:
            print("再生する音声データがありません")
            return False
        
        # 再生中はLEDをパルスさせる(Raspberry Piのみ)
        if IS_RASPBERRY_PI:
            control_led("pulse")
        
        # ファイルに保存
        try:
            with open(output_file, "wb") as f:
                f.write(audio_data)
            print(f"音声ファイルを保存しました: {output_file}")
        except Exception as e:
            print(f"ファイル保存エラー: {e}")
            return False
        
        # 自動再生が有効な場合
        if autoplay:
            try:
                # simpleaudioを使って再生(利用可能な場合)
                if SIMPLEAUDIO_AVAILABLE:
                    try:
                        print("simpleaudioで再生中...")
                        wave_obj = sa.WaveObject.from_wave_file(output_file)
                        play_obj = wave_obj.play()
                        play_obj.wait_done()
                        print("再生完了")
                        
                        if delete_after:
                            try:
                                os.remove(output_file)
                                print("一時ファイルを削除しました")
                            except Exception as e:
                                print(f"ファイル削除エラー: {e}")
                        
                        # 再生終了後はLEDを消灯
                        if IS_RASPBERRY_PI:
                            control_led(False)
                        
                        return True
                    except Exception as e:
                        print(f"simpleaudioでの再生エラー: {e}")
                        print("代替方法で再生を試みます...")
                
                # OS別の再生方法
                system = platform.system()
                print(f"プラットフォーム: {system}での再生を試みます")
                
                if system == "Windows":
                    subprocess.call(["start", os.path.abspath(output_file)], shell=True)
                    time.sleep(5)  # 再生が終わるまで適当に待機
                    print("Windowsでの再生を試みました")
                elif system == "Darwin":  # macOS
                    subprocess.call(["afplay", output_file])
                    print("macOSでの再生完了")
                elif system == "Linux":
                    subprocess.call(["aplay", output_file])
                    print("Linuxでの再生完了")
                
                if delete_after:
                    try:
                        os.remove(output_file)
                        print("一時ファイルを削除しました")
                    except Exception as e:
                        print(f"ファイル削除エラー: {e}")
                
                # 再生終了後はLEDを消灯
                if IS_RASPBERRY_PI:
                    control_led(False)
                
                return True
            except Exception as e:
                print(f"自動再生中にエラーが発生しました: {e}")
                # エラー時もLEDを消灯
                if IS_RASPBERRY_PI:
                    control_led(False)
                return False
        
        return True
    
    def show_current_settings():
        """現在の設定を表示"""
        print("\n=== 現在の設定 ===")
        print(f"TTSサーバーURL: {TTS_API_URL}")
        print(f"モデル名: {MODEL_NAME}")
        print(f"テキスト分割最大長: {MAX_CHUNK_LENGTH}")
        print(f"音声パラメータ:")
        print(f"  - 長さ調整: {AUDIO_PARAMS['length']}")
        print(f"  - スプリット間隔: {AUDIO_PARAMS['split_interval']}")
        print(f"  - スタイルの重み: {AUDIO_PARAMS['style_weight']}")
        
        if IS_RASPBERRY_PI:
            print("\n=== GPIO設定 ===")
            print(f"LED PIN: {LED_PIN}")
            print(f"サーボモーター PIN: {SERVO_PIN}")
            print("\n=== キーワードアクション ===")
            for keyword, action in KEYWORD_ACTIONS.items():
                print(f"「{keyword}」: LED={action['led']}, サーボ={action['servo']}度")
        
        print("===============")
    
    def test_gpio():
        """GPIO機能のテスト"""
        if not IS_RASPBERRY_PI:
            print("Raspberry Piではないため、GPIO機能をテストできません。")
            return
            
        print("\n=== GPIO機能テスト ===")
        print("1. LEDテスト")
        
        print("LEDを点灯します...")
        control_led(True)
        time.sleep(1)
        
        print("LEDを消灯します...")
        control_led(False)
        time.sleep(1)
        
        print("LEDを点滅させます...")
        control_led("blink", 3)
        time.sleep(3.5)
        
        print("LEDをパルスさせます...")
        control_led("pulse", 3)
        time.sleep(3.5)
        
        print("2. サーボモーターテスト")
        
        print("サーボを0度に動かします...")
        control_servo(0)
        time.sleep(1)
        
        print("サーボを90度に動かします...")
        control_servo(90)
        time.sleep(1)
        
        print("サーボを180度に動かします...")
        control_servo(180)
        time.sleep(1)
        
        print("サーボを90度(中央)に戻します...")
        control_servo(90)
        
        print("GPIOテスト完了")
    
    def update_gpio_settings():
        """GPIO設定を更新する"""
        global LED_PIN, SERVO_PIN, KEYWORD_ACTIONS
        
        if not IS_RASPBERRY_PI:
            print("Raspberry Piではないため、GPIO設定を変更できません。")
            return
            
        print("\n=== GPIO設定の変更 ===")
        
        try:
            # ピン設定の変更
            print("\n--- ピン設定 ---")
            new_led_pin = int(input(f"LED PIN [{LED_PIN}]: ").strip() or LED_PIN)
            new_servo_pin = int(input(f"サーボモーター PIN [{SERVO_PIN}]: ").strip() or SERVO_PIN)
            
            # 設定が変更された場合は、GPIOをクリーンアップして再設定
            if new_led_pin != LED_PIN or new_servo_pin != SERVO_PIN:
                LED_PIN = new_led_pin
                SERVO_PIN = new_servo_pin
                
                # クリーンアップ
                cleanup_gpio()
                
                # 再設定
                GPIO.setmode(GPIO.BCM)
                GPIO.setup(LED_PIN, GPIO.OUT)
                GPIO.output(LED_PIN, GPIO.LOW)
                
                led_pwm = GPIO.PWM(LED_PIN, 100)
                led_pwm.start(0)
                
                # pigpioサーボ用再設定
                pi = pigpio.pi()
                if pi.connected:
                    pi.set_servo_pulsewidth(SERVO_PIN, 1500)
                    
                print("GPIO設定を更新しました。")
            
            # キーワードアクションの変更
            print("\n--- キーワードアクション設定 ---")
            print("現在のキーワードアクション:")
            for keyword, action in KEYWORD_ACTIONS.items():
                print(f"「{keyword}」: LED={action['led']}, サーボ={action['servo']}度")
            
            update_keyword = input("\nキーワードアクションを更新しますか? (y/n): ").strip().lower()
            if update_keyword == 'y':
                keyword = input("更新するキーワード(新規の場合は新しいキーワード): ").strip()
                if keyword:
                    led_action = input("LEDアクション (True/False/blink/pulse): ").strip()
                    led_action = eval(led_action) if led_action in ["True", "False"] else led_action
                    
                    try:
                        servo_angle = int(input("サーボ角度 (0-180): ").strip())
                        servo_angle = max(0, min(180, servo_angle))
                        
                        KEYWORD_ACTIONS[keyword] = {"led": led_action, "servo": servo_angle}
                        print(f"キーワード「{keyword}」のアクションを更新しました。")
                    except ValueError:
                        print("サーボ角度には数値を入力してください。")
        
        except ValueError as e:
            print(f"入力エラー: 数値を入力してください: {e}")
    
    def main():
        global TTS_API_URL, MODEL_NAME, MAX_CHUNK_LENGTH, AUDIO_PARAMS
        
        print("="*50)
        print("=== Raspberry Pi TTS-API & GPIO制御テスト ===")
        print("="*50)
        print("※ 設定変更コマンド:")
        print("  - 'exit': プログラムを終了")
        print("  - 'url': TTSサーバーURLを変更")
        print("  - 'model': モデルを変更")
        print("  - 'settings': 現在の設定を表示")
        print("  - 'params': 音声パラメータを変更")
        if IS_RASPBERRY_PI:
            print("  - 'gpio': GPIO設定を変更")
            print("  - 'test': GPIO機能をテスト")
        print("="*50)
        
        show_current_settings()
        
        try:
            # メインループ
            while True:
                user_input = input("\n話させるテキスト(もしくはコマンド): ").strip()
                
                # 終了コマンド
                if user_input.lower() == "exit":
                    print("プログラムを終了します。")
                    break
                    
                # URL変更コマンド
                elif user_input.lower() == "url":
                    new_url = input("新しいTTS-API URLを入力してください: ").strip()
                    if new_url:
                        TTS_API_URL = new_url
                        print(f"TTS-API URLを変更しました: {TTS_API_URL}")
                    continue
                    
                # モデル変更コマンド
                elif user_input.lower() == "model":
                    new_model = input("新しいモデル名を入力してください: ").strip()
                    if new_model:
                        MODEL_NAME = new_model
                        print(f"モデルを変更しました: {MODEL_NAME}")
                    continue
                    
                # 設定表示コマンド
                elif user_input.lower() == "settings":
                    show_current_settings()
                    continue
                    
                # パラメータ変更コマンド
                elif user_input.lower() == "params":
                    print("\n=== 音声パラメータの変更 ===")
                    try:
                        new_length = float(input(f"長さ調整 [{AUDIO_PARAMS['length']}]: ").strip() or str(AUDIO_PARAMS['length']))
                        new_split = float(input(f"スプリット間隔 [{AUDIO_PARAMS['split_interval']}]: ").strip() or str(AUDIO_PARAMS['split_interval']))
                        new_weight = float(input(f"スタイルの重み [{AUDIO_PARAMS['style_weight']}]: ").strip() or str(AUDIO_PARAMS['style_weight']))
                        new_chunk = int(input(f"テキスト分割最大長 [{MAX_CHUNK_LENGTH}]: ").strip() or str(MAX_CHUNK_LENGTH))
                        
                        AUDIO_PARAMS["length"] = new_length
                        AUDIO_PARAMS["split_interval"] = new_split
                        AUDIO_PARAMS["style_weight"] = new_weight
                        MAX_CHUNK_LENGTH = new_chunk
                        
                        print("パラメータを更新しました。")
                        show_current_settings()
                    except ValueError as e:
                        print(f"入力エラー: 数値を入力してください: {e}")
                    continue
                    
                # GPIO設定変更コマンド(Raspberry Piのみ)
                elif user_input.lower() == "gpio" and IS_RASPBERRY_PI:
                    update_gpio_settings()
                    continue
                    
                # GPIO機能テストコマンド(Raspberry Piのみ)
                elif user_input.lower() == "test" and IS_RASPBERRY_PI:
                    test_gpio()
                    continue
                
                # TTS処理と再生
                audio_data = generate_tts_audio(
                    text=user_input,
                    tts_api_url=TTS_API_URL,
                    model_name=MODEL_NAME
                )
                
                play_audio(
                    audio_data=audio_data,
                    output_file=output_file,
                    autoplay=True,
                    delete_after=True
                )
        
        finally:
            # プログラム終了時にGPIOをクリーンアップ
            if IS_RASPBERRY_PI:
                cleanup_gpio()
                print("GPIO設定をクリーンアップしました。")
    
    if __name__ == "__main__":
        main()

    2. 解説

    ラズベリーパイでTTSとGPIO制御を組み合わせるために、以下の機能を追加しました:

    1) 追加したモジュール

    ①RPi.GPIO - 基本的なGPIO制御用
    ②pigpio - サーボモーターをより正確に制御するため
    ③threading - LEDの点滅やパルス効果を非同期で実行するため


    2) 主な追加機能

    ①自動環境検出

    • Raspberry Pi環境かどうかを自動検出し、一般PCでも実行可能なようにしました

    • Raspberry Pi以外の環境では、GPIO関連の機能はシミュレーションモードで動作します

    ②GPIO制御機能

    • control_led() - LEDの点灯、消灯、点滅、パルス効果を制御

    • control_servo() - サーボモーターの角度を制御(0〜180度)

    • test_gpio() - GPIO機能のテスト用コマンド

    ③テキスト解析とアクション機能

    • analyze_text_for_actions() - テキスト内のキーワードを分析し、対応するアクションを決定

    • KEYWORD_ACTIONS - キーワードとそれに対応するLEDとサーボのアクションを定義

    ④設定機能の拡張

    • GPIO関連の設定を変更するコマンド (gpio)

    • GPIOテスト用コマンド (test)

    • 現在の設定表示にGPIO情報を追加


    3) 具体的な使用例

    このコードでは以下のようなことが可能になります:

    ①音声再生に連動したLED点灯

    • 音声再生中はLEDがパルス点灯し、再生が終わると消灯します

    ②テキスト内容に応じた表現

    • 「喜び」という言葉があればLEDが点灯し、サーボが90度に動きます

    • 「悲しみ」という言葉があればLEDが消灯し、サーボが0度に動きます

    • 「驚き」という言葉があればLEDが点滅し、サーボが180度に動きます

    • 質問文(「?」や「?」を含む)では、LEDがパルス点灯し、サーボが45度に動きます


    4) 使用方法

    Raspberry Piでのセットアップ手順:

    ①必要なライブラリをインストール

    sudo apt-get update
    sudo apt-get install python3-rpi.gpio
    sudo apt-get install pigpio python3-pigpio
    sudo pip3 install pydub simpleaudio requests

    ②pigpioデーモンを起動

    sudo pigpiod

    ③ハードウェア接続

    • LED: GPIO 17(デフォルト設定、変更可能)に接続

    • サーボモーター: GPIO 18(デフォルト設定、変更可能)に接続

    ④コードを実行

    python3 tts_gpio_control.py

    ⑤コマンド

    • test: GPIO機能をテスト

    • gpio: GPIO設定を変更

    • settings: 現在の設定を表示

    • その他、元のコードと同じコマンドも使用可能

    これでAIのレスポンスに合わせてLEDが点灯したり、サーボモーターが動いたりする対話型のシステムを作ることができます。


    これを応用して「赤あげて」「白あげて」の旗揚げゲームなんかは簡単に作れそうですね!

     
     
    猫とオカルトと宇宙人、そしてAIが好きな普通のおじさんです。

    あなたへのおすすめ