forked from strands-agents/harness-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_gemini_live.py
More file actions
364 lines (298 loc) · 12.9 KB
/
Copy pathtest_gemini_live.py
File metadata and controls
364 lines (298 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
"""Test suite for Gemini Live bidirectional streaming with camera support.
Tests the Gemini Live API with real-time audio and video interaction including:
- Audio input/output streaming
- Camera frame capture and transmission
- Interruption handling
- Concurrent tool execution
- Transcript events
Requirements:
- pip install opencv-python pillow pyaudio google-genai
- Camera access permissions
- GOOGLE_AI_API_KEY environment variable
"""
import asyncio
import base64
import io
import logging
import os
import sys
from pathlib import Path
# Add the src directory to Python path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent))
import time
try:
import cv2
import PIL.Image
CAMERA_AVAILABLE = True
except ImportError as e:
print(f"Camera dependencies not available: {e}")
print("Install with: pip install opencv-python pillow")
CAMERA_AVAILABLE = False
import pyaudio
from strands_tools import calculator
from strands.experimental.bidirectional_streaming.agent.agent import BidirectionalAgent
from strands.experimental.bidirectional_streaming.models.gemini_live import BidiGeminiLiveModel
# Configure logging - debug only for Gemini Live, info for everything else
logging.basicConfig(level=logging.WARN, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
gemini_logger = logging.getLogger('strands.experimental.bidirectional_streaming.models.gemini_live')
gemini_logger.setLevel(logging.WARN)
logger = logging.getLogger(__name__)
async def play(context):
"""Play audio output with responsive interruption support."""
audio = pyaudio.PyAudio()
speaker = audio.open(
channels=1,
format=pyaudio.paInt16,
output=True,
rate=24000,
frames_per_buffer=1024,
)
try:
while context["active"]:
try:
# Check for interruption first
if context.get("interrupted", False):
# Clear entire audio queue immediately
while not context["audio_out"].empty():
try:
context["audio_out"].get_nowait()
except asyncio.QueueEmpty:
break
context["interrupted"] = False
await asyncio.sleep(0.05)
continue
# Get next audio data
audio_data = await asyncio.wait_for(context["audio_out"].get(), timeout=0.1)
if audio_data and context["active"]:
chunk_size = 1024
for i in range(0, len(audio_data), chunk_size):
# Check for interruption before each chunk
if context.get("interrupted", False) or not context["active"]:
break
end = min(i + chunk_size, len(audio_data))
chunk = audio_data[i:end]
speaker.write(chunk)
await asyncio.sleep(0.001)
except asyncio.TimeoutError:
continue # No audio available
except asyncio.QueueEmpty:
await asyncio.sleep(0.01)
except asyncio.CancelledError:
break
except asyncio.CancelledError:
pass
finally:
speaker.close()
audio.terminate()
async def record(context):
"""Record audio input from microphone."""
audio = pyaudio.PyAudio()
# List all available audio devices
print("Available audio devices:")
for i in range(audio.get_device_count()):
device_info = audio.get_device_info_by_index(i)
if device_info['maxInputChannels'] > 0: # Only show input devices
print(f" Device {i}: {device_info['name']} (inputs: {device_info['maxInputChannels']})")
# Get default input device info
default_device = audio.get_default_input_device_info()
print(f"\nUsing default input device: {default_device['name']} (Device {default_device['index']})")
microphone = audio.open(
channels=1,
format=pyaudio.paInt16,
frames_per_buffer=1024,
input=True,
rate=16000,
)
try:
while context["active"]:
try:
audio_bytes = microphone.read(1024, exception_on_overflow=False)
context["audio_in"].put_nowait(audio_bytes)
await asyncio.sleep(0.01)
except asyncio.CancelledError:
break
except asyncio.CancelledError:
pass
finally:
microphone.close()
audio.terminate()
async def receive(agent, context):
"""Receive and process events from agent."""
try:
async for event in agent.receive():
# Debug: Log event type and keys
event_type = event.get("type", "unknown")
event_keys = list(event.keys())
logger.debug(f"Received event type: {event_type}, keys: {event_keys}")
# Handle audio stream events (bidirectional_audio_stream)
if event_type == "bidirectional_audio_stream":
if not context.get("interrupted", False):
# Decode base64 audio string to bytes for playback
audio_b64 = event["audio"]
audio_data = base64.b64decode(audio_b64)
context["audio_out"].put_nowait(audio_data)
logger.info(f"🔊 Audio queued for playback: {len(audio_data)} bytes")
# Handle interruption events (bidirectional_interruption)
elif event_type == "bidirectional_interruption":
context["interrupted"] = True
print("⚠️ Interruption detected")
# Handle transcript events (bidirectional_transcript_stream)
elif event_type == "bidirectional_transcript_stream":
transcript_text = event.get("text", "")
transcript_role = event.get("role", "unknown")
is_final = event.get("is_final", False)
# Print transcripts with special formatting
if transcript_role == "user":
print(f"🎤 User: {transcript_text}")
elif transcript_role == "assistant":
print(f"🔊 Assistant: {transcript_text}")
# Handle turn complete events (bidirectional_turn_complete)
elif event_type == "bidirectional_turn_complete":
logger.debug("Turn complete - model ready for next input")
# Reset interrupted state since the turn is complete
context["interrupted"] = False
# Handle session start events (bidirectional_session_start)
elif event_type == "bidirectional_session_start":
logger.info(f"Session started: {event.get('model', 'unknown')}")
# Handle session end events (bidirectional_session_end)
elif event_type == "bidirectional_session_end":
logger.info(f"Session ended: {event.get('reason', 'unknown')}")
# Handle error events (bidirectional_error)
elif event_type == "bidirectional_error":
logger.error(f"Error: {event.get('error_message', 'unknown')}")
# Handle turn start events (bidirectional_turn_start)
elif event_type == "bidirectional_turn_start":
logger.debug(f"Turn started: {event.get('response_id', 'unknown')}")
except asyncio.CancelledError:
pass
def _get_frame(cap):
"""Capture and process a frame from camera."""
if not CAMERA_AVAILABLE:
return None
# Read the frame
ret, frame = cap.read()
# Check if the frame was read successfully
if not ret:
return None
# Convert BGR to RGB color space
# OpenCV captures in BGR but PIL expects RGB format
# This prevents the blue tint in the video feed
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = PIL.Image.fromarray(frame_rgb)
img.thumbnail([1024, 1024])
image_io = io.BytesIO()
img.save(image_io, format="jpeg")
image_io.seek(0)
mime_type = "image/jpeg"
image_bytes = image_io.read()
return {"mime_type": mime_type, "data": base64.b64encode(image_bytes).decode()}
async def get_frames(context):
"""Capture frames from camera and send to agent."""
if not CAMERA_AVAILABLE:
print("Camera not available - skipping video capture")
return
# This takes about a second, and will block the whole program
# causing the audio pipeline to overflow if you don't to_thread it.
cap = await asyncio.to_thread(cv2.VideoCapture, 0) # 0 represents the default camera
print("Camera initialized. Starting video capture...")
try:
while context["active"] and time.time() - context["start_time"] < context["duration"]:
frame = await asyncio.to_thread(_get_frame, cap)
if frame is None:
break
# Send frame to agent as image input
try:
from strands.experimental.bidirectional_streaming.types.events import BidiImageInputEvent
image_event = BidiImageInputEvent(
image=frame["data"], # Already base64 encoded
mime_type=frame["mime_type"]
)
await context["agent"].send(image_event)
print("📸 Frame sent to model")
except Exception as e:
logger.error(f"Error sending frame: {e}")
# Wait 1 second between frames (1 FPS)
await asyncio.sleep(1.0)
except asyncio.CancelledError:
pass
finally:
# Release the VideoCapture object
cap.release()
async def send(agent, context):
"""Send audio input to agent."""
try:
while time.time() - context["start_time"] < context["duration"]:
try:
audio_bytes = context["audio_in"].get_nowait()
# Create audio event using TypedEvent
from strands.experimental.bidirectional_streaming.types.events import BidiAudioInputEvent
audio_b64 = base64.b64encode(audio_bytes).decode('utf-8')
audio_event = BidiAudioInputEvent(
audio=audio_b64,
format="pcm",
sample_rate=16000,
channels=1
)
await agent.send(audio_event)
except asyncio.QueueEmpty:
await asyncio.sleep(0.01)
except asyncio.CancelledError:
break
context["active"] = False
except asyncio.CancelledError:
pass
async def main(duration=180):
"""Main function for Gemini Live bidirectional streaming test with camera support."""
print("Starting Gemini Live bidirectional streaming test with camera...")
print("Audio optimizations: 1024-byte buffers, balanced smooth playback + responsive interruption")
print("Video: Camera frames sent at 1 FPS to model")
# Get API key from environment variable
api_key = os.getenv("GOOGLE_AI_API_KEY")
if not api_key:
print("ERROR: GOOGLE_AI_API_KEY environment variable not set")
print("Please set it with: export GOOGLE_AI_API_KEY=your_api_key")
return
# Initialize Gemini Live model with proper configuration
logger.info("Initializing Gemini Live model with API key")
# Use default model and config (includes transcription enabled by default)
model = BidiGeminiLiveModel(api_key=api_key)
logger.info("Gemini Live model initialized successfully")
print("Using Gemini Live model with default config (audio output + transcription enabled)")
agent = BidirectionalAgent(
model=model,
tools=[calculator],
system_prompt="You are a helpful assistant."
)
await agent.start()
# Create shared context for all tasks
context = {
"active": True,
"audio_in": asyncio.Queue(),
"audio_out": asyncio.Queue(),
"connection": agent._session,
"duration": duration,
"start_time": time.time(),
"interrupted": False,
"agent": agent, # Add agent reference for camera task
}
print("Speak into microphone and show things to camera. Press Ctrl+C to exit.")
try:
# Run all tasks concurrently including camera
await asyncio.gather(
play(context),
record(context),
receive(agent, context),
send(agent, context),
get_frames(context), # Add camera task
return_exceptions=True
)
except KeyboardInterrupt:
print("\nInterrupted by user")
except asyncio.CancelledError:
print("\nTest cancelled")
finally:
print("Cleaning up...")
context["active"] = False
await agent.stop()
if __name__ == "__main__":
asyncio.run(main())