1+ import os
2+ import openai
3+ import speech_recognition as sr
4+ import cv2
5+ import serial
6+ import time
7+ import logging
8+ import traceback
9+ import pyttsx3 # For TTS (robot "speaking")
10+
11+ # Config
12+ # Prefer reading the OpenAI API key from the environment for safety.
13+ openai .api_key = os .getenv ("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" )
14+ if not openai .api_key :
15+ print ("Warning: OPENAI_API_KEY is not set in environment. Set it to use OpenAI services." )
16+
17+ # Arduino serial port (updated to COM6 as provided)
18+ serial_port = 'COM6' # Check in Arduino IDE (e.g., COM6 on Windows)
19+ baud_rate = 9600
20+ webcam_id = 0 # Webcam index
21+
22+ # Retry / timeout settings
23+ serial_retry_attempts = 5
24+ serial_retry_delay = 2.0 # seconds between attempts
25+
26+ # Webcam preview toggle
27+ enable_preview = False
28+
29+ # Initialize
30+ ser = None
31+ for attempt in range (1 , serial_retry_attempts + 1 ):
32+ try :
33+ ser = serial .Serial (serial_port , baud_rate , timeout = 1 )
34+ time .sleep (2 ) # Wait for Arduino connection
35+ print (f"Connected to Arduino on { serial_port } " )
36+ break
37+ except Exception as e :
38+ print (f"Attempt { attempt } /{ serial_retry_attempts } : could not open serial port { serial_port } : { e } " )
39+ ser = None
40+ if attempt < serial_retry_attempts :
41+ time .sleep (serial_retry_delay )
42+
43+ recognizer = sr .Recognizer ()
44+ mic = None
45+ try :
46+ mic = sr .Microphone ()
47+ except Exception as e :
48+ print (f"Warning: microphone not available: { e } " )
49+
50+ engine = pyttsx3 .init () # TTS
51+ # Configure TTS engine for Windows (SAPI5) where possible
52+ try :
53+ voices = engine .getProperty ('voices' )
54+ # pick a default voice (0) if available
55+ if voices :
56+ engine .setProperty ('voice' , voices [0 ].id )
57+ engine .setProperty ('rate' , 170 )
58+ engine .setProperty ('volume' , 1.0 )
59+ except Exception as e :
60+ print (f"Warning: could not configure TTS engine: { e } " )
61+
62+ def speak (text ):
63+ """Speak text through system speakers. Falls back to winsound playback on Windows if pyttsx3 fails."""
64+ try :
65+ engine .say (text )
66+ engine .runAndWait ()
67+ return
68+ except Exception as e :
69+ print (f"TTS engine failed, attempting fallback: { e } " )
70+
71+ # Fallback: synthesize to WAV and play via winsound (Windows only)
72+ try :
73+ import tempfile
74+ import sys
75+ if sys .platform .startswith ('win' ):
76+ import wave
77+ import os
78+ import pyttsx3
79+ fd , path = tempfile .mkstemp (suffix = '.wav' )
80+ os .close (fd )
81+ # Try to save via pyttsx3 engine (may not work on some systems)
82+ try :
83+ engine .save_to_file (text , path )
84+ engine .runAndWait ()
85+ import winsound
86+ winsound .PlaySound (path , winsound .SND_FILENAME )
87+ os .remove (path )
88+ return
89+ except Exception as e2 :
90+ print (f"Fallback TTS save/play failed: { e2 } " )
91+ except Exception as e :
92+ print (f"TTS fallback not available: { e } " )
93+
94+ # Face tracking cascade (use built-in haarcascade)
95+ face_cascade = cv2 .CascadeClassifier (cv2 .data .haarcascades + 'haarcascade_frontalface_default.xml' )
96+ cap = None
97+ try :
98+ cap = cv2 .VideoCapture (webcam_id )
99+ if not cap .isOpened ():
100+ print (f"Warning: could not open webcam id { webcam_id } " )
101+ cap .release ()
102+ cap = None
103+ else :
104+ print (f"Webcam { webcam_id } opened" )
105+ if enable_preview :
106+ cv2 .namedWindow ('Webcam Preview' , cv2 .WINDOW_NORMAL )
107+ except Exception as e :
108+ print (f"Warning: webcam init failed: { e } " )
109+ cap = None
110+
111+ # Robot identity / wake-word
112+ robot_name = "Kevin"
113+
114+ def track_face (frame ):
115+ if frame is None :
116+ return None , None
117+ gray = cv2 .cvtColor (frame , cv2 .COLOR_BGR2GRAY )
118+ faces = face_cascade .detectMultiScale (gray , 1.3 , 5 )
119+ if len (faces ) > 0 :
120+ (x , y , w , h ) = faces [0 ]
121+ fx = x + w // 2
122+ fy = y + h // 2
123+ return fx , fy , (x , y , w , h )
124+ return None , None , None
125+
126+ def send_command (cmd ):
127+ if ser :
128+ try :
129+ ser .write ((cmd + '\n ' ).encode ())
130+ print (f"Sent: { cmd } " )
131+ except Exception as e :
132+ print (f"Warning: failed to send command over serial: { e } " )
133+ else :
134+ print (f"Serial unavailable - would send: { cmd } " )
135+
136+ def get_emotion_from_response (response ):
137+ # Simple mapping; improve with ChatGPT prompt
138+ if "happy" in response .lower ():
139+ return "HAPPY"
140+ elif "sad" in response .lower ():
141+ return "SAD"
142+ elif "angry" in response .lower ():
143+ return "ANGRY"
144+ elif "excited" in response .lower ():
145+ return "EXCITED"
146+ return "DEFAULT"
147+
148+ # Main loop
149+ try :
150+ while True :
151+ frame = None
152+ if cap :
153+ ret , frame = cap .read ()
154+ if not ret :
155+ frame = None
156+
157+ # Face tracking and preview overlay
158+ fx = fy = None
159+ face_rect = None
160+ if frame is not None :
161+ fx , fy , face_rect = track_face (frame )
162+ if face_rect is not None :
163+ (x , y , w , h ) = face_rect
164+ cv2 .rectangle (frame , (x , y ), (x + w , y + h ), (0 , 255 , 0 ), 2 )
165+ cv2 .circle (frame , (fx , fy ), 4 , (0 , 0 , 255 ), - 1 )
166+ send_command (f"TRACK|{ fx } ,{ fy } " )
167+
168+ if enable_preview :
169+ cv2 .imshow ('Webcam Preview' , frame )
170+ # If user presses 'q' in the preview window, exit
171+ if cv2 .waitKey (1 ) & 0xFF == ord ('q' ):
172+ print ('Preview quit requested' )
173+ break
174+
175+ # Speech recognition (guarded)
176+ user_input = None
177+ if mic :
178+ try :
179+ with mic as source :
180+ recognizer .adjust_for_ambient_noise (source , duration = 0.5 )
181+ # timeout prevents blocking indefinitely; phrase_time_limit bounds recording length
182+ audio = recognizer .listen (source , timeout = 5 , phrase_time_limit = 8 )
183+ try :
184+ if openai .api_key :
185+ user_input = recognizer .recognize_whisper (audio , model = "base" )
186+ else :
187+ # fallback to Google recognizer if OpenAI key not set
188+ user_input = recognizer .recognize_google (audio )
189+ print (f"You said: { user_input } " )
190+ except Exception as e :
191+ print (f"Speech not recognized: { e } " )
192+ except Exception as e :
193+ print (f"Microphone listen failed or timed out: { e } " )
194+
195+ if not user_input :
196+ # nothing heard this loop; small sleep to avoid busy loop
197+ time .sleep (0.1 )
198+ continue
199+
200+ # If user addressed the robot by name, respond locally and skip OpenAI
201+ response = None
202+ lowered = user_input .lower () if user_input else ""
203+ if any (phrase in lowered for phrase in [robot_name .lower (), f"hey { robot_name .lower ()} " , f"hi { robot_name .lower ()} " ]):
204+ response = f"Hello — I'm { robot_name } . How can I help you?"
205+ print (f"Robot (local): { response } " )
206+ else :
207+ # Chat with OpenAI (guarded)
208+ try :
209+ if openai .api_key :
210+ system_prompt = f"You are a friendly robot companion named { robot_name } . Be concise and helpful."
211+ resp = openai .ChatCompletion .create (
212+ model = "gpt-4o" ,
213+ messages = [
214+ {"role" : "system" , "content" : system_prompt },
215+ {"role" : "user" , "content" : user_input }
216+ ]
217+ )
218+ # Access response safely
219+ response = resp .choices [0 ].message .content
220+ else :
221+ response = "(OpenAI API key not set) I can't call OpenAI, but I'm listening!"
222+ print (f"Robot: { response } " )
223+ except Exception as e :
224+ print (f"OpenAI request failed: { e } " )
225+ response = "Sorry, I couldn't think of a response."
226+
227+ # TTS response
228+ try :
229+ speak (response )
230+ except Exception as e :
231+ print (f"TTS failed: { e } " )
232+
233+ # Determine emotion and send to Arduino
234+ emotion = get_emotion_from_response (response )
235+ send_command (f"EMO|{ emotion } " )
236+
237+ except KeyboardInterrupt :
238+ print ('\n Interrupted by user' )
239+ finally :
240+ # Cleanup
241+ print ('Cleaning up...' )
242+ if cap :
243+ try :
244+ cap .release ()
245+ except Exception :
246+ pass
247+ if enable_preview :
248+ try :
249+ cv2 .destroyAllWindows ()
250+ except Exception :
251+ pass
252+ if ser :
253+ try :
254+ ser .close ()
255+ except Exception :
256+ pass
0 commit comments