#!/usr/bin/env python3
"""
Vision Chatbot — voice/keyboard chatbot that can SEE.
Captures a photo with every question and sends it to GPT-4o-mini vision.
Usage:
python3 vision_chatbot.py # voice mode
python3 vision_chatbot.py --type # keyboard mode
"""
import sys, os, io, json, time, base64, ctypes
try:
asound = ctypes.cdll.LoadLibrary("libasound.so.2")
c_handler = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_int,
ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p)
asound.snd_lib_error_set_handler(c_handler(lambda *_: None))
except: pass
sys.path.insert(0, os.path.expanduser("~/camp"))
from secret import OPENAI_KEY
from openai import OpenAI
from picamera2 import Picamera2
try:
import vosk, pyaudio
except ImportError:
vosk = pyaudio = None
MODEL_PATH = os.path.expanduser("~/camp/vosk-model")
SYSTEM_PROMPT = """You are a friendly robot with a camera. Describe what you see
clearly in 1-3 sentences. Be specific about colors, shapes, and objects."""
def speak(text, speed=160):
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 find_mic():
pa = pyaudio.PyAudio(); usb_idx = any_idx = None
for i in range(pa.get_device_count()):
info = pa.get_device_info_by_index(i)
if info["maxInputChannels"] < 1: continue
if any_idx is None: any_idx = i
if "usb" in info["name"].lower(): usb_idx = i
idx = usb_idx if usb_idx is not None else any_idx
rate = int(pa.get_device_info_by_index(idx)["defaultSampleRate"])
pa.terminate(); return idx, rate
def listen_once(recognizer, stream, chunk, timeout=10):
start = time.time()
while time.time() - start < timeout:
data = stream.read(chunk, exception_on_overflow=False)
if recognizer.AcceptWaveform(data):
text = json.loads(recognizer.Result()).get("text", "").strip()
if text: return text
return json.loads(recognizer.FinalResult()).get("text", "").strip()
def ask_vision(client, image_b64, text, history):
messages = history.copy()
messages.append({"role": "user", "content": [
{"type": "text", "text": text},
{"type": "image_url", "image_url": {
"url": f"data:image/jpeg;base64,{image_b64}", "detail": "low"}}]})
response = client.chat.completions.create(
model="gpt-4o-mini", messages=messages, max_tokens=200)
answer = response.choices[0].message.content.strip()
history.append({"role": "user", "content": text})
history.append({"role": "assistant", "content": answer})
return answer
def main():
type_mode = "--type" in sys.argv or "-t" in sys.argv
cam = Picamera2()
cam.configure(cam.create_still_configuration(main={"size": (640, 480)}))
cam.start(); time.sleep(2)
audio_stream = pa = recognizer = None; chunk = 0
if not type_mode and vosk and pyaudio and os.path.isdir(MODEL_PATH):
mic_idx, mic_rate = find_mic(); chunk = mic_rate // 4
vosk.SetLogLevel(-1)
recognizer = vosk.KaldiRecognizer(vosk.Model(MODEL_PATH), mic_rate)
pa = pyaudio.PyAudio()
audio_stream = pa.open(format=pyaudio.paInt16, channels=1, rate=mic_rate,
input=True, input_device_index=mic_idx, frames_per_buffer=chunk)
else:
type_mode = True
client = OpenAI(api_key=OPENAI_KEY)
history = [{"role": "system", "content": SYSTEM_PROMPT}]
speak("Hello! I can see through my camera. Ask me what I see!")
try:
while True:
if type_mode:
text = input("\n You: ").strip()
else:
sys.stdout.write(" 🎤 Listening... "); sys.stdout.flush()
text = listen_once(recognizer, audio_stream, chunk)
if not text: sys.stdout.write("(nothing heard)\n"); continue
print(f"\n You: {text}")
if not text: continue
if text.lower() in ("goodbye", "bye", "quit", "exit", "q"):
speak("Goodbye!"); break
image_b64 = capture_frame(cam)
answer = ask_vision(client, image_b64, text, history)
print(f" Robot: {answer}")
speak(answer)
except KeyboardInterrupt: print("\n Interrupted.")
finally:
cam.stop(); cam.close()
if audio_stream: audio_stream.close()
if pa: pa.terminate()
if __name__ == "__main__":
main()