#!/usr/bin/env python3
"""
Robot Reporter — tours the room, captures photos, narrates what it sees.
Combines movement (Days 1-2), TTS (Day 2), OpenAI API (Day 3), VLM (Day 4).
Usage: sudo python3 robot_reporter.py
"""
import sys, os, io, time, base64
sys.path.insert(0, os.path.expanduser("~/camp"))
from secret import OPENAI_KEY
from openai import OpenAI
from picamera2 import Picamera2
from picarx import Picarx
# ── CUSTOMIZE THESE ──────────────────────────────────────────
REPORTER_PERSONA = """You are a news reporter doing a live broadcast from inside a room.
Describe what you see in 1-2 exciting sentences as if reporting live on TV.
Be specific about objects, colors, and anything interesting.
Always start with 'And here we can see...' or 'Looking around, I notice...'"""
INTRO_LINE = "Good afternoon! This is PiCar X reporting live. Let me take you on a tour!"
TOUR_STOPS = [
("Stop 1: Straight ahead", "forward", {"speed": 25, "duration": 2}),
("Stop 2: Look right", "turn_right", {"degrees": 90}),
("Stop 3: Move forward", "forward", {"speed": 25, "duration": 2}),
("Stop 4: Look left", "turn_left", {"degrees": 90}),
("Stop 5: Final position", "forward", {"speed": 25, "duration": 1}),
]
# ── END CUSTOMIZATION ────────────────────────────────────────
def speak(text, speed=150):
safe = text.replace("'", "'\\''").replace('"', '\\"')
os.system(f"espeak -s {speed} '{safe}' 2>/dev/null")
def capture_frame(cam):
stream = io.BytesIO()
cam.capture_file(stream, format="jpeg")
stream.seek(0)
return base64.b64encode(stream.read()).decode("utf-8")
def ask_vision(client, image_b64, persona):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": persona},
{"role": "user", "content": [
{"type": "text", "text": "Describe what you see in this image."},
{"type": "image_url", "image_url": {
"url": f"data:image/jpeg;base64,{image_b64}", "detail": "low"}}]}],
max_tokens=150)
return response.choices[0].message.content.strip()
def do_movement(px, action, args):
if action == "forward":
px.forward(args.get("speed", 25)); time.sleep(args.get("duration", 2)); px.stop()
elif action == "backward":
px.backward(args.get("speed", 25)); time.sleep(args.get("duration", 2)); px.stop()
elif action == "turn_left":
d = args.get("degrees", 90); px.set_dir_servo_angle(-35)
px.forward(25); time.sleep(d / 30.0); px.stop(); px.set_dir_servo_angle(0)
elif action == "turn_right":
d = args.get("degrees", 90); px.set_dir_servo_angle(35)
px.forward(25); time.sleep(d / 30.0); px.stop(); px.set_dir_servo_angle(0)
elif action == "pause":
time.sleep(args.get("duration", 2))
time.sleep(0.5)
def main():
print("=" * 50)
print(" PiCar-X — Robot Reporter")
print("=" * 50)
px = Picarx(); px.stop(); px.set_dir_servo_angle(0)
cam = Picamera2()
cam.configure(cam.create_still_configuration(main={"size": (640, 480)}))
cam.start(); time.sleep(2)
client = OpenAI(api_key=OPENAI_KEY)
print(f"\n Tour has {len(TOUR_STOPS)} stops.")
print(" Press Ctrl-C at any time to stop.\n")
input(" Press ENTER to start the tour...")
print(f"\n Reporter: {INTRO_LINE}")
speak(INTRO_LINE); time.sleep(1)
try:
for i, (desc, action, args) in enumerate(TOUR_STOPS):
print(f"\n ── {desc} ({i+1}/{len(TOUR_STOPS)}) ──")
print(f" Moving: {action} {args}")
do_movement(px, action, args)
print(" Capturing photo...")
image_b64 = capture_frame(cam)
print(" Sending to GPT-4o-mini vision...")
try:
narration = ask_vision(client, image_b64, REPORTER_PERSONA)
except Exception as e:
narration = f"Technical difficulties at stop {i+1}!"
print(f" ERROR: {e}")
print(f" Reporter: {narration}")
speak(narration); time.sleep(1)
outro = "And that concludes our tour! This is PiCar X, signing off."
print(f"\n Reporter: {outro}")
speak(outro)
except KeyboardInterrupt:
print("\n\n Tour interrupted!")
speak("Tour stopped. Signing off!")
finally:
px.stop(); px.set_dir_servo_angle(0)
cam.stop(); cam.close()
print("\n Tour complete!\n")
if __name__ == "__main__":
main()