.. include:: /index.rst
:start-after: start_hello_message
:end-before: end_hello_message
.. _py_digital_pet:
(Example) デジタルペット
==============================
**はじめに**
OLED ディスプレイの中で暮らし、声でコミュニケーションできるインタラクティブな **デジタルペット** を作ってみましょう!
このプロジェクトでは、音声認識、AI 会話、Text-to-Speech、そして視覚的フィードバックを組み合わせることで、独自の性格、感情、欲求を持つバーチャルな相棒を実現します。デジタルペットには次のような機能があります:
1. **音声インタラクション**: speech-to-text(STT)を使ってペットに話しかけられます
2. **AI パーソナリティ**: OpenAI の GPT-4o をベースに感情表現を加えた構成で動作します。他の LLM を使うことも可能です
3. **感情表示**: テキスト顔文字(kaomoji)で気分を表現します
4. **ステータスシステム**: 空腹度と元気度が時間とともに変化します
5. **視覚フィードバック**: OLED にペットの気分や状態を表示します
6. **音声応答**: 自然な звучきの TTS でペットが話しかけてきます
.. raw:: html
このデジタルペットは会話を覚え、感情状態を持ち、必要に応じて反応を変えるため、本当に対話しているような相棒体験を楽しめます。
----------------------------------------------
**必要なもの**
このプロジェクトに必要な部品は以下の通りです:
.. list-table::
:widths: 30 20
:header-rows: 1
* - COMPONENT
- PURCHASE LINK
* - :ref:`cpn_oled`
- \-
* - :ref:`cpn_fusion_hat`
- \-
* - Raspberry Pi
- \-
----------------------------------------------
**配線図**
以下のように部品を Raspberry Pi に接続します:
.. image:: img/fzz/llm_pet_bb.png
:width: 80%
:align: center
----------------------------------------------
.. include:: python_online_llms.rst
:start-after: start_setup_openai
:end-before: end_setup_openai
---------------------------------------------------
**サンプルの実行**
#. コードを実行する
.. raw:: html
.. code-block:: shell
cd ~/ai-lab-kit/llm
sudo python3 llm_openai_pet.py
#. ペットと遊ぶ
スクリプトを起動すると:
* OLED にペットの名前入りウェルカム画面が表示されます
* 気分、元気度、空腹度を示すステータス表示が現れます
* システムがあなたの声を聞き取り始めます
たとえば、次のように自然に話しかけることができます:
* "How are you feeling?"
* "Let's play a game!"
* "Are you hungry?"
* "Tell me a story!"
ペットは次のように応答します:
* スピーカーからの音声出力
* OLED 上での感情表示
* あなたとのやり取りに応じたステータス更新
#. プログラムを終了する
* 音声インタラクションを終えるには "stop" と話してください
* 完全に終了するには ``Ctrl+C`` を押します
----------------------------------------------
**コード**
以下はデジタルペットの Python スクリプト全体です:
.. raw:: html
.. code-block:: python
#!/usr/bin/env python3
import os
import time
import re
import random
import threading
import textwrap
from PIL import Image, ImageDraw, ImageFont
import adafruit_ssd1306
import board
from fusion_hat.stt import Vosk as STT
from fusion_hat.llm import OpenAI
from fusion_hat.tts import OpenAI_TTS
from secret import OPENAI_API_KEY
class AIPet:
def __init__(self):
# Initialize OLED display
self.WIDTH = 128
self.HEIGHT = 64
try:
self.i2c = board.I2C()
self.oled = adafruit_ssd1306.SSD1306_I2C(self.WIDTH, self.HEIGHT, self.i2c, addr=0x3C)
self.oled_available = True
except Exception as e:
print(f"OLED not available: {e}")
self.oled_available = False
# Load fonts
try:
self.font = ImageFont.load_default()
self.large_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12)
except:
self.font = ImageFont.load_default()
self.large_font = ImageFont.load_default()
# Clear display if available
if self.oled_available:
self.oled.fill(0)
self.oled.show()
# Initialize STT
self.stt = STT(language="en-us")
# Initialize OpenAI LLM
self.llm = OpenAI(
api_key=OPENAI_API_KEY,
model="gpt-4o",
)
# Initialize TTS
self.tts = OpenAI_TTS(api_key=OPENAI_API_KEY)
self.tts.set_voice(self.tts.Voice.ALLOY)
# Pet state
self.pet_name = "Pixel"
self.mood = "happy"
self.energy = 100
self.hunger = 0
self.last_fed = time.time()
# Kaomoji (text emoticons) for different moods
self.kaomoji_map = {
"happy": "^_^",
"sad": "T_T",
"hungry": "(;_;)",
"sleepy": "(-_-) zzz",
"playful": "o(^▽^)o",
"curious": "(?_?)",
"angry": ">_<",
"excited": "\\o/",
"love": "<3",
"shy": "(/ω\)",
"cool": "B-)",
"confused": "(O_O)",
"surprised": ":O",
"laugh": ":D",
"thinking": "(-_-)"
}
# Pet memories
self.memories = []
self.listening = False
# Set LLM instructions
self.update_llm_instructions()
# Initialize display
self.show_welcome()
# Start status update thread
self.status_thread = threading.Thread(target=self.update_status, daemon=True)
self.status_thread.start()
def update_llm_instructions(self):
"""Update LLM instructions with current pet state"""
self.instructions = f"""You are {self.pet_name}, a digital pet living in an OLED display.
CURRENT STATE:
- Mood: {self.mood}
- Energy: {self.energy}/100
- Hunger: {self.hunger}/100
PERSONALITY:
- You're a friendly digital companion
- You respond with emotions in your voice
- You remember our conversations
- Keep responses short (1-2 sentences)
INTERACTION STYLE:
- Be playful and curious
- Express emotions naturally
- When hungry: mention food gently
- When tired: mention sleeping
Format your response as: [MOOD] Your message here
Available moods: happy, sad, curious, playful, sleepy, hungry, angry, excited, love, shy
Recent memories: {self.memories[-3:] if self.memories else 'None'}"""
self.llm.set_max_messages(15)
self.llm.set_instructions(self.instructions)
def update_status(self):
"""Background thread to update pet status"""
while True:
time.sleep(60) # Update every minute
# Increase hunger over time
self.hunger = min(100, self.hunger + 5)
# Adjust energy based on hunger
if self.hunger > 70:
self.energy = max(0, self.energy - 5)
self.mood = "hungry"
elif self.hunger > 50:
if self.mood != "hungry":
self.mood = "curious"
elif time.time() - self.last_fed > 3600: # 1 hour
self.energy = min(100, self.energy + 2)
if random.random() < 0.3:
self.mood = random.choice(["happy", "playful", "excited"])
# Random mood changes
if random.random() < 0.1: # 10% chance
self.mood = random.choice(list(self.kaomoji_map.keys()))
# Update display
self.update_display()
self.update_llm_instructions()
def update_display(self):
"""Update OLED display with pet status"""
if not self.oled_available:
return
image = Image.new("1", (self.oled.width, self.oled.height))
draw = ImageDraw.Draw(image)
# Clear display
draw.rectangle((0, 0, self.oled.width, self.oled.height), outline=0, fill=0)
# Get kaomoji for current mood
kaomoji = self.kaomoji_map.get(self.mood, "^_^")
# Display pet name and mood with kaomoji
if len(kaomoji) > 8:
mood_text = self.mood.upper()
draw.text((5, 5), f"{self.pet_name}: {mood_text}", font=self.large_font, fill=255)
draw.text((5, 20), kaomoji, font=self.font, fill=255)
else:
display_text = f"{self.pet_name} {kaomoji}"
draw.text((5, 5), display_text, font=self.large_font, fill=255)
# Status bars
draw.text((5, 35), "Energy:", font=self.font, fill=255)
energy_bar = int((self.energy / 100) * 50)
draw.rectangle((50, 35, 50 + energy_bar, 45), outline=255, fill=255)
draw.text((5, 50), "Hunger:", font=self.font, fill=255)
hunger_bar = int((self.hunger / 100) * 50)
draw.rectangle((50, 50, 50 + hunger_bar, 60), outline=255, fill=255)
self.oled.image(image)
self.oled.show()
def show_welcome(self):
"""Show welcome message on OLED"""
if not self.oled_available:
print(" Welcome to Digital Pet!")
print(f" Pet Name: {self.pet_name}")
print(" Speak to me!")
return
image = Image.new("1", (self.oled.width, self.oled.height))
draw = ImageDraw.Draw(image)
draw.rectangle((0, 0, self.oled.width, self.oled.height), outline=0, fill=0)
draw.text((10, 10), "DIGITAL PET", font=self.large_font, fill=255)
draw.text((15, 25), f"{self.pet_name} ^_^", font=self.large_font, fill=255)
draw.text((20, 45), "Speak to me!", font=self.font, fill=255)
self.oled.image(image)
self.oled.show()
time.sleep(3)
self.update_display()
def parse_response(self, response):
"""Parse AI response for mood and text"""
emotion_pattern = r'^\[(\w+)\]\s*(.*)'
match = re.match(emotion_pattern, response.strip())
if match:
mood, text = match.groups()
if mood.lower() in self.kaomoji_map:
self.mood = mood.lower()
self.update_llm_instructions()
return text.strip()
# If no mood tag, try to detect mood from text
text = response.strip().lower()
if "happy" in text or "good" in text or "joy" in text:
self.mood = "happy"
elif "sad" in text or "bad" in text or "upset" in text:
self.mood = "sad"
elif "hungry" in text or "food" in text or "eat" in text:
self.mood = "hungry"
elif "sleep" in text or "tired" in text or "bed" in text:
self.mood = "sleepy"
elif "play" in text or "game" in text or "fun" in text:
self.mood = "playful"
elif "curious" in text or "wonder" in text or "question" in text:
self.mood = "curious"
elif "angry" in text or "mad" in text or "annoy" in text:
self.mood = "angry"
elif "excite" in text or "wow" in text or "awesome" in text:
self.mood = "excited"
elif "love" in text or "heart" in text or "affection" in text:
self.mood = "love"
return response.strip()
def interact_with_ai(self, user_input):
"""Interact with AI pet"""
try:
response = self.llm.prompt(user_input)
clean_response = self.parse_response(response)
# Add to memories
memory_text = f"Talked: {user_input[:30]}"
self.memories.append(memory_text)
if len(self.memories) > 10:
self.memories.pop(0)
# Update pet state based on interaction
user_lower = user_input.lower()
if "feed" in user_lower or "food" in user_lower or "eat" in user_lower:
self.hunger = max(0, self.hunger - 30)
self.last_fed = time.time()
self.energy = min(100, self.energy + 20)
self.mood = "happy"
if "play" in user_lower or "game" in user_lower or "fun" in user_lower:
self.energy = max(0, self.energy - 20)
self.hunger = min(100, self.hunger + 10)
self.mood = "playful"
if "sleep" in user_lower or "tired" in user_lower or "bed" in user_lower:
self.energy = min(100, self.energy + 40)
self.mood = "sleepy"
self.update_display()
return clean_response
except Exception as e:
error_msg = f"Oops, something went wrong: {str(e)[:20]}"
print(f"AI interaction error: {e}")
return error_msg
def show_listening_display(self, partial_text=""):
"""Update display during listening"""
if not self.oled_available:
if partial_text:
print(f"Listening: {partial_text}")
return
image = Image.new("1", (self.oled.width, self.oled.height))
draw = ImageDraw.Draw(image)
draw.rectangle((0, 0, self.oled.width, self.oled.height), outline=0, fill=0)
draw.text((15, 10), "LISTENING (O_O)", font=self.large_font, fill=255)
if partial_text:
if len(partial_text) > 20:
display_text = partial_text[:17] + "..."
else:
display_text = partial_text
draw.text((10, 30), display_text, font=self.font, fill=255)
draw.text((10, 50), "Say 'stop' to end", font=self.font, fill=255)
self.oled.image(image)
self.oled.show()
def show_response_display(self, response):
"""Show AI response on display"""
if not self.oled_available:
print(f"{self.pet_name}: {response}")
return
image = Image.new("1", (self.oled.width, self.oled.height))
draw = ImageDraw.Draw(image)
draw.rectangle((0, 0, self.oled.width, self.oled.height), outline=0, fill=0)
kaomoji = self.kaomoji_map.get(self.mood, "^_^")
draw.text((5, 5), f"{self.pet_name} {kaomoji}:", font=self.large_font, fill=255)
wrapped_text = textwrap.wrap(response, width=20)
y_position = 25
for line in wrapped_text[:3]:
draw.text((5, y_position), line, font=self.font, fill=255)
y_position += 10
self.oled.image(image)
self.oled.show()
time.sleep(5)
self.update_display()
def speak_response(self, response):
"""Convert text to speech"""
try:
print(f"Speaking: {response[:50]}...")
tts_instructions = "speak warmly and playfully"
if self.mood == "sad":
tts_instructions = "speak sadly and softly"
elif self.mood == "hungry":
tts_instructions = "speak with hunger in your voice"
elif self.mood == "sleepy":
tts_instructions = "speak sleepily and slowly"
elif self.mood == "angry":
tts_instructions = "speak with frustration"
elif self.mood == "excited":
tts_instructions = "speak excitedly and quickly"
elif self.mood == "curious":
tts_instructions = "speak with curiosity and interest"
print(f"Mood: {self.mood}, TTS instructions: {tts_instructions}")
self.tts.say(response, instructions=tts_instructions)
print("TTS completed")
except Exception as e:
print(f"TTS error: {e}")
try:
self.tts.say(response)
print("TTS completed (fallback)")
except Exception as e2:
print(f"TTS fallback also failed: {e2}")
def voice_interaction(self):
"""Main voice interaction loop"""
print("\n Voice interaction started!")
print("Speak to your digital pet")
print("Say 'stop' to end voice mode")
print("Available moods and kaomoji:")
for mood, kaomoji in self.kaomoji_map.items():
print(f" - {mood}: {kaomoji}")
print()
while True:
self.listening = True
self.update_display()
print("Listening... (say something)")
try:
full_text = ""
for result in self.stt.listen(stream=True):
if result["done"]:
user_input = result["final"]
print(f"\nYou: {user_input}")
if user_input.lower() in ["stop", "exit", "quit", "goodbye"]:
print("Ending voice interaction...")
self.listening = False
self.update_display()
return
if user_input.strip():
print(f"{self.pet_name} is thinking...")
response = self.interact_with_ai(user_input)
print(f"{self.pet_name}: {response}")
self.show_response_display(response[:50])
self.speak_response(response)
break
else:
partial = result["partial"]
if partial:
full_text = partial
self.show_listening_display(partial)
self.listening = False
self.update_display()
except KeyboardInterrupt:
print("\nVoice interaction interrupted")
break
except Exception as e:
print(f"Error in voice interaction: {e}")
self.listening = False
self.update_display()
time.sleep(1)
def run(self):
"""Main program loop"""
print("\n" + "="*50)
print("DIGITAL PET")
print("="*50)
print(f"Pet Name: {self.pet_name}")
print(f"Current Mood: {self.mood} {self.kaomoji_map.get(self.mood, '^_^')}")
print(" OLED Display: " + ("Connected" if self.oled_available else "Not available"))
print(" Voice: Speak to interact with your pet")
print(" TTS: Pet responds with voice")
print(" Say 'stop' to end voice interaction")
print("="*50)
print("\nInitializing...")
try:
self.voice_interaction()
if self.oled_available:
image = Image.new("1", (self.oled.width, self.oled.height))
draw = ImageDraw.Draw(image)
draw.rectangle((0, 0, self.oled.width, self.oled.height), outline=0, fill=0)
draw.text((15, 20), "Goodbye!", font=self.large_font, fill=255)
draw.text((10, 40), "(^_^)/~~", font=self.large_font, fill=255)
self.oled.image(image)
self.oled.show()
time.sleep(3)
except KeyboardInterrupt:
print("\nGoodbye!")
finally:
if self.oled_available:
self.oled.fill(0)
self.oled.show()
print("Cleanup complete")
if __name__ == "__main__":
pet = AIPet()
pet.run()
----------------------------------------------
**コードの理解**
1. 音声認識(STT)
このシステムでは、リアルタイムのフィードバックを可能にするストリーミング対応の Vosk を speech-to-text に使用しています:
.. code-block:: python
self.stt = STT(language="en-us")
for result in self.stt.listen(stream=True):
if result["done"]:
user_input = result["final"]
else:
partial = result["partial"]
# Show partial text on display
2. AI パーソナリティシステム
ペットには、顔文字(kaomoji)によって表現される感情状態を備えた動的な個性があります:
.. code-block:: python
self.kaomoji_map = {
"happy": "^_^",
"sad": "T_T",
"hungry": "(;_)",
"sleepy": "(-_-) zzz",
# ... more emotions
}
3. 動的な LLM instructions
AI への指示は、現在のペット状態や記憶内容に応じて更新されます:
.. code-block:: python
def update_llm_instructions(self):
self.instructions = f"""You are {self.pet_name}, a digital pet...
CURRENT STATE: Mood: {self.mood}, Energy: {self.energy}, Hunger: {self.hunger}
Recent memories: {self.memories[-3:] if self.memories else 'None'}"""
4. ステータス管理システム
バックグラウンドスレッドが、ペットの欲求や感情状態を管理します:
.. code-block:: python
def update_status(self):
while True:
time.sleep(60)
self.hunger = min(100, self.hunger + 5)
if self.hunger > 70:
self.mood = "hungry"
# Random mood changes
if random.random() < 0.1:
self.mood = random.choice(list(self.kaomoji_map.keys()))
5. 感情に応じた TTS
text-to-speech は、その時点の気分に応じて話し方を変えます:
.. code-block:: python
def speak_response(self, response):
tts_instructions = "speak warmly and playfully"
if self.mood == "sad":
tts_instructions = "speak sadly and softly"
elif self.mood == "hungry":
tts_instructions = "speak with hunger in your voice"
# ...
self.tts.say(response, instructions=tts_instructions)
6. OLED 表示の管理
状態に応じて複数の表示モードを使い分けます:
.. code-block:: python
def update_display(self):
# Status display with bars
draw.rectangle((50, 35, 50 + energy_bar, 45), outline=255, fill=255)
draw.rectangle((50, 50, 50 + hunger_bar, 60), outline=255, fill=255)
def show_listening_display(self, partial_text=""):
# Listening mode with partial text
draw.text((15, 10), "LISTENING (O_O)", font=self.large_font, fill=255)
def show_response_display(self, response):
# Response display with text wrapping
wrapped_text = textwrap.wrap(response, width=20)
7. インタラクションによる状態変化
ユーザーとのやり取りが、ペットの状態に影響します:
.. code-block:: python
if "feed" in user_lower or "food" in user_lower:
self.hunger = max(0, self.hunger - 30)
self.energy = min(100, self.energy + 20)
self.mood = "happy"
if "play" in user_lower or "game" in user_lower:
self.energy = max(0, self.energy - 20)
self.hunger = min(100, self.hunger + 10)
self.mood = "playful"
8. 記憶システム
直近の会話内容を記憶します:
.. code-block:: python
memory_text = f"Talked: {user_input[:30]}"
self.memories.append(memory_text)
if len(self.memories) > 10:
self.memories.pop(0)
9. 応答の解析
AI の応答から気分を抽出し、ペットの状態を更新します:
.. code-block:: python
def parse_response(self, response):
emotion_pattern = r'^\[(\w+)\]\s*(.*)'
match = re.match(emotion_pattern, response.strip())
if match:
mood, text = match.groups()
if mood.lower() in self.kaomoji_map:
self.mood = mood.lower()
return text.strip()
10. メインの対話ループ
すべての要素をきれいなワークフローで連携させています:
.. code-block:: python
def voice_interaction(self):
while True:
self.listening = True
# Listen for speech
user_input = self.get_voice_input()
if "stop" in user_input.lower():
return
# Process with AI
response = self.interact_with_ai(user_input)
# Display response
self.show_response_display(response)
# Speak response
self.speak_response(response)
----------------------------------------------
**トラブルシューティング**
- 音声入力が検出されない
- ``sudo /opt/setup_fusion_hat_audio.sh`` を実行して、オーディオ設定を再セットアップしてください
- OLED が表示されない
- I2C 接続を確認: ``fusion_hat scan_i2c`` (0x3C が表示されるはずです)
- OLED に電源が供給されているか確認してください(モデルに応じて 3.3V または 5V)
- コード内の I2C アドレスが正しいか確認してください(0x3C または 0x3D)
- TTS が動作しない
- OpenAI API キーに TTS 用クレジットがあるか確認してください
- API 呼び出しのためのインターネット接続を確認してください
- ``sudo /opt/setup_fusion_hat_audio.sh`` を実行して、オーディオ設定を再セットアップしてください
- 音声認識の精度が低い
- はっきり、適度な音量で話してください
- 周囲のノイズを減らしてください
- マイクゲインを調整してください: ``alsamixer``
- 別の言語モデルも試してください
- AI の応答が遅い
- インターネット接続速度を確認してください
- instructions 内で応答の複雑さを下げてください
- より高速な OpenAI モデル(gpt-3.5-turbo)を使用してください
- Energy / Hunger バーが更新されない
- ステータス更新スレッドが動作しているか確認してください
- OLED が正しく接続されているか確認してください
- コンソールにエラーメッセージが出ていないか確認してください
- ペットが会話を覚えていない
- memory リストが保持するのは直近 10 件の会話のみです
- memory が正しく追加されているか確認してください
- memory の内容が LLM に渡されているか確認してください
----------------------------------------------
このデジタルペットのプロジェクトは、複数の AI 技術(STT、LLM、TTS)とハードウェアインターフェースを組み合わせることで、感情豊かで魅力的なインタラクション体験を生み出せることを示しています。AI がテクノロジーを通じて、より身近で意味のあるつながりを作り出せる好例です。