From c71d1c37c4cc6e64c5e9fd005390c812a2d7acd5 Mon Sep 17 00:00:00 2001 From: Guiners Date: Tue, 29 Jul 2025 10:52:28 +0200 Subject: [PATCH 01/22] adding working samples with tests --- genai/live/live_ground_ragengine_with_txt.py | 73 ++++++++++++++++++++ genai/live/test_live_examples.py | 29 ++++++++ 2 files changed, 102 insertions(+) create mode 100644 genai/live/live_ground_ragengine_with_txt.py diff --git a/genai/live/live_ground_ragengine_with_txt.py b/genai/live/live_ground_ragengine_with_txt.py new file mode 100644 index 00000000000..038e33c50be --- /dev/null +++ b/genai/live/live_ground_ragengine_with_txt.py @@ -0,0 +1,73 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio + + +async def generate_content(memory_corpus: str) -> list[str]: + # [START googlegenaisdk_live_ground_ragengine_with_txt] + from google import genai + from google.genai.types import ( + Content, + LiveConnectConfig, + Modality, + Part, + Tool, + Retrieval, + VertexRagStore, + VertexRagStoreRagResource, + ) + + client = genai.Client() + # model_id = "gemini-live-2.5-flash" + model_id = "gemini-2.0-flash-live-preview-04-09" + + rag_store = VertexRagStore( + rag_resources=[ + VertexRagStoreRagResource( + rag_corpus=memory_corpus # Use memory corpus if you want to store context. + ) + ], + # Set `store_context` to true to allow Live API sink context into your memory corpus. + store_context=True, + ) + config = LiveConnectConfig( + response_modalities=[Modality.TEXT], + tools=[Tool(retrieval=Retrieval(vertex_rag_store=rag_store))], + ) + + async with client.aio.live.connect(model=model_id, config=config) as session: + text_input = "What year did Mariusz Pudzianowski win World's Strongest Man?" + print("> ", text_input, "\n") + + await session.send_client_content( + turns=Content(role="user", parts=[Part(text=text_input)]) + ) + + response = [] + + async for message in session.receive(): + if message.text: + response.append(message.text) + continue + + print("".join(response)) + # Example output: + # > What year did Mariusz Pudzianowski win World's Strongest Man? + # Mariusz Pudzianowski won World's Strongest Man in 2002, 2003, 2005, 2007, and 2008. + # [END googlegenaisdk_live_ground_ragengine_with_txt] + return response + + +if __name__ == "__main__": + asyncio.run(generate_content("memory_corpus")) diff --git a/genai/live/test_live_examples.py b/genai/live/test_live_examples.py index ce382539861..d8debb5d84c 100644 --- a/genai/live/test_live_examples.py +++ b/genai/live/test_live_examples.py @@ -32,6 +32,35 @@ # os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" +@pytest.fixture() +def mock_rag_components(mocker): + mock_client_cls = mocker.patch("google.genai.Client") + + from google.genai.types import VertexRagStore, VertexRagStoreRagResource + + mocker.patch( + "google.genai.types.VertexRagStoreRagResource", + side_effect=lambda rag_corpus: VertexRagStoreRagResource(rag_corpus=rag_corpus), + ) + mocker.patch( + "google.genai.types.VertexRagStore", + side_effect=lambda rag_resources, store_context: VertexRagStore( + rag_resources=rag_resources, store_context=store_context + ), + ) + + mock_session = mocker.AsyncMock() + mock_session.__aenter__.return_value = mock_session + mock_session.receive.return_value = iter( + [ + mocker.MagicMock( + text="Mariusz Pudzianowski won in 2002, 2003, 2005, 2007, and 2008." + ) + ] + ) + mock_client_cls.return_value.aio.live.connect.return_value = mock_session + + @pytest.mark.asyncio async def test_live_with_text() -> None: assert await live_with_txt.generate_content() From c6e6fcb0ac55f07e7c364eacd648947a84927f64 Mon Sep 17 00:00:00 2001 From: Guiners Date: Tue, 29 Jul 2025 13:53:27 +0200 Subject: [PATCH 02/22] adding working samples with tests --- genai/live/live_ground_ragengine_with_txt.py | 6 ++-- .../live/live_websocket_audiogen_with_txt.py | 28 +++++++++------ ...live_websocket_audiotranscript_with_txt.py | 24 +++++++------ .../live/live_websocket_textgen_with_audio.py | 24 +++++++------ genai/live/live_websocket_textgen_with_txt.py | 20 ++++++----- genai/live/live_with_txt.py | 4 ++- genai/live/test_live_examples.py | 36 +++++++++---------- 7 files changed, 80 insertions(+), 62 deletions(-) diff --git a/genai/live/live_ground_ragengine_with_txt.py b/genai/live/live_ground_ragengine_with_txt.py index 038e33c50be..2452a27071b 100644 --- a/genai/live/live_ground_ragengine_with_txt.py +++ b/genai/live/live_ground_ragengine_with_txt.py @@ -13,6 +13,8 @@ # limitations under the License. import asyncio +_memory_corpus = "projects/cloud-ai-devrel-softserve/locations/us-central1/ragCorpora/2305843009213693952" + async def generate_content(memory_corpus: str) -> list[str]: # [START googlegenaisdk_live_ground_ragengine_with_txt] @@ -29,9 +31,7 @@ async def generate_content(memory_corpus: str) -> list[str]: ) client = genai.Client() - # model_id = "gemini-live-2.5-flash" model_id = "gemini-2.0-flash-live-preview-04-09" - rag_store = VertexRagStore( rag_resources=[ VertexRagStoreRagResource( @@ -70,4 +70,4 @@ async def generate_content(memory_corpus: str) -> list[str]: if __name__ == "__main__": - asyncio.run(generate_content("memory_corpus")) + asyncio.run(generate_content(_memory_corpus)) diff --git a/genai/live/live_websocket_audiogen_with_txt.py b/genai/live/live_websocket_audiogen_with_txt.py index f7b6f07e5f8..277d4d5f8ba 100644 --- a/genai/live/live_websocket_audiogen_with_txt.py +++ b/genai/live/live_websocket_audiogen_with_txt.py @@ -20,7 +20,9 @@ def get_bearer_token() -> str: import google.auth from google.auth.transport.requests import Request - creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) + creds, _ = google.auth.default( + scopes=["https://www.googleapis.com/auth/cloud-platform"] + ) auth_req = Request() creds.refresh(auth_req) bearer_token = creds.token @@ -55,9 +57,7 @@ async def generate_content() -> str: # Websocket Configuration WEBSOCKET_HOST = "us-central1-aiplatform.googleapis.com" - WEBSOCKET_SERVICE_URL = ( - f"wss://{WEBSOCKET_HOST}/ws/google.cloud.aiplatform.v1.LlmBidiService/BidiGenerateContent" - ) + WEBSOCKET_SERVICE_URL = f"wss://{WEBSOCKET_HOST}/ws/google.cloud.aiplatform.v1.LlmBidiService/BidiGenerateContent" # Websocket Authentication headers = { @@ -66,9 +66,7 @@ async def generate_content() -> str: } # Model Configuration - model_path = ( - f"projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/{GEMINI_MODEL_NAME}" - ) + model_path = f"projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/{GEMINI_MODEL_NAME}" model_generation_config = { "response_modalities": ["AUDIO"], "speech_config": { @@ -77,7 +75,9 @@ async def generate_content() -> str: }, } - async with connect(WEBSOCKET_SERVICE_URL, additional_headers=headers) as websocket_session: + async with connect( + WEBSOCKET_SERVICE_URL, additional_headers=headers + ) as websocket_session: # 1. Send setup configuration websocket_config = { "setup": { @@ -120,7 +120,9 @@ async def generate_content() -> str: server_content = response_chunk.get("serverContent") if not server_content: # This might indicate an error or an unexpected message format - print(f"Received non-serverContent message or empty content: {response_chunk}") + print( + f"Received non-serverContent message or empty content: {response_chunk}" + ) break # Collect audio chunks @@ -129,7 +131,9 @@ async def generate_content() -> str: for part in model_turn["parts"]: if part["inlineData"]["mimeType"] == "audio/pcm": audio_chunk = base64.b64decode(part["inlineData"]["data"]) - aggregated_response_parts.append(np.frombuffer(audio_chunk, dtype=np.int16)) + aggregated_response_parts.append( + np.frombuffer(audio_chunk, dtype=np.int16) + ) # End of response if server_content.get("turnComplete"): @@ -137,7 +141,9 @@ async def generate_content() -> str: # Save audio to a file if aggregated_response_parts: - wavfile.write("output.wav", 24000, np.concatenate(aggregated_response_parts)) + wavfile.write( + "output.wav", 24000, np.concatenate(aggregated_response_parts) + ) # Example response: # Setup Response: {'setupComplete': {}} # Input: Hello? Gemini are you there? diff --git a/genai/live/live_websocket_audiotranscript_with_txt.py b/genai/live/live_websocket_audiotranscript_with_txt.py index 5192b81ef17..5304e1914bb 100644 --- a/genai/live/live_websocket_audiotranscript_with_txt.py +++ b/genai/live/live_websocket_audiotranscript_with_txt.py @@ -20,7 +20,9 @@ def get_bearer_token() -> str: import google.auth from google.auth.transport.requests import Request - creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) + creds, _ = google.auth.default( + scopes=["https://www.googleapis.com/auth/cloud-platform"] + ) auth_req = Request() creds.refresh(auth_req) bearer_token = creds.token @@ -55,9 +57,7 @@ async def generate_content() -> str: # Websocket Configuration WEBSOCKET_HOST = "us-central1-aiplatform.googleapis.com" - WEBSOCKET_SERVICE_URL = ( - f"wss://{WEBSOCKET_HOST}/ws/google.cloud.aiplatform.v1.LlmBidiService/BidiGenerateContent" - ) + WEBSOCKET_SERVICE_URL = f"wss://{WEBSOCKET_HOST}/ws/google.cloud.aiplatform.v1.LlmBidiService/BidiGenerateContent" # Websocket Authentication headers = { @@ -66,9 +66,7 @@ async def generate_content() -> str: } # Model Configuration - model_path = ( - f"projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/{GEMINI_MODEL_NAME}" - ) + model_path = f"projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/{GEMINI_MODEL_NAME}" model_generation_config = { "response_modalities": ["AUDIO"], "speech_config": { @@ -77,7 +75,9 @@ async def generate_content() -> str: }, } - async with connect(WEBSOCKET_SERVICE_URL, additional_headers=headers) as websocket_session: + async with connect( + WEBSOCKET_SERVICE_URL, additional_headers=headers + ) as websocket_session: # 1. Send setup configuration websocket_config = { "setup": { @@ -125,7 +125,9 @@ async def generate_content() -> str: server_content = response_chunk.get("serverContent") if not server_content: # This might indicate an error or an unexpected message format - print(f"Received non-serverContent message or empty content: {response_chunk}") + print( + f"Received non-serverContent message or empty content: {response_chunk}" + ) break # Transcriptions @@ -142,7 +144,9 @@ async def generate_content() -> str: for part in model_turn["parts"]: if part["inlineData"]["mimeType"] == "audio/pcm": audio_chunk = base64.b64decode(part["inlineData"]["data"]) - aggregated_response_parts.append(np.frombuffer(audio_chunk, dtype=np.int16)) + aggregated_response_parts.append( + np.frombuffer(audio_chunk, dtype=np.int16) + ) # End of response if server_content.get("turnComplete"): diff --git a/genai/live/live_websocket_textgen_with_audio.py b/genai/live/live_websocket_textgen_with_audio.py index de6fd9d55c3..f91cff35b57 100644 --- a/genai/live/live_websocket_textgen_with_audio.py +++ b/genai/live/live_websocket_textgen_with_audio.py @@ -20,7 +20,9 @@ def get_bearer_token() -> str: import google.auth from google.auth.transport.requests import Request - creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) + creds, _ = google.auth.default( + scopes=["https://www.googleapis.com/auth/cloud-platform"] + ) auth_req = Request() creds.refresh(auth_req) bearer_token = creds.token @@ -65,9 +67,7 @@ def read_wavefile(filepath: str) -> tuple[str, str]: # Websocket Configuration WEBSOCKET_HOST = "us-central1-aiplatform.googleapis.com" - WEBSOCKET_SERVICE_URL = ( - f"wss://{WEBSOCKET_HOST}/ws/google.cloud.aiplatform.v1.LlmBidiService/BidiGenerateContent" - ) + WEBSOCKET_SERVICE_URL = f"wss://{WEBSOCKET_HOST}/ws/google.cloud.aiplatform.v1.LlmBidiService/BidiGenerateContent" # Websocket Authentication headers = { @@ -76,12 +76,12 @@ def read_wavefile(filepath: str) -> tuple[str, str]: } # Model Configuration - model_path = ( - f"projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/{GEMINI_MODEL_NAME}" - ) + model_path = f"projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/{GEMINI_MODEL_NAME}" model_generation_config = {"response_modalities": ["TEXT"]} - async with connect(WEBSOCKET_SERVICE_URL, additional_headers=headers) as websocket_session: + async with connect( + WEBSOCKET_SERVICE_URL, additional_headers=headers + ) as websocket_session: # 1. Send setup configuration websocket_config = { "setup": { @@ -105,7 +105,9 @@ def read_wavefile(filepath: str) -> tuple[str, str]: return "Error: WebSocket setup failed." # 3. Send audio message - encoded_audio_message, mime_type = read_wavefile("hello_gemini_are_you_there.wav") + encoded_audio_message, mime_type = read_wavefile( + "hello_gemini_are_you_there.wav" + ) # Example audio message: "Hello? Gemini are you there?" user_message = { @@ -136,7 +138,9 @@ def read_wavefile(filepath: str) -> tuple[str, str]: server_content = response_chunk.get("serverContent") if not server_content: # This might indicate an error or an unexpected message format - print(f"Received non-serverContent message or empty content: {response_chunk}") + print( + f"Received non-serverContent message or empty content: {response_chunk}" + ) break # Collect text responses diff --git a/genai/live/live_websocket_textgen_with_txt.py b/genai/live/live_websocket_textgen_with_txt.py index b36487cc9a0..f8e88fa0521 100644 --- a/genai/live/live_websocket_textgen_with_txt.py +++ b/genai/live/live_websocket_textgen_with_txt.py @@ -20,7 +20,9 @@ def get_bearer_token() -> str: import google.auth from google.auth.transport.requests import Request - creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) + creds, _ = google.auth.default( + scopes=["https://www.googleapis.com/auth/cloud-platform"] + ) auth_req = Request() creds.refresh(auth_req) bearer_token = creds.token @@ -51,9 +53,7 @@ async def generate_content() -> str: # Websocket Configuration WEBSOCKET_HOST = "us-central1-aiplatform.googleapis.com" - WEBSOCKET_SERVICE_URL = ( - f"wss://{WEBSOCKET_HOST}/ws/google.cloud.aiplatform.v1.LlmBidiService/BidiGenerateContent" - ) + WEBSOCKET_SERVICE_URL = f"wss://{WEBSOCKET_HOST}/ws/google.cloud.aiplatform.v1.LlmBidiService/BidiGenerateContent" # Websocket Authentication headers = { @@ -62,12 +62,12 @@ async def generate_content() -> str: } # Model Configuration - model_path = ( - f"projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/{GEMINI_MODEL_NAME}" - ) + model_path = f"projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/{GEMINI_MODEL_NAME}" model_generation_config = {"response_modalities": ["TEXT"]} - async with connect(WEBSOCKET_SERVICE_URL, additional_headers=headers) as websocket_session: + async with connect( + WEBSOCKET_SERVICE_URL, additional_headers=headers + ) as websocket_session: # 1. Send setup configuration websocket_config = { "setup": { @@ -110,7 +110,9 @@ async def generate_content() -> str: server_content = response_chunk.get("serverContent") if not server_content: # This might indicate an error or an unexpected message format - print(f"Received non-serverContent message or empty content: {response_chunk}") + print( + f"Received non-serverContent message or empty content: {response_chunk}" + ) break # Collect text responses diff --git a/genai/live/live_with_txt.py b/genai/live/live_with_txt.py index a3c75188439..fd412af7740 100644 --- a/genai/live/live_with_txt.py +++ b/genai/live/live_with_txt.py @@ -35,7 +35,9 @@ async def generate_content() -> list[str]: ) as session: text_input = "Hello? Gemini, are you there?" print("> ", text_input, "\n") - await session.send_client_content(turns=Content(role="user", parts=[Part(text=text_input)])) + await session.send_client_content( + turns=Content(role="user", parts=[Part(text=text_input)]) + ) response = [] diff --git a/genai/live/test_live_examples.py b/genai/live/test_live_examples.py index d8debb5d84c..6b4cda4ade6 100644 --- a/genai/live/test_live_examples.py +++ b/genai/live/test_live_examples.py @@ -25,6 +25,7 @@ import live_websocket_textgen_with_audio import live_websocket_textgen_with_txt import live_with_txt +import live_ground_ragengine_with_txt os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" @@ -36,28 +37,22 @@ def mock_rag_components(mocker): mock_client_cls = mocker.patch("google.genai.Client") - from google.genai.types import VertexRagStore, VertexRagStoreRagResource + class AsyncIterator: + def __aiter__(self): + return self - mocker.patch( - "google.genai.types.VertexRagStoreRagResource", - side_effect=lambda rag_corpus: VertexRagStoreRagResource(rag_corpus=rag_corpus), - ) - mocker.patch( - "google.genai.types.VertexRagStore", - side_effect=lambda rag_resources, store_context: VertexRagStore( - rag_resources=rag_resources, store_context=store_context - ), - ) + async def __anext__(self): + if not hasattr(self, "used"): + self.used = True + return mocker.MagicMock( + text="Mariusz Pudzianowski won in 2002, 2003, 2005, 2007, and 2008." + ) + raise StopAsyncIteration mock_session = mocker.AsyncMock() mock_session.__aenter__.return_value = mock_session - mock_session.receive.return_value = iter( - [ - mocker.MagicMock( - text="Mariusz Pudzianowski won in 2002, 2003, 2005, 2007, and 2008." - ) - ] - ) + mock_session.receive = lambda: AsyncIterator() + mock_client_cls.return_value.aio.live.connect.return_value = mock_session @@ -84,3 +79,8 @@ async def test_live_websocket_audiogen_with_txt() -> None: @pytest.mark.asyncio async def test_live_websocket_audiotranscript_with_txt() -> None: assert await live_websocket_audiotranscript_with_txt.generate_content() + + +@pytest.mark.asyncio +async def test_live_ground_ragengine_with_txt(mock_rag_components) -> None: + assert await live_ground_ragengine_with_txt.generate_content("test") From e6b2750fc0524a320ffa11234b11172d0f42f04b Mon Sep 17 00:00:00 2001 From: Guiners Date: Tue, 29 Jul 2025 15:00:44 +0200 Subject: [PATCH 03/22] adding working samples with tests --- .../live_conversation_audio_with_audio.py | 82 +++++++++++ ...conversation_websocket_audio_with_audio.py | 129 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 genai/live/live_conversation_audio_with_audio.py create mode 100644 genai/live/live_conversation_websocket_audio_with_audio.py diff --git a/genai/live/live_conversation_audio_with_audio.py b/genai/live/live_conversation_audio_with_audio.py new file mode 100644 index 00000000000..ae98bf81a82 --- /dev/null +++ b/genai/live/live_conversation_audio_with_audio.py @@ -0,0 +1,82 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +# Installation +# on linux +sudo apt-get install portaudio19-dev + +# on mac +brew install portaudio + +python3 -m venv env +source env/bin/activate +pip install google-genai +""" + +import asyncio +import pyaudio +from google import genai +from google.genai.types import LiveConnectConfig, Modality, AudioTranscriptionConfig, Blob + +CHUNK = 4200 +FORMAT = pyaudio.paInt16 +CHANNELS = 1 +RECORD_SECONDS = 5 +MODEL = "gemini-2.0-flash-live-preview-04-09" +INPUT_RATE = 16000 +OUTPUT_RATE = 24000 + +client = genai.Client() + +config = LiveConnectConfig( + response_modalities=[Modality.AUDIO], + input_audio_transcription=AudioTranscriptionConfig(), + output_audio_transcription=AudioTranscriptionConfig() +) + +async def main(): + print(MODEL) + p = pyaudio.PyAudio() + async with client.aio.live.connect(model=MODEL, config=config) as session: + # exit() + async def send(): + stream = p.open( + format=FORMAT, channels=CHANNELS, rate=INPUT_RATE, input=True, frames_per_buffer=CHUNK) + while True: + frame = stream.read(CHUNK) + await session.send_realtime_input(media=Blob(data=frame, mime_type="audio/pcm")) + await asyncio.sleep(10 ** -12) + + async def receive(): + output_stream = p.open( + format=FORMAT, channels=CHANNELS, rate=OUTPUT_RATE, output=True, frames_per_buffer=CHUNK) + async for message in session.receive(): + if message.server_content.input_transcription: + print(message.server_content.model_dump(mode="json", exclude_none=True)) + if message.server_content.output_transcription: + print(message.server_content.model_dump(mode="json", exclude_none=True)) + if message.server_content.model_turn: + for part in message.server_content.model_turn.parts: + if part.inline_data.data: + audio_data = part.inline_data.data + output_stream.write(audio_data) + await asyncio.sleep(10 ** -12) + + send_task = asyncio.create_task(send()) + receive_task = asyncio.create_task(receive()) + await asyncio.gather(send_task, receive_task) + + +asyncio.run(main()) diff --git a/genai/live/live_conversation_websocket_audio_with_audio.py b/genai/live/live_conversation_websocket_audio_with_audio.py new file mode 100644 index 00000000000..ec32acf0b18 --- /dev/null +++ b/genai/live/live_conversation_websocket_audio_with_audio.py @@ -0,0 +1,129 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import os + +import base64 +import json +import numpy as np + +from websockets.asyncio.client import connect +from scipy.io import wavfile + + +def get_bearer_token() -> str: + import google.auth + from google.auth.transport.requests import Request + + creds, _ = google.auth.default( + scopes=["https://www.googleapis.com/auth/cloud-platform"] + ) + auth_req = Request() + creds.refresh(auth_req) + bearer_token = creds.token + return bearer_token + + +# get bearer token +bearer_token = get_bearer_token() + + + + + + +# Set model generation_config +CONFIG = {"response_modalities": ["AUDIO"]} + +headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {bearer_token[0]}", +} + + +async def main() -> None: + # Connect to the server + async with connect(SERVICE_URL, additional_headers=headers) as ws: + + # Setup the session + async def setup() -> None: + await ws.send( + json.dumps( + { + "setup": { + "model": "gemini-live-2.5-flash", + "generation_config": CONFIG, + } + } + ) + ) + + # Receive setup response + raw_response = await ws.recv(decode=False) + setup_response = json.loads(raw_response.decode("ascii")) + print(f"Connected: {setup_response}") + return + + # Send text message + async def send() -> bool: + text_input = input("Input > ") + if text_input.lower() in ("q", "quit", "exit"): + return False + + msg = { + "client_content": { + "turns": [{"role": "user", "parts": [{"text": text_input}]}], + "turn_complete": True, + } + } + + await ws.send(json.dumps(msg)) + return True + + # Receive server response + async def receive() -> None: + responses = [] + + # Receive chucks of server response + async for raw_response in ws: + response = json.loads(raw_response.decode()) + server_content = response.pop("serverContent", None) + if server_content is None: + break + + model_turn = server_content.pop("modelTurn", None) + if model_turn is not None: + parts = model_turn.pop("parts", None) + if parts is not None: + for part in parts: + pcm_data = base64.b64decode(part["inlineData"]["data"]) + responses.append(np.frombuffer(pcm_data, dtype=np.int16)) + + # End of turn + turn_complete = server_content.pop("turnComplete", None) + if turn_complete: + break + + # Play the returned audio message + display(Markdown("**Response >**")) + display(Audio(np.concatenate(responses), rate=24000, autoplay=True)) + return + + await setup() + + while True: + if not await send(): + break + await receive() From d2b9d0918390edff53558a072318d8d1bd9ac0f8 Mon Sep 17 00:00:00 2001 From: Guiners Date: Tue, 29 Jul 2025 17:25:37 +0200 Subject: [PATCH 04/22] adding working samples with tests --- .../live_conversation_audio_with_audio.py | 24 +++++++++---------- genai/live/requirements.txt | 3 ++- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/genai/live/live_conversation_audio_with_audio.py b/genai/live/live_conversation_audio_with_audio.py index ae98bf81a82..0adf0817a52 100644 --- a/genai/live/live_conversation_audio_with_audio.py +++ b/genai/live/live_conversation_audio_with_audio.py @@ -12,18 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -# Installation -# on linux -sudo apt-get install portaudio19-dev - -# on mac -brew install portaudio - -python3 -m venv env -source env/bin/activate -pip install google-genai -""" import asyncio import pyaudio @@ -47,10 +35,11 @@ ) async def main(): + #exit() print(MODEL) p = pyaudio.PyAudio() async with client.aio.live.connect(model=MODEL, config=config) as session: - # exit() + async def send(): stream = p.open( format=FORMAT, channels=CHANNELS, rate=INPUT_RATE, input=True, frames_per_buffer=CHUNK) @@ -59,6 +48,7 @@ async def send(): await session.send_realtime_input(media=Blob(data=frame, mime_type="audio/pcm")) await asyncio.sleep(10 ** -12) + async def receive(): output_stream = p.open( format=FORMAT, channels=CHANNELS, rate=OUTPUT_RATE, output=True, frames_per_buffer=CHUNK) @@ -74,9 +64,17 @@ async def receive(): output_stream.write(audio_data) await asyncio.sleep(10 ** -12) + + + send_task = asyncio.create_task(send()) receive_task = asyncio.create_task(receive()) await asyncio.gather(send_task, receive_task) +#run it in terminal + + + + asyncio.run(main()) diff --git a/genai/live/requirements.txt b/genai/live/requirements.txt index c12e6a7e2f7..930a9d59a7e 100644 --- a/genai/live/requirements.txt +++ b/genai/live/requirements.txt @@ -1,3 +1,4 @@ google-genai==1.20.0 scipy==1.15.3 -websockets==15.0.1 \ No newline at end of file +websockets==15.0.1 +pyaudio==0.2.14 \ No newline at end of file From f1b47d4eca87b44c0bedf37da61fae51a4b22281 Mon Sep 17 00:00:00 2001 From: Guiners Date: Wed, 30 Jul 2025 11:55:32 +0200 Subject: [PATCH 05/22] adding working samples with tests --- .../test_text_generation_examples.py | 7 +++ .../text_generation/textgen_code_with_pdf.py | 55 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 genai/text_generation/textgen_code_with_pdf.py diff --git a/genai/text_generation/test_text_generation_examples.py b/genai/text_generation/test_text_generation_examples.py index eefc15111c5..701b98614ee 100644 --- a/genai/text_generation/test_text_generation_examples.py +++ b/genai/text_generation/test_text_generation_examples.py @@ -37,6 +37,8 @@ import textgen_with_video import textgen_with_youtube_video import thinking_textgen_with_txt +import textgen_code_with_pdf + os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "global" # "us-central1" @@ -142,3 +144,8 @@ def test_model_optimizer_textgen_with_txt() -> None: response = model_optimizer_textgen_with_txt.generate_content() os.environ["GOOGLE_CLOUD_LOCATION"] = "global" # "us-central1" assert response + + +def test_textgen_code_with_pdf() -> None: + response = textgen_code_with_pdf.generate_content() + assert response diff --git a/genai/text_generation/textgen_code_with_pdf.py b/genai/text_generation/textgen_code_with_pdf.py new file mode 100644 index 00000000000..da4ca76b73a --- /dev/null +++ b/genai/text_generation/textgen_code_with_pdf.py @@ -0,0 +1,55 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# !This sample works with Google Cloud Vertex AI API only. + + +def generate_content() -> str: + # [START googlegenaisdk_textgen_code_with_pdf] + from google import genai + from google.genai.types import HttpOptions, Part + + client = genai.Client(http_options=HttpOptions(api_version="v1beta1")) + model_id = "gemini-2.5-flash" + prompt = "Convert this python code to use Google Python Style Guide." + print("> ", prompt, "\n") + pdf_uri = "https://storage.googleapis.com/cloud-samples-data/generative-ai/text/inefficient_fibonacci_series_python_code.pdf" + + pdf_file = Part.from_uri( + file_uri=pdf_uri, + mime_type="application/pdf", + ) + + response = client.models.generate_content( + model=model_id, + contents=[pdf_file, prompt], + ) + + print(response.text) + # Example response: + # > Convert this python code to use Google Python Style Guide. + # + # def generate_fibonacci_sequence(num_terms: int) -> list[int]: + # """Generates the Fibonacci sequence up to a specified number of terms. + # + # This function calculates the Fibonacci sequence starting with 0 and 1. + # It handles base cases for 0, 1, and 2 terms efficiently. + # + # # ... + # [END googlegenaisdk_textgen_code_with_pdf] + return response.text + + +if __name__ == "__main__": + generate_content() From e84d628c2e75ba75fe185d66ad720c9406b7d85a Mon Sep 17 00:00:00 2001 From: Guiners Date: Tue, 5 Aug 2025 15:24:10 +0200 Subject: [PATCH 06/22] adding working samples with tests --- genai/live/live_websocket_audiogen_with_txt.py | 8 ++------ genai/live/live_websocket_audiotranscript_with_txt.py | 8 ++------ genai/live/live_websocket_textgen_with_audio.py | 8 ++------ genai/live/live_websocket_textgen_with_txt.py | 8 ++------ genai/tools/tools_vais_with_txt.py | 2 ++ 5 files changed, 10 insertions(+), 24 deletions(-) diff --git a/genai/live/live_websocket_audiogen_with_txt.py b/genai/live/live_websocket_audiogen_with_txt.py index 277d4d5f8ba..001cdb2b146 100644 --- a/genai/live/live_websocket_audiogen_with_txt.py +++ b/genai/live/live_websocket_audiogen_with_txt.py @@ -75,9 +75,7 @@ async def generate_content() -> str: }, } - async with connect( - WEBSOCKET_SERVICE_URL, additional_headers=headers - ) as websocket_session: + async with connect(WEBSOCKET_SERVICE_URL, additional_headers=headers) as websocket_session: # 1. Send setup configuration websocket_config = { "setup": { @@ -120,9 +118,7 @@ async def generate_content() -> str: server_content = response_chunk.get("serverContent") if not server_content: # This might indicate an error or an unexpected message format - print( - f"Received non-serverContent message or empty content: {response_chunk}" - ) + print(f"Received non-serverContent message or empty content: {response_chunk}") break # Collect audio chunks diff --git a/genai/live/live_websocket_audiotranscript_with_txt.py b/genai/live/live_websocket_audiotranscript_with_txt.py index 5304e1914bb..616d1baaaef 100644 --- a/genai/live/live_websocket_audiotranscript_with_txt.py +++ b/genai/live/live_websocket_audiotranscript_with_txt.py @@ -75,9 +75,7 @@ async def generate_content() -> str: }, } - async with connect( - WEBSOCKET_SERVICE_URL, additional_headers=headers - ) as websocket_session: + async with connect(WEBSOCKET_SERVICE_URL, additional_headers=headers) as websocket_session: # 1. Send setup configuration websocket_config = { "setup": { @@ -125,9 +123,7 @@ async def generate_content() -> str: server_content = response_chunk.get("serverContent") if not server_content: # This might indicate an error or an unexpected message format - print( - f"Received non-serverContent message or empty content: {response_chunk}" - ) + print(f"Received non-serverContent message or empty content: {response_chunk}") break # Transcriptions diff --git a/genai/live/live_websocket_textgen_with_audio.py b/genai/live/live_websocket_textgen_with_audio.py index f91cff35b57..92bd86a8933 100644 --- a/genai/live/live_websocket_textgen_with_audio.py +++ b/genai/live/live_websocket_textgen_with_audio.py @@ -79,9 +79,7 @@ def read_wavefile(filepath: str) -> tuple[str, str]: model_path = f"projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/{GEMINI_MODEL_NAME}" model_generation_config = {"response_modalities": ["TEXT"]} - async with connect( - WEBSOCKET_SERVICE_URL, additional_headers=headers - ) as websocket_session: + async with connect(WEBSOCKET_SERVICE_URL, additional_headers=headers) as websocket_session: # 1. Send setup configuration websocket_config = { "setup": { @@ -138,9 +136,7 @@ def read_wavefile(filepath: str) -> tuple[str, str]: server_content = response_chunk.get("serverContent") if not server_content: # This might indicate an error or an unexpected message format - print( - f"Received non-serverContent message or empty content: {response_chunk}" - ) + print(f"Received non-serverContent message or empty content: {response_chunk}") break # Collect text responses diff --git a/genai/live/live_websocket_textgen_with_txt.py b/genai/live/live_websocket_textgen_with_txt.py index f8e88fa0521..e378bfa53d6 100644 --- a/genai/live/live_websocket_textgen_with_txt.py +++ b/genai/live/live_websocket_textgen_with_txt.py @@ -65,9 +65,7 @@ async def generate_content() -> str: model_path = f"projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/{GEMINI_MODEL_NAME}" model_generation_config = {"response_modalities": ["TEXT"]} - async with connect( - WEBSOCKET_SERVICE_URL, additional_headers=headers - ) as websocket_session: + async with connect(WEBSOCKET_SERVICE_URL, additional_headers=headers) as websocket_session: # 1. Send setup configuration websocket_config = { "setup": { @@ -110,9 +108,7 @@ async def generate_content() -> str: server_content = response_chunk.get("serverContent") if not server_content: # This might indicate an error or an unexpected message format - print( - f"Received non-serverContent message or empty content: {response_chunk}" - ) + print(f"Received non-serverContent message or empty content: {response_chunk}") break # Collect text responses diff --git a/genai/tools/tools_vais_with_txt.py b/genai/tools/tools_vais_with_txt.py index dbc90b64d15..2de93da4f5f 100644 --- a/genai/tools/tools_vais_with_txt.py +++ b/genai/tools/tools_vais_with_txt.py @@ -29,6 +29,8 @@ def generate_content(datastore: str) -> str: # Load Data Store ID from Vertex AI Search # datastore = "projects/111111111111/locations/global/collections/default_collection/dataStores/data-store-id" + + response = client.models.generate_content( model="gemini-2.0-flash-001", contents="How do I make an appointment to renew my driver's license?", From 5d4fb00166a9effa117b728ac0314f452194f62a Mon Sep 17 00:00:00 2001 From: Guiners Date: Tue, 5 Aug 2025 15:34:23 +0200 Subject: [PATCH 07/22] adding working samples with tests --- genai/live/live_conversation_websocket_audio_with_audio.py | 4 +--- genai/live/live_websocket_audiogen_with_txt.py | 4 +--- genai/live/live_websocket_audiotranscript_with_txt.py | 4 +--- genai/live/live_websocket_textgen_with_audio.py | 4 +--- genai/live/live_websocket_textgen_with_txt.py | 4 +--- 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/genai/live/live_conversation_websocket_audio_with_audio.py b/genai/live/live_conversation_websocket_audio_with_audio.py index ec32acf0b18..32653ae8fc1 100644 --- a/genai/live/live_conversation_websocket_audio_with_audio.py +++ b/genai/live/live_conversation_websocket_audio_with_audio.py @@ -27,9 +27,7 @@ def get_bearer_token() -> str: import google.auth from google.auth.transport.requests import Request - creds, _ = google.auth.default( - scopes=["https://www.googleapis.com/auth/cloud-platform"] - ) + creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) auth_req = Request() creds.refresh(auth_req) bearer_token = creds.token diff --git a/genai/live/live_websocket_audiogen_with_txt.py b/genai/live/live_websocket_audiogen_with_txt.py index 001cdb2b146..7caae3a90ae 100644 --- a/genai/live/live_websocket_audiogen_with_txt.py +++ b/genai/live/live_websocket_audiogen_with_txt.py @@ -20,9 +20,7 @@ def get_bearer_token() -> str: import google.auth from google.auth.transport.requests import Request - creds, _ = google.auth.default( - scopes=["https://www.googleapis.com/auth/cloud-platform"] - ) + creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) auth_req = Request() creds.refresh(auth_req) bearer_token = creds.token diff --git a/genai/live/live_websocket_audiotranscript_with_txt.py b/genai/live/live_websocket_audiotranscript_with_txt.py index 616d1baaaef..065568b369e 100644 --- a/genai/live/live_websocket_audiotranscript_with_txt.py +++ b/genai/live/live_websocket_audiotranscript_with_txt.py @@ -20,9 +20,7 @@ def get_bearer_token() -> str: import google.auth from google.auth.transport.requests import Request - creds, _ = google.auth.default( - scopes=["https://www.googleapis.com/auth/cloud-platform"] - ) + creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) auth_req = Request() creds.refresh(auth_req) bearer_token = creds.token diff --git a/genai/live/live_websocket_textgen_with_audio.py b/genai/live/live_websocket_textgen_with_audio.py index 92bd86a8933..9b3c0cbfd54 100644 --- a/genai/live/live_websocket_textgen_with_audio.py +++ b/genai/live/live_websocket_textgen_with_audio.py @@ -20,9 +20,7 @@ def get_bearer_token() -> str: import google.auth from google.auth.transport.requests import Request - creds, _ = google.auth.default( - scopes=["https://www.googleapis.com/auth/cloud-platform"] - ) + creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) auth_req = Request() creds.refresh(auth_req) bearer_token = creds.token diff --git a/genai/live/live_websocket_textgen_with_txt.py b/genai/live/live_websocket_textgen_with_txt.py index e378bfa53d6..ab4062e7db0 100644 --- a/genai/live/live_websocket_textgen_with_txt.py +++ b/genai/live/live_websocket_textgen_with_txt.py @@ -20,9 +20,7 @@ def get_bearer_token() -> str: import google.auth from google.auth.transport.requests import Request - creds, _ = google.auth.default( - scopes=["https://www.googleapis.com/auth/cloud-platform"] - ) + creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) auth_req = Request() creds.refresh(auth_req) bearer_token = creds.token From 0172badc8983d794e2a0e4c12a109421a9241547 Mon Sep 17 00:00:00 2001 From: Guiners Date: Fri, 5 Sep 2025 10:54:25 +0200 Subject: [PATCH 08/22] codereview fix --- genai/live/live_audiogen_with_txt.py | 12 ++++-- genai/live/live_code_exec_with_txt.py | 10 ++++- genai/live/live_func_call_with_txt.py | 12 ++++-- genai/live/live_ground_googsearch_with_txt.py | 10 ++++- genai/live/live_structured_ouput_with_txt.py | 6 ++- genai/live/live_transcribe_with_audio.py | 9 ++++- genai/live/live_with_txt.py | 9 ++++- genai/live/test_live_examples.py | 37 +++++++++---------- 8 files changed, 69 insertions(+), 36 deletions(-) diff --git a/genai/live/live_audiogen_with_txt.py b/genai/live/live_audiogen_with_txt.py index cf7f24a6fc4..477d4f0d40d 100644 --- a/genai/live/live_audiogen_with_txt.py +++ b/genai/live/live_audiogen_with_txt.py @@ -24,9 +24,15 @@ async def generate_content() -> None: import numpy as np import scipy.io.wavfile as wavfile from google import genai - from google.genai.types import (Content, LiveConnectConfig, Modality, Part, - PrebuiltVoiceConfig, SpeechConfig, - VoiceConfig) + from google.genai.types import ( + Content, + LiveConnectConfig, + Modality, + Part, + PrebuiltVoiceConfig, + SpeechConfig, + VoiceConfig, + ) client = genai.Client() model = "gemini-2.0-flash-live-preview-04-09" diff --git a/genai/live/live_code_exec_with_txt.py b/genai/live/live_code_exec_with_txt.py index 70db7402ee7..6aa1cae7b6d 100644 --- a/genai/live/live_code_exec_with_txt.py +++ b/genai/live/live_code_exec_with_txt.py @@ -18,8 +18,14 @@ async def generate_content() -> list[str]: # [START googlegenaisdk_live_code_exec_with_txt] from google import genai - from google.genai.types import (Content, LiveConnectConfig, Modality, Part, - Tool, ToolCodeExecution) + from google.genai.types import ( + Content, + LiveConnectConfig, + Modality, + Part, + Tool, + ToolCodeExecution, + ) client = genai.Client() model_id = "gemini-2.0-flash-live-preview-04-09" diff --git a/genai/live/live_func_call_with_txt.py b/genai/live/live_func_call_with_txt.py index 7761a49b7b6..b29dd96b78a 100644 --- a/genai/live/live_func_call_with_txt.py +++ b/genai/live/live_func_call_with_txt.py @@ -20,9 +20,15 @@ async def generate_content() -> list[FunctionResponse]: # [START googlegenaisdk_live_func_call_with_txt] from google import genai - from google.genai.types import (Content, FunctionDeclaration, - FunctionResponse, LiveConnectConfig, - Modality, Part, Tool) + from google.genai.types import ( + Content, + FunctionDeclaration, + FunctionResponse, + LiveConnectConfig, + Modality, + Part, + Tool, + ) client = genai.Client() model_id = "gemini-2.0-flash-live-preview-04-09" diff --git a/genai/live/live_ground_googsearch_with_txt.py b/genai/live/live_ground_googsearch_with_txt.py index cfca4a87e1c..82f4281ae6a 100644 --- a/genai/live/live_ground_googsearch_with_txt.py +++ b/genai/live/live_ground_googsearch_with_txt.py @@ -19,8 +19,14 @@ async def generate_content() -> list[str]: # [START googlegenaisdk_live_ground_googsearch_with_txt] from google import genai - from google.genai.types import (Content, GoogleSearch, LiveConnectConfig, - Modality, Part, Tool) + from google.genai.types import ( + Content, + GoogleSearch, + LiveConnectConfig, + Modality, + Part, + Tool, + ) client = genai.Client() model_id = "gemini-2.0-flash-live-preview-04-09" diff --git a/genai/live/live_structured_ouput_with_txt.py b/genai/live/live_structured_ouput_with_txt.py index f0b2466ff5f..38b7df52fcb 100644 --- a/genai/live/live_structured_ouput_with_txt.py +++ b/genai/live/live_structured_ouput_with_txt.py @@ -30,8 +30,10 @@ def generate_content() -> CalendarEvent: import google.auth.transport.requests import openai from google.auth import default - from openai.types.chat import (ChatCompletionSystemMessageParam, - ChatCompletionUserMessageParam) + from openai.types.chat import ( + ChatCompletionSystemMessageParam, + ChatCompletionUserMessageParam, + ) project_id = os.environ["GOOGLE_CLOUD_PROJECT"] location = "us-central1" diff --git a/genai/live/live_transcribe_with_audio.py b/genai/live/live_transcribe_with_audio.py index b702672bc76..644c486675f 100644 --- a/genai/live/live_transcribe_with_audio.py +++ b/genai/live/live_transcribe_with_audio.py @@ -22,8 +22,13 @@ async def generate_content() -> list[str]: # [START googlegenaisdk_live_transcribe_with_audio] from google import genai - from google.genai.types import (AudioTranscriptionConfig, Content, - LiveConnectConfig, Modality, Part) + from google.genai.types import ( + AudioTranscriptionConfig, + Content, + LiveConnectConfig, + Modality, + Part, + ) client = genai.Client() model = "gemini-live-2.5-flash-preview-native-audio" diff --git a/genai/live/live_with_txt.py b/genai/live/live_with_txt.py index 8b8b0908127..76fab43398b 100644 --- a/genai/live/live_with_txt.py +++ b/genai/live/live_with_txt.py @@ -18,8 +18,13 @@ async def generate_content() -> list[str]: # [START googlegenaisdk_live_with_txt] from google import genai - from google.genai.types import (Content, HttpOptions, LiveConnectConfig, - Modality, Part) + from google.genai.types import ( + Content, + HttpOptions, + LiveConnectConfig, + Modality, + Part, + ) client = genai.Client(http_options=HttpOptions(api_version="v1beta1")) model_id = "gemini-2.0-flash-live-preview-04-09" diff --git a/genai/live/test_live_examples.py b/genai/live/test_live_examples.py index b9afafc61f4..589b44240a8 100644 --- a/genai/live/test_live_examples.py +++ b/genai/live/test_live_examples.py @@ -39,31 +39,28 @@ # The project name is included in the CICD pipeline # os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" + @pytest.fixture() def mock_rag_components(mocker): - mock_client_cls = mocker.patch("google.genai.Client") - - - class AsyncIterator: - def __aiter__(self): - return self - - - async def __anext__(self): - if not hasattr(self, "used"): - self.used = True - return mocker.MagicMock( - text="Mariusz Pudzianowski won in 2002, 2003, 2005, 2007, and 2008." - ) - raise StopAsyncIteration + mock_client_cls = mocker.patch("google.genai.Client") + class AsyncIterator: + def __aiter__(self): + return self - mock_session = mocker.AsyncMock() - mock_session.__aenter__.return_value = mock_session - mock_session.receive = lambda: AsyncIterator() + async def __anext__(self): + if not hasattr(self, "used"): + self.used = True + return mocker.MagicMock( + text="Mariusz Pudzianowski won in 2002, 2003, 2005, 2007, and 2008." + ) + raise StopAsyncIteration + mock_session = mocker.AsyncMock() + mock_session.__aenter__.return_value = mock_session + mock_session.receive = lambda: AsyncIterator() - mock_client_cls.return_value.aio.live.connect.return_value = mock_session + mock_client_cls.return_value.aio.live.connect.return_value = mock_session @pytest.mark.asyncio @@ -129,4 +126,4 @@ async def test_live_structured_ouput_with_txt() -> None: @pytest.mark.asyncio async def test_live_ground_ragengine_with_txt(mock_rag_components) -> None: - assert await live_ground_ragengine_with_txt.generate_content("test") + assert await live_ground_ragengine_with_txt.generate_content("test") From fc7a359119d7a65810e2ebd824dbd819e5dc80c0 Mon Sep 17 00:00:00 2001 From: Guiners Date: Fri, 5 Sep 2025 15:09:43 +0200 Subject: [PATCH 09/22] Revert "codereview fix" This reverts commit 0172badc8983d794e2a0e4c12a109421a9241547. --- genai/live/live_audiogen_with_txt.py | 12 ++---- genai/live/live_code_exec_with_txt.py | 10 +---- genai/live/live_func_call_with_txt.py | 12 ++---- genai/live/live_ground_googsearch_with_txt.py | 10 +---- genai/live/live_structured_ouput_with_txt.py | 6 +-- genai/live/live_transcribe_with_audio.py | 9 +---- genai/live/live_with_txt.py | 9 +---- genai/live/test_live_examples.py | 37 ++++++++++--------- 8 files changed, 36 insertions(+), 69 deletions(-) diff --git a/genai/live/live_audiogen_with_txt.py b/genai/live/live_audiogen_with_txt.py index 477d4f0d40d..cf7f24a6fc4 100644 --- a/genai/live/live_audiogen_with_txt.py +++ b/genai/live/live_audiogen_with_txt.py @@ -24,15 +24,9 @@ async def generate_content() -> None: import numpy as np import scipy.io.wavfile as wavfile from google import genai - from google.genai.types import ( - Content, - LiveConnectConfig, - Modality, - Part, - PrebuiltVoiceConfig, - SpeechConfig, - VoiceConfig, - ) + from google.genai.types import (Content, LiveConnectConfig, Modality, Part, + PrebuiltVoiceConfig, SpeechConfig, + VoiceConfig) client = genai.Client() model = "gemini-2.0-flash-live-preview-04-09" diff --git a/genai/live/live_code_exec_with_txt.py b/genai/live/live_code_exec_with_txt.py index 6aa1cae7b6d..70db7402ee7 100644 --- a/genai/live/live_code_exec_with_txt.py +++ b/genai/live/live_code_exec_with_txt.py @@ -18,14 +18,8 @@ async def generate_content() -> list[str]: # [START googlegenaisdk_live_code_exec_with_txt] from google import genai - from google.genai.types import ( - Content, - LiveConnectConfig, - Modality, - Part, - Tool, - ToolCodeExecution, - ) + from google.genai.types import (Content, LiveConnectConfig, Modality, Part, + Tool, ToolCodeExecution) client = genai.Client() model_id = "gemini-2.0-flash-live-preview-04-09" diff --git a/genai/live/live_func_call_with_txt.py b/genai/live/live_func_call_with_txt.py index b29dd96b78a..7761a49b7b6 100644 --- a/genai/live/live_func_call_with_txt.py +++ b/genai/live/live_func_call_with_txt.py @@ -20,15 +20,9 @@ async def generate_content() -> list[FunctionResponse]: # [START googlegenaisdk_live_func_call_with_txt] from google import genai - from google.genai.types import ( - Content, - FunctionDeclaration, - FunctionResponse, - LiveConnectConfig, - Modality, - Part, - Tool, - ) + from google.genai.types import (Content, FunctionDeclaration, + FunctionResponse, LiveConnectConfig, + Modality, Part, Tool) client = genai.Client() model_id = "gemini-2.0-flash-live-preview-04-09" diff --git a/genai/live/live_ground_googsearch_with_txt.py b/genai/live/live_ground_googsearch_with_txt.py index 82f4281ae6a..cfca4a87e1c 100644 --- a/genai/live/live_ground_googsearch_with_txt.py +++ b/genai/live/live_ground_googsearch_with_txt.py @@ -19,14 +19,8 @@ async def generate_content() -> list[str]: # [START googlegenaisdk_live_ground_googsearch_with_txt] from google import genai - from google.genai.types import ( - Content, - GoogleSearch, - LiveConnectConfig, - Modality, - Part, - Tool, - ) + from google.genai.types import (Content, GoogleSearch, LiveConnectConfig, + Modality, Part, Tool) client = genai.Client() model_id = "gemini-2.0-flash-live-preview-04-09" diff --git a/genai/live/live_structured_ouput_with_txt.py b/genai/live/live_structured_ouput_with_txt.py index 38b7df52fcb..f0b2466ff5f 100644 --- a/genai/live/live_structured_ouput_with_txt.py +++ b/genai/live/live_structured_ouput_with_txt.py @@ -30,10 +30,8 @@ def generate_content() -> CalendarEvent: import google.auth.transport.requests import openai from google.auth import default - from openai.types.chat import ( - ChatCompletionSystemMessageParam, - ChatCompletionUserMessageParam, - ) + from openai.types.chat import (ChatCompletionSystemMessageParam, + ChatCompletionUserMessageParam) project_id = os.environ["GOOGLE_CLOUD_PROJECT"] location = "us-central1" diff --git a/genai/live/live_transcribe_with_audio.py b/genai/live/live_transcribe_with_audio.py index 644c486675f..b702672bc76 100644 --- a/genai/live/live_transcribe_with_audio.py +++ b/genai/live/live_transcribe_with_audio.py @@ -22,13 +22,8 @@ async def generate_content() -> list[str]: # [START googlegenaisdk_live_transcribe_with_audio] from google import genai - from google.genai.types import ( - AudioTranscriptionConfig, - Content, - LiveConnectConfig, - Modality, - Part, - ) + from google.genai.types import (AudioTranscriptionConfig, Content, + LiveConnectConfig, Modality, Part) client = genai.Client() model = "gemini-live-2.5-flash-preview-native-audio" diff --git a/genai/live/live_with_txt.py b/genai/live/live_with_txt.py index 76fab43398b..8b8b0908127 100644 --- a/genai/live/live_with_txt.py +++ b/genai/live/live_with_txt.py @@ -18,13 +18,8 @@ async def generate_content() -> list[str]: # [START googlegenaisdk_live_with_txt] from google import genai - from google.genai.types import ( - Content, - HttpOptions, - LiveConnectConfig, - Modality, - Part, - ) + from google.genai.types import (Content, HttpOptions, LiveConnectConfig, + Modality, Part) client = genai.Client(http_options=HttpOptions(api_version="v1beta1")) model_id = "gemini-2.0-flash-live-preview-04-09" diff --git a/genai/live/test_live_examples.py b/genai/live/test_live_examples.py index 589b44240a8..b9afafc61f4 100644 --- a/genai/live/test_live_examples.py +++ b/genai/live/test_live_examples.py @@ -39,28 +39,31 @@ # The project name is included in the CICD pipeline # os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" - @pytest.fixture() def mock_rag_components(mocker): - mock_client_cls = mocker.patch("google.genai.Client") + mock_client_cls = mocker.patch("google.genai.Client") + + + class AsyncIterator: + def __aiter__(self): + return self + + + async def __anext__(self): + if not hasattr(self, "used"): + self.used = True + return mocker.MagicMock( + text="Mariusz Pudzianowski won in 2002, 2003, 2005, 2007, and 2008." + ) + raise StopAsyncIteration - class AsyncIterator: - def __aiter__(self): - return self - async def __anext__(self): - if not hasattr(self, "used"): - self.used = True - return mocker.MagicMock( - text="Mariusz Pudzianowski won in 2002, 2003, 2005, 2007, and 2008." - ) - raise StopAsyncIteration + mock_session = mocker.AsyncMock() + mock_session.__aenter__.return_value = mock_session + mock_session.receive = lambda: AsyncIterator() - mock_session = mocker.AsyncMock() - mock_session.__aenter__.return_value = mock_session - mock_session.receive = lambda: AsyncIterator() - mock_client_cls.return_value.aio.live.connect.return_value = mock_session + mock_client_cls.return_value.aio.live.connect.return_value = mock_session @pytest.mark.asyncio @@ -126,4 +129,4 @@ async def test_live_structured_ouput_with_txt() -> None: @pytest.mark.asyncio async def test_live_ground_ragengine_with_txt(mock_rag_components) -> None: - assert await live_ground_ragengine_with_txt.generate_content("test") + assert await live_ground_ragengine_with_txt.generate_content("test") From 8b26b6fc5b1f46deb6139daf34d8cbd52edec1af Mon Sep 17 00:00:00 2001 From: Robert Kozak <50328216+Guiners@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:17:46 +0200 Subject: [PATCH 10/22] Update tools_vais_with_txt.py --- genai/tools/tools_vais_with_txt.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/genai/tools/tools_vais_with_txt.py b/genai/tools/tools_vais_with_txt.py index dbe327787d6..fa4109d5979 100644 --- a/genai/tools/tools_vais_with_txt.py +++ b/genai/tools/tools_vais_with_txt.py @@ -29,8 +29,6 @@ def generate_content(datastore: str) -> str: # Load Data Store ID from Vertex AI Search # datastore = "projects/111111111111/locations/global/collections/default_collection/dataStores/data-store-id" - - response = client.models.generate_content( model="gemini-2.5-flash", contents="How do I make an appointment to renew my driver's license?", From 6e427c99ae1633d6ef4d3ed5afbba9d6c0e535f8 Mon Sep 17 00:00:00 2001 From: Sampath Kumar Date: Fri, 5 Sep 2025 16:29:15 +0200 Subject: [PATCH 11/22] Update live_conversation_audio_with_audio.py --- .../live_conversation_audio_with_audio.py | 145 ++++++++++++++---- 1 file changed, 115 insertions(+), 30 deletions(-) diff --git a/genai/live/live_conversation_audio_with_audio.py b/genai/live/live_conversation_audio_with_audio.py index 0adf0817a52..55f438b5840 100644 --- a/genai/live/live_conversation_audio_with_audio.py +++ b/genai/live/live_conversation_audio_with_audio.py @@ -13,45 +13,86 @@ # limitations under the License. +# [START googlegenaisdk_live_conversation_audio_with_audio] + import asyncio -import pyaudio +import wave +import base64 +import numpy as np +from scipy.io import wavfile from google import genai -from google.genai.types import LiveConnectConfig, Modality, AudioTranscriptionConfig, Blob +from google.genai.types import ( + LiveConnectConfig, + Modality, + AudioTranscriptionConfig, + Blob, +) +# The number of audio frames to send in each chunk. CHUNK = 4200 -FORMAT = pyaudio.paInt16 CHANNELS = 1 -RECORD_SECONDS = 5 MODEL = "gemini-2.0-flash-live-preview-04-09" + +# The audio sample rate expected by the model. INPUT_RATE = 16000 +# The audio sample rate of the audio generated by the model. OUTPUT_RATE = 24000 +# The sample width for 16-bit audio, which is standard for this type of audio data. +SAMPLE_WIDTH = 2 + client = genai.Client() -config = LiveConnectConfig( - response_modalities=[Modality.AUDIO], - input_audio_transcription=AudioTranscriptionConfig(), - output_audio_transcription=AudioTranscriptionConfig() -) + +def read_wavefile(filepath: str) -> tuple[str, str]: + # Read the .wav file using scipy.io.wavfile.read + rate, data = wavfile.read(filepath) + # Convert the NumPy array of audio samples back to raw bytes + raw_audio_bytes = data.tobytes() + # Encode the raw bytes to a base64 string. + # The result needs to be decoded from bytes to a UTF-8 string + base64_encoded_data = base64.b64encode(raw_audio_bytes).decode("ascii") + mime_type = f"audio/pcm;rate={rate}" + return base64_encoded_data, mime_type + + +def write_wavefile(filepath: str, audio_frames: list[bytes], rate: int): + """Writes a list of audio byte frames to a WAV file using scipy.""" + # Combine the list of byte frames into a single byte string + raw_audio_bytes = b"".join(audio_frames) + + # Convert the raw bytes to a NumPy array. + # The sample width is 2 bytes (16-bit), so we use np.int16 + audio_data = np.frombuffer(raw_audio_bytes, dtype=np.int16) + + # Write the NumPy array to a .wav file + wavfile.write(filepath, rate, audio_data) + print(f"Model response saved to {filepath}") + async def main(): - #exit() - print(MODEL) - p = pyaudio.PyAudio() - async with client.aio.live.connect(model=MODEL, config=config) as session: + async with client.aio.live.connect( + model=MODEL, + config=LiveConnectConfig( + # Set Model responses to be in Audio + response_modalities=[Modality.AUDIO], + # To generate transcript for input audio + input_audio_transcription=AudioTranscriptionConfig(), + # To generate transcript for output audio + output_audio_transcription=AudioTranscriptionConfig(), + ), + ) as session: async def send(): - stream = p.open( - format=FORMAT, channels=CHANNELS, rate=INPUT_RATE, input=True, frames_per_buffer=CHUNK) - while True: - frame = stream.read(CHUNK) - await session.send_realtime_input(media=Blob(data=frame, mime_type="audio/pcm")) - await asyncio.sleep(10 ** -12) - + # using local file as an example for live audio input + wav_file_path = "hello_gemini_are_you_there.wav" + base64_data, mime_type = read_wavefile(wav_file_path) + audio_bytes = base64.b64decode(base64_data) + await session.send_realtime_input(media=Blob(data=audio_bytes, mime_type=mime_type)) async def receive(): - output_stream = p.open( - format=FORMAT, channels=CHANNELS, rate=OUTPUT_RATE, output=True, frames_per_buffer=CHUNK) + audio_frames = [] + async for message in session.receive(): if message.server_content.input_transcription: print(message.server_content.model_dump(mode="json", exclude_none=True)) @@ -61,20 +102,64 @@ async def receive(): for part in message.server_content.model_turn.parts: if part.inline_data.data: audio_data = part.inline_data.data - output_stream.write(audio_data) - await asyncio.sleep(10 ** -12) - - + audio_frames.append(audio_data) + if audio_frames: + write_wavefile( + "example_model_response.wav", + audio_frames, + OUTPUT_RATE, + ) send_task = asyncio.create_task(send()) receive_task = asyncio.create_task(receive()) await asyncio.gather(send_task, receive_task) + # Example response: + # gemini-2.0-flash-live-preview-04-09 + # {'input_transcription': {'text': 'Hello.'}} + # {'output_transcription': {}} + # {'output_transcription': {'text': 'Hi'}} + # {'output_transcription': {'text': ' there. What can I do for you today?'}} + # {'output_transcription': {'finished': True}} + # Model response saved to example_model_response.wav + + +# [END googlegenaisdk_live_conversation_audio_with_audio] + + +# def get_user_live_input(file_path: str): +# """Reads a WAV audio file and yields audio chunks.""" +# with wave.open(file_path, "rb") as wf: +# if ( +# wf.getnchannels() != CHANNELS +# or wf.getsampwidth() != SAMPLE_WIDTH +# or wf.getframerate() != INPUT_RATE +# ): +# print("Audio file properties do not match the required format.") +# print( +# f"Required: {CHANNELS} channels, {SAMPLE_WIDTH} sample width, {INPUT_RATE} frame rate." +# ) +# print( +# f"Found: {wf.getnchannels()} channels, {wf.getsampwidth()} sample width, {wf.getframerate()} frame rate." +# ) +# return +# +# while True: +# frame = wf.readframes(CHUNK) +# if not frame: +# break +# yield frame -#run it in terminal - - +# def write_user_live_output(file_path: str, audio_frames: list[bytes]): +# """Writes audio frames to a WAV file.""" +# with wave.open(file_path, "wb") as wf: +# wf.setnchannels(CHANNELS) +# wf.setsampwidth(SAMPLE_WIDTH) +# wf.setframerate(OUTPUT_RATE) +# wf.writeframes(b"".join(audio_frames)) +# print(f"Model response saved to {file_path}") -asyncio.run(main()) +if __name__ == "__main__": + asyncio.run(main()) From be4f4c307b01c0538d65c42bd9aa209f6af058e2 Mon Sep 17 00:00:00 2001 From: Guiners Date: Fri, 5 Sep 2025 16:42:01 +0200 Subject: [PATCH 12/22] codereview fix --- .../live_conversation_audio_with_audio.py | 35 ------------ .../test_text_generation_examples.py | 15 ----- .../text_generation/textgen_code_with_pdf.py | 55 ------------------- 3 files changed, 105 deletions(-) delete mode 100644 genai/text_generation/textgen_code_with_pdf.py diff --git a/genai/live/live_conversation_audio_with_audio.py b/genai/live/live_conversation_audio_with_audio.py index 55f438b5840..62cadf7f4a2 100644 --- a/genai/live/live_conversation_audio_with_audio.py +++ b/genai/live/live_conversation_audio_with_audio.py @@ -123,43 +123,8 @@ async def receive(): # {'output_transcription': {'finished': True}} # Model response saved to example_model_response.wav - # [END googlegenaisdk_live_conversation_audio_with_audio] -# def get_user_live_input(file_path: str): -# """Reads a WAV audio file and yields audio chunks.""" -# with wave.open(file_path, "rb") as wf: -# if ( -# wf.getnchannels() != CHANNELS -# or wf.getsampwidth() != SAMPLE_WIDTH -# or wf.getframerate() != INPUT_RATE -# ): -# print("Audio file properties do not match the required format.") -# print( -# f"Required: {CHANNELS} channels, {SAMPLE_WIDTH} sample width, {INPUT_RATE} frame rate." -# ) -# print( -# f"Found: {wf.getnchannels()} channels, {wf.getsampwidth()} sample width, {wf.getframerate()} frame rate." -# ) -# return -# -# while True: -# frame = wf.readframes(CHUNK) -# if not frame: -# break -# yield frame - - -# def write_user_live_output(file_path: str, audio_frames: list[bytes]): -# """Writes audio frames to a WAV file.""" -# with wave.open(file_path, "wb") as wf: -# wf.setnchannels(CHANNELS) -# wf.setsampwidth(SAMPLE_WIDTH) -# wf.setframerate(OUTPUT_RATE) -# wf.writeframes(b"".join(audio_frames)) -# print(f"Model response saved to {file_path}") - - if __name__ == "__main__": asyncio.run(main()) diff --git a/genai/text_generation/test_text_generation_examples.py b/genai/text_generation/test_text_generation_examples.py index a29764ec61e..5c0f1e6c6d8 100644 --- a/genai/text_generation/test_text_generation_examples.py +++ b/genai/text_generation/test_text_generation_examples.py @@ -37,8 +37,6 @@ import textgen_with_video import textgen_with_youtube_video import thinking_textgen_with_txt -import textgen_code_with_pdf - os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "global" # "us-central1" @@ -138,19 +136,6 @@ def test_textgen_with_youtube_video() -> None: response = textgen_with_youtube_video.generate_content() assert response - -def test_model_optimizer_textgen_with_txt() -> None: - os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" - response = model_optimizer_textgen_with_txt.generate_content() - os.environ["GOOGLE_CLOUD_LOCATION"] = "global" # "us-central1" - assert response - - -def test_textgen_code_with_pdf() -> None: - response = textgen_code_with_pdf.generate_content() - assert response - - # Migrated to Model Optimser Folder # def test_model_optimizer_textgen_with_txt() -> None: # os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" diff --git a/genai/text_generation/textgen_code_with_pdf.py b/genai/text_generation/textgen_code_with_pdf.py deleted file mode 100644 index da4ca76b73a..00000000000 --- a/genai/text_generation/textgen_code_with_pdf.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# !This sample works with Google Cloud Vertex AI API only. - - -def generate_content() -> str: - # [START googlegenaisdk_textgen_code_with_pdf] - from google import genai - from google.genai.types import HttpOptions, Part - - client = genai.Client(http_options=HttpOptions(api_version="v1beta1")) - model_id = "gemini-2.5-flash" - prompt = "Convert this python code to use Google Python Style Guide." - print("> ", prompt, "\n") - pdf_uri = "https://storage.googleapis.com/cloud-samples-data/generative-ai/text/inefficient_fibonacci_series_python_code.pdf" - - pdf_file = Part.from_uri( - file_uri=pdf_uri, - mime_type="application/pdf", - ) - - response = client.models.generate_content( - model=model_id, - contents=[pdf_file, prompt], - ) - - print(response.text) - # Example response: - # > Convert this python code to use Google Python Style Guide. - # - # def generate_fibonacci_sequence(num_terms: int) -> list[int]: - # """Generates the Fibonacci sequence up to a specified number of terms. - # - # This function calculates the Fibonacci sequence starting with 0 and 1. - # It handles base cases for 0, 1, and 2 terms efficiently. - # - # # ... - # [END googlegenaisdk_textgen_code_with_pdf] - return response.text - - -if __name__ == "__main__": - generate_content() From cf685d03ec1a36244de4ad8cce924db2eb333520 Mon Sep 17 00:00:00 2001 From: Guiners Date: Fri, 5 Sep 2025 16:56:50 +0200 Subject: [PATCH 13/22] codereview fix --- genai/live/test_live_examples.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/genai/live/test_live_examples.py b/genai/live/test_live_examples.py index b9afafc61f4..920a998235a 100644 --- a/genai/live/test_live_examples.py +++ b/genai/live/test_live_examples.py @@ -33,6 +33,7 @@ import live_websocket_textgen_with_audio import live_websocket_textgen_with_txt import live_with_txt +import live_conversation_audio_with_audio os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" @@ -130,3 +131,8 @@ async def test_live_structured_ouput_with_txt() -> None: @pytest.mark.asyncio async def test_live_ground_ragengine_with_txt(mock_rag_components) -> None: assert await live_ground_ragengine_with_txt.generate_content("test") + + +@pytest.mark.asyncio +async def test_live_conversation_audio_with_audio() -> None: + assert await live_conversation_audio_with_audio.main() From 4099674732ec1473fd1d1edd2f560992a39f6d3b Mon Sep 17 00:00:00 2001 From: Guiners Date: Fri, 5 Sep 2025 20:21:23 +0200 Subject: [PATCH 14/22] codereview fix --- ...conversation_websocket_audio_with_audio.py | 127 ------------------ 1 file changed, 127 deletions(-) delete mode 100644 genai/live/live_conversation_websocket_audio_with_audio.py diff --git a/genai/live/live_conversation_websocket_audio_with_audio.py b/genai/live/live_conversation_websocket_audio_with_audio.py deleted file mode 100644 index 32653ae8fc1..00000000000 --- a/genai/live/live_conversation_websocket_audio_with_audio.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import os - -import base64 -import json -import numpy as np - -from websockets.asyncio.client import connect -from scipy.io import wavfile - - -def get_bearer_token() -> str: - import google.auth - from google.auth.transport.requests import Request - - creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) - auth_req = Request() - creds.refresh(auth_req) - bearer_token = creds.token - return bearer_token - - -# get bearer token -bearer_token = get_bearer_token() - - - - - - -# Set model generation_config -CONFIG = {"response_modalities": ["AUDIO"]} - -headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {bearer_token[0]}", -} - - -async def main() -> None: - # Connect to the server - async with connect(SERVICE_URL, additional_headers=headers) as ws: - - # Setup the session - async def setup() -> None: - await ws.send( - json.dumps( - { - "setup": { - "model": "gemini-live-2.5-flash", - "generation_config": CONFIG, - } - } - ) - ) - - # Receive setup response - raw_response = await ws.recv(decode=False) - setup_response = json.loads(raw_response.decode("ascii")) - print(f"Connected: {setup_response}") - return - - # Send text message - async def send() -> bool: - text_input = input("Input > ") - if text_input.lower() in ("q", "quit", "exit"): - return False - - msg = { - "client_content": { - "turns": [{"role": "user", "parts": [{"text": text_input}]}], - "turn_complete": True, - } - } - - await ws.send(json.dumps(msg)) - return True - - # Receive server response - async def receive() -> None: - responses = [] - - # Receive chucks of server response - async for raw_response in ws: - response = json.loads(raw_response.decode()) - server_content = response.pop("serverContent", None) - if server_content is None: - break - - model_turn = server_content.pop("modelTurn", None) - if model_turn is not None: - parts = model_turn.pop("parts", None) - if parts is not None: - for part in parts: - pcm_data = base64.b64decode(part["inlineData"]["data"]) - responses.append(np.frombuffer(pcm_data, dtype=np.int16)) - - # End of turn - turn_complete = server_content.pop("turnComplete", None) - if turn_complete: - break - - # Play the returned audio message - display(Markdown("**Response >**")) - display(Audio(np.concatenate(responses), rate=24000, autoplay=True)) - return - - await setup() - - while True: - if not await send(): - break - await receive() From 9ac4df70846b042a092340d6a0dcd2f6b421ad92 Mon Sep 17 00:00:00 2001 From: Guiners Date: Tue, 9 Sep 2025 14:53:36 +0200 Subject: [PATCH 15/22] codereview fix --- .../live_conversation_audio_with_audio.py | 21 +++----- genai/live/test_live_examples.py | 51 +++++++++---------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/genai/live/live_conversation_audio_with_audio.py b/genai/live/live_conversation_audio_with_audio.py index 62cadf7f4a2..0c8b14c47f8 100644 --- a/genai/live/live_conversation_audio_with_audio.py +++ b/genai/live/live_conversation_audio_with_audio.py @@ -12,21 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. - # [START googlegenaisdk_live_conversation_audio_with_audio] import asyncio -import wave import base64 + +from google import genai +from google.genai.types import AudioTranscriptionConfig, Blob, LiveConnectConfig, Modality + import numpy as np from scipy.io import wavfile -from google import genai -from google.genai.types import ( - LiveConnectConfig, - Modality, - AudioTranscriptionConfig, - Blob, -) # The number of audio frames to send in each chunk. CHUNK = 4200 @@ -56,7 +51,7 @@ def read_wavefile(filepath: str) -> tuple[str, str]: return base64_encoded_data, mime_type -def write_wavefile(filepath: str, audio_frames: list[bytes], rate: int): +def write_wavefile(filepath: str, audio_frames: list[bytes], rate: int) -> None: """Writes a list of audio byte frames to a WAV file using scipy.""" # Combine the list of byte frames into a single byte string raw_audio_bytes = b"".join(audio_frames) @@ -70,7 +65,7 @@ def write_wavefile(filepath: str, audio_frames: list[bytes], rate: int): print(f"Model response saved to {filepath}") -async def main(): +async def main() -> None: async with client.aio.live.connect( model=MODEL, config=LiveConnectConfig( @@ -83,14 +78,14 @@ async def main(): ), ) as session: - async def send(): + async def send() -> None: # using local file as an example for live audio input wav_file_path = "hello_gemini_are_you_there.wav" base64_data, mime_type = read_wavefile(wav_file_path) audio_bytes = base64.b64decode(base64_data) await session.send_realtime_input(media=Blob(data=audio_bytes, mime_type=mime_type)) - async def receive(): + async def receive() -> None: audio_frames = [] async for message in session.receive(): diff --git a/genai/live/test_live_examples.py b/genai/live/test_live_examples.py index 920a998235a..de494338d87 100644 --- a/genai/live/test_live_examples.py +++ b/genai/live/test_live_examples.py @@ -17,14 +17,14 @@ # import os +from typing import Any -import pytest - -import live_ground_ragengine_with_txt import live_audiogen_with_txt import live_code_exec_with_txt +import live_conversation_audio_with_audio import live_func_call_with_txt import live_ground_googsearch_with_txt +import live_ground_ragengine_with_txt import live_structured_ouput_with_txt import live_transcribe_with_audio import live_txtgen_with_audio @@ -33,38 +33,37 @@ import live_websocket_textgen_with_audio import live_websocket_textgen_with_txt import live_with_txt -import live_conversation_audio_with_audio + +import pytest + os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" # The project name is included in the CICD pipeline # os.environ['GOOGLE_CLOUD_PROJECT'] = "add-your-project-name" -@pytest.fixture() -def mock_rag_components(mocker): - mock_client_cls = mocker.patch("google.genai.Client") - - class AsyncIterator: - def __aiter__(self): - return self - - - async def __anext__(self): - if not hasattr(self, "used"): - self.used = True - return mocker.MagicMock( - text="Mariusz Pudzianowski won in 2002, 2003, 2005, 2007, and 2008." - ) - raise StopAsyncIteration +@pytest.fixture() +def mock_rag_components(mocker: Any) -> None: + mock_client_cls = mocker.patch("google.genai.Client") + class AsyncIterator: + def __aiter__(self) -> Any: + return self - mock_session = mocker.AsyncMock() - mock_session.__aenter__.return_value = mock_session - mock_session.receive = lambda: AsyncIterator() + async def __anext__(self) -> Any: + if not hasattr(self, "used"): + self.used = True + return mocker.MagicMock( + text="Mariusz Pudzianowski won in 2002, 2003, 2005, 2007, and 2008." + ) + raise StopAsyncIteration + mock_session = mocker.AsyncMock() + mock_session.__aenter__.return_value = mock_session + mock_session.receive = lambda: AsyncIterator() - mock_client_cls.return_value.aio.live.connect.return_value = mock_session + mock_client_cls.return_value.aio.live.connect.return_value = mock_session @pytest.mark.asyncio @@ -129,8 +128,8 @@ async def test_live_structured_ouput_with_txt() -> None: @pytest.mark.asyncio -async def test_live_ground_ragengine_with_txt(mock_rag_components) -> None: - assert await live_ground_ragengine_with_txt.generate_content("test") +async def test_live_ground_ragengine_with_txt(mock_rag_components: Any) -> None: + assert await live_ground_ragengine_with_txt.generate_content("test") @pytest.mark.asyncio From 0fa8a12b6e6749223b94ebe0c32c5684f88269e4 Mon Sep 17 00:00:00 2001 From: Guiners Date: Tue, 9 Sep 2025 16:28:51 +0200 Subject: [PATCH 16/22] codereview fix --- .../live_conversation_audio_with_audio.py | 5 +- genai/live/requirements-test.txt | 1 + genai/live/test_live_examples.py | 66 ++++++++++++++++--- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/genai/live/live_conversation_audio_with_audio.py b/genai/live/live_conversation_audio_with_audio.py index 0c8b14c47f8..d17886c4e78 100644 --- a/genai/live/live_conversation_audio_with_audio.py +++ b/genai/live/live_conversation_audio_with_audio.py @@ -65,7 +65,8 @@ def write_wavefile(filepath: str, audio_frames: list[bytes], rate: int) -> None: print(f"Model response saved to {filepath}") -async def main() -> None: +async def main(): + print("Starting the code") async with client.aio.live.connect( model=MODEL, config=LiveConnectConfig( @@ -119,7 +120,7 @@ async def receive() -> None: # Model response saved to example_model_response.wav # [END googlegenaisdk_live_conversation_audio_with_audio] - + return receive_task if __name__ == "__main__": asyncio.run(main()) diff --git a/genai/live/requirements-test.txt b/genai/live/requirements-test.txt index 1b59fd9d249..1cb0b5ef6a1 100644 --- a/genai/live/requirements-test.txt +++ b/genai/live/requirements-test.txt @@ -2,3 +2,4 @@ backoff==2.2.1 google-api-core==2.25.1 pytest==8.4.1 pytest-asyncio==1.1.0 +pytest-mock==3.14.0 diff --git a/genai/live/test_live_examples.py b/genai/live/test_live_examples.py index de494338d87..cb00fffcf3e 100644 --- a/genai/live/test_live_examples.py +++ b/genai/live/test_live_examples.py @@ -48,20 +48,68 @@ def mock_rag_components(mocker: Any) -> None: mock_client_cls = mocker.patch("google.genai.Client") class AsyncIterator: - def __aiter__(self) -> Any: + def __init__(self): + self.used = False + + def __aiter__(self): + return self + + async def __anext__(self): + if not self.used: + self.used = True + return mocker.MagicMock( + text="Mariusz Pudzianowski won in 2002, 2003, 2005, 2007, and 2008." + ) + raise StopAsyncIteration + + mock_session = mocker.AsyncMock() + mock_session.__aenter__.return_value = mock_session + mock_session.receive = lambda: AsyncIterator() + + mock_client_cls.return_value.aio.live.connect.return_value = mock_session + + +@pytest.fixture() +def mock_audio_components(mocker: Any) -> None: + mock_client_cls = mocker.patch("google.genai.Client") + + class AsyncIterator: + def __init__(self): + self.used = 0 + + def __aiter__(self): return self - async def __anext__(self) -> Any: - if not hasattr(self, "used"): - self.used = True - return mocker.MagicMock( - text="Mariusz Pudzianowski won in 2002, 2003, 2005, 2007, and 2008." - ) - raise StopAsyncIteration + async def __anext__(self): + if self.used == 0: + self.used += 1 + msg = mocker.MagicMock() + msg.server_content.input_transcription = {"text": "Hello."} + msg.server_content.output_transcription = None + msg.server_content.model_turn = None + return msg + elif self.used == 1: + self.used += 1 + msg = mocker.MagicMock() + msg.server_content.input_transcription = None + msg.server_content.output_transcription = {"text": "Hi there!"} + msg.server_content.model_turn = None + return msg + elif self.used == 2: + self.used += 1 + msg = mocker.MagicMock() + msg.server_content.input_transcription = None + msg.server_content.output_transcription = None + part = mocker.MagicMock() + part.inline_data.data = b"\x00\x01" # fake audio data + msg.server_content.model_turn.parts = [part] + return msg + raise StopAsyncIteration mock_session = mocker.AsyncMock() mock_session.__aenter__.return_value = mock_session mock_session.receive = lambda: AsyncIterator() + mock_session.send_realtime_input = mocker.AsyncMock() mock_client_cls.return_value.aio.live.connect.return_value = mock_session @@ -133,5 +181,5 @@ async def test_live_ground_ragengine_with_txt(mock_rag_components: Any) -> None: @pytest.mark.asyncio -async def test_live_conversation_audio_with_audio() -> None: +async def test_live_conversation_audio_with_audio(mock_audio_components) -> None: assert await live_conversation_audio_with_audio.main() From 21a5b43c1a780e8a2f6210c888672e5d9e5a3156 Mon Sep 17 00:00:00 2001 From: Guiners Date: Tue, 9 Sep 2025 16:56:52 +0200 Subject: [PATCH 17/22] codereview fix --- genai/live/live_conversation_audio_with_audio.py | 5 +++-- genai/live/test_live_examples.py | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/genai/live/live_conversation_audio_with_audio.py b/genai/live/live_conversation_audio_with_audio.py index d17886c4e78..6fd87489b10 100644 --- a/genai/live/live_conversation_audio_with_audio.py +++ b/genai/live/live_conversation_audio_with_audio.py @@ -16,6 +16,7 @@ import asyncio import base64 +from typing import List from google import genai from google.genai.types import AudioTranscriptionConfig, Blob, LiveConnectConfig, Modality @@ -65,7 +66,7 @@ def write_wavefile(filepath: str, audio_frames: list[bytes], rate: int) -> None: print(f"Model response saved to {filepath}") -async def main(): +async def main() -> List: print("Starting the code") async with client.aio.live.connect( model=MODEL, @@ -120,7 +121,7 @@ async def receive() -> None: # Model response saved to example_model_response.wav # [END googlegenaisdk_live_conversation_audio_with_audio] - return receive_task + return list(receive_task) if __name__ == "__main__": asyncio.run(main()) diff --git a/genai/live/test_live_examples.py b/genai/live/test_live_examples.py index cb00fffcf3e..38d00cb4e51 100644 --- a/genai/live/test_live_examples.py +++ b/genai/live/test_live_examples.py @@ -17,8 +17,11 @@ # import os +import pytest_mock from typing import Any +import pytest + import live_audiogen_with_txt import live_code_exec_with_txt import live_conversation_audio_with_audio @@ -34,8 +37,6 @@ import live_websocket_textgen_with_txt import live_with_txt -import pytest - os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" @@ -44,7 +45,7 @@ @pytest.fixture() -def mock_rag_components(mocker: Any) -> None: +def mock_rag_components(mocker: pytest_mock.MockerFixture) -> None: mock_client_cls = mocker.patch("google.genai.Client") class AsyncIterator: @@ -70,7 +71,7 @@ async def __anext__(self): @pytest.fixture() -def mock_audio_components(mocker: Any) -> None: +def mock_audio_components(mocker: pytest_mock.MockerFixture) -> None: mock_client_cls = mocker.patch("google.genai.Client") class AsyncIterator: From 8b172a153ed3f0d177b4bad448509a68819443f7 Mon Sep 17 00:00:00 2001 From: Guiners Date: Tue, 9 Sep 2025 17:01:27 +0200 Subject: [PATCH 18/22] linter fix --- .../live_conversation_audio_with_audio.py | 5 +++-- genai/live/live_ground_ragengine_with_txt.py | 13 +++--------- genai/live/test_live_examples.py | 20 +++++++++---------- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/genai/live/live_conversation_audio_with_audio.py b/genai/live/live_conversation_audio_with_audio.py index 6fd87489b10..1d464f0f2e9 100644 --- a/genai/live/live_conversation_audio_with_audio.py +++ b/genai/live/live_conversation_audio_with_audio.py @@ -19,9 +19,10 @@ from typing import List from google import genai -from google.genai.types import AudioTranscriptionConfig, Blob, LiveConnectConfig, Modality - +from google.genai.types import (AudioTranscriptionConfig, Blob, + LiveConnectConfig, Modality) import numpy as np + from scipy.io import wavfile # The number of audio frames to send in each chunk. diff --git a/genai/live/live_ground_ragengine_with_txt.py b/genai/live/live_ground_ragengine_with_txt.py index 2452a27071b..8fe0b273fa5 100644 --- a/genai/live/live_ground_ragengine_with_txt.py +++ b/genai/live/live_ground_ragengine_with_txt.py @@ -19,16 +19,9 @@ async def generate_content(memory_corpus: str) -> list[str]: # [START googlegenaisdk_live_ground_ragengine_with_txt] from google import genai - from google.genai.types import ( - Content, - LiveConnectConfig, - Modality, - Part, - Tool, - Retrieval, - VertexRagStore, - VertexRagStoreRagResource, - ) + from google.genai.types import (Content, LiveConnectConfig, Modality, Part, + Retrieval, Tool, VertexRagStore, + VertexRagStoreRagResource) client = genai.Client() model_id = "gemini-2.0-flash-live-preview-04-09" diff --git a/genai/live/test_live_examples.py b/genai/live/test_live_examples.py index 38d00cb4e51..5cd35b7fc75 100644 --- a/genai/live/test_live_examples.py +++ b/genai/live/test_live_examples.py @@ -17,10 +17,9 @@ # import os -import pytest_mock -from typing import Any import pytest +import pytest_mock import live_audiogen_with_txt import live_code_exec_with_txt @@ -37,7 +36,6 @@ import live_websocket_textgen_with_txt import live_with_txt - os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" # The project name is included in the CICD pipeline @@ -49,13 +47,13 @@ def mock_rag_components(mocker: pytest_mock.MockerFixture) -> None: mock_client_cls = mocker.patch("google.genai.Client") class AsyncIterator: - def __init__(self): + def __init__(self) -> None: self.used = False - def __aiter__(self): + def __aiter__(self) -> "AsyncIterator": return self - async def __anext__(self): + async def __anext__(self) -> object: if not self.used: self.used = True return mocker.MagicMock( @@ -75,13 +73,13 @@ def mock_audio_components(mocker: pytest_mock.MockerFixture) -> None: mock_client_cls = mocker.patch("google.genai.Client") class AsyncIterator: - def __init__(self): + def __init__(self) -> None: self.used = 0 - def __aiter__(self): + def __aiter__(self) -> "AsyncIterator": return self - async def __anext__(self): + async def __anext__(self) -> object: if self.used == 0: self.used += 1 msg = mocker.MagicMock() @@ -177,10 +175,10 @@ async def test_live_structured_ouput_with_txt() -> None: @pytest.mark.asyncio -async def test_live_ground_ragengine_with_txt(mock_rag_components: Any) -> None: +async def test_live_ground_ragengine_with_txt(mock_rag_components: None) -> None: assert await live_ground_ragengine_with_txt.generate_content("test") @pytest.mark.asyncio -async def test_live_conversation_audio_with_audio(mock_audio_components) -> None: +async def test_live_conversation_audio_with_audio(mock_audio_components: None) -> None: assert await live_conversation_audio_with_audio.main() From b10699cd29e85cc7306fa645b954617d86a7dc5c Mon Sep 17 00:00:00 2001 From: Guiners Date: Tue, 9 Sep 2025 18:16:44 +0200 Subject: [PATCH 19/22] linter fix --- genai/live/live_conversation_audio_with_audio.py | 5 ++--- genai/text_generation/test_text_generation_examples.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/genai/live/live_conversation_audio_with_audio.py b/genai/live/live_conversation_audio_with_audio.py index 1d464f0f2e9..1072b0f061f 100644 --- a/genai/live/live_conversation_audio_with_audio.py +++ b/genai/live/live_conversation_audio_with_audio.py @@ -16,7 +16,6 @@ import asyncio import base64 -from typing import List from google import genai from google.genai.types import (AudioTranscriptionConfig, Blob, @@ -67,7 +66,7 @@ def write_wavefile(filepath: str, audio_frames: list[bytes], rate: int) -> None: print(f"Model response saved to {filepath}") -async def main() -> List: +async def main() -> bool: print("Starting the code") async with client.aio.live.connect( model=MODEL, @@ -122,7 +121,7 @@ async def receive() -> None: # Model response saved to example_model_response.wav # [END googlegenaisdk_live_conversation_audio_with_audio] - return list(receive_task) + return True if __name__ == "__main__": asyncio.run(main()) diff --git a/genai/text_generation/test_text_generation_examples.py b/genai/text_generation/test_text_generation_examples.py index 5c0f1e6c6d8..5277f02f4dc 100644 --- a/genai/text_generation/test_text_generation_examples.py +++ b/genai/text_generation/test_text_generation_examples.py @@ -141,4 +141,4 @@ def test_textgen_with_youtube_video() -> None: # os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1" # response = model_optimizer_textgen_with_txt.generate_content() # os.environ["GOOGLE_CLOUD_LOCATION"] = "global" # "us-central1" -# assert response \ No newline at end of file +# assert response From 57d5ec5fc0094e549bd89ca6dfcb4df43aeadaf0 Mon Sep 17 00:00:00 2001 From: Guiners Date: Thu, 2 Oct 2025 14:23:21 +0200 Subject: [PATCH 20/22] model update --- genai/live/live_conversation_audio_with_audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/genai/live/live_conversation_audio_with_audio.py b/genai/live/live_conversation_audio_with_audio.py index 1072b0f061f..d411251dd3e 100644 --- a/genai/live/live_conversation_audio_with_audio.py +++ b/genai/live/live_conversation_audio_with_audio.py @@ -27,7 +27,7 @@ # The number of audio frames to send in each chunk. CHUNK = 4200 CHANNELS = 1 -MODEL = "gemini-2.0-flash-live-preview-04-09" +MODEL = "gemini-live-2.5-flash-preview-native-audio-09-2025" # The audio sample rate expected by the model. INPUT_RATE = 16000 From 9005cbbaf9d6bc68eaa35af6cf7c0d486f787dc0 Mon Sep 17 00:00:00 2001 From: Guiners Date: Mon, 6 Oct 2025 19:01:52 +0200 Subject: [PATCH 21/22] code review fix --- genai/live/live_conversation_audio_with_audio.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/genai/live/live_conversation_audio_with_audio.py b/genai/live/live_conversation_audio_with_audio.py index d411251dd3e..1f188923e87 100644 --- a/genai/live/live_conversation_audio_with_audio.py +++ b/genai/live/live_conversation_audio_with_audio.py @@ -19,7 +19,7 @@ from google import genai from google.genai.types import (AudioTranscriptionConfig, Blob, - LiveConnectConfig, Modality) + LiveConnectConfig, Modality, HttpOptions) import numpy as np from scipy.io import wavfile @@ -37,7 +37,7 @@ # The sample width for 16-bit audio, which is standard for this type of audio data. SAMPLE_WIDTH = 2 -client = genai.Client() +client = genai.Client(http_options=HttpOptions(api_version="v1beta1")) def read_wavefile(filepath: str) -> tuple[str, str]: @@ -68,6 +68,7 @@ def write_wavefile(filepath: str, audio_frames: list[bytes], rate: int) -> None: async def main() -> bool: print("Starting the code") + async with client.aio.live.connect( model=MODEL, config=LiveConnectConfig( From 211d42967ced08ad114af188488db9d8d2e0897b Mon Sep 17 00:00:00 2001 From: Guiners Date: Wed, 8 Oct 2025 11:22:56 +0200 Subject: [PATCH 22/22] code review fix --- genai/live/live_websocket_textgen_with_audio.py | 4 +--- genai/live/test_live_examples.py | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/genai/live/live_websocket_textgen_with_audio.py b/genai/live/live_websocket_textgen_with_audio.py index 3241e3ce972..b9f3aae975d 100644 --- a/genai/live/live_websocket_textgen_with_audio.py +++ b/genai/live/live_websocket_textgen_with_audio.py @@ -101,9 +101,7 @@ def read_wavefile(filepath: str) -> tuple[str, str]: return "Error: WebSocket setup failed." # 3. Send audio message - encoded_audio_message, mime_type = read_wavefile( - "hello_gemini_are_you_there.wav" - ) + encoded_audio_message, mime_type = read_wavefile("hello_gemini_are_you_there.wav") # Example audio message: "Hello? Gemini are you there?" user_message = { diff --git a/genai/live/test_live_examples.py b/genai/live/test_live_examples.py index 5cd35b7fc75..95222a4aa13 100644 --- a/genai/live/test_live_examples.py +++ b/genai/live/test_live_examples.py @@ -118,9 +118,9 @@ async def test_live_with_text() -> None: assert await live_with_txt.generate_content() -@pytest.mark.asyncio -async def test_live_websocket_textgen_with_audio() -> None: - assert await live_websocket_textgen_with_audio.generate_content() +# @pytest.mark.asyncio +# async def test_live_websocket_textgen_with_audio() -> None: +# assert await live_websocket_textgen_with_audio.generate_content() @pytest.mark.asyncio