LLMContext: create_image_message/create_audio_message are now async

This commit is contained in:
Aleix Conchillo Flaqué
2025-11-18 08:38:45 -08:00
parent 6481094638
commit 9f45ad4d2e
6 changed files with 38 additions and 21 deletions

View File

@@ -28,6 +28,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- ⚠️ Breaking change: `LLMContext.create_image_message()` and
`LLMContext.create_audio_message()` are now async methods. This fixes and
issue where the asyncio event loop would be blocked while encoding audio or
images.
- `ConsumerProcessor` now queues frames from the producer internally instead of - `ConsumerProcessor` now queues frames from the producer internally instead of
pushing them directly. This allows us to subclass consumer processors and pushing them directly. This allows us to subclass consumer processors and
manipulate frames before they are pushed. manipulate frames before they are pushed.

View File

@@ -110,7 +110,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
# Kick off the conversation. # Kick off the conversation.
image = Image.open(image_path) image = Image.open(image_path)
message = LLMContext.create_image_message( message = await LLMContext.create_image_message(
image=image.tobytes(), image=image.tobytes(),
format="RGB", format="RGB",
size=image.size, size=image.size,

View File

@@ -110,7 +110,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
# Kick off the conversation. # Kick off the conversation.
image = Image.open(image_path) image = Image.open(image_path)
message = LLMContext.create_image_message( message = await LLMContext.create_image_message(
image=image.tobytes(), image=image.tobytes(),
format="RGB", format="RGB",
size=image.size, size=image.size,

View File

@@ -117,7 +117,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
# Kick off the conversation. # Kick off the conversation.
image = Image.open(image_path) image = Image.open(image_path)
message = LLMContext.create_image_message( message = await LLMContext.create_image_message(
image=image.tobytes(), image=image.tobytes(),
format="RGB", format="RGB",
size=image.size, size=image.size,

View File

@@ -110,7 +110,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
# Kick off the conversation. # Kick off the conversation.
image = Image.open(image_path) image = Image.open(image_path)
message = LLMContext.create_image_message( message = await LLMContext.create_image_message(
image=image.tobytes(), image=image.tobytes(),
format="RGB", format="RGB",
size=image.size, size=image.size,

View File

@@ -14,6 +14,7 @@ translation from this universal context into whatever format it needs, using a
service-specific adapter. service-specific adapter.
""" """
import asyncio
import base64 import base64
import io import io
import wave import wave
@@ -137,7 +138,7 @@ class LLMContext:
return {"role": role, "content": content} return {"role": role, "content": content}
@staticmethod @staticmethod
def create_image_message( async def create_image_message(
*, *,
role: str = "user", role: str = "user",
format: str, format: str,
@@ -154,15 +155,21 @@ class LLMContext:
image: Raw image bytes. image: Raw image bytes.
text: Optional text to include with the image. text: Optional text to include with the image.
""" """
buffer = io.BytesIO()
Image.frombytes(format, size, image).save(buffer, format="JPEG") def encode_image():
encoded_image = base64.b64encode(buffer.getvalue()).decode("utf-8") buffer = io.BytesIO()
Image.frombytes(format, size, image).save(buffer, format="JPEG")
encoded_image = base64.b64encode(buffer.getvalue()).decode("utf-8")
return encoded_image
encoded_image = await asyncio.to_thread(encode_image)
url = f"data:image/jpeg;base64,{encoded_image}" url = f"data:image/jpeg;base64,{encoded_image}"
return LLMContext.create_image_url_message(role=role, url=url, text=text) return LLMContext.create_image_url_message(role=role, url=url, text=text)
@staticmethod @staticmethod
def create_audio_message( async def create_audio_message(
*, role: str = "user", audio_frames: list[AudioRawFrame], text: str = "Audio follows" *, role: str = "user", audio_frames: list[AudioRawFrame], text: str = "Audio follows"
) -> LLMContextMessage: ) -> LLMContextMessage:
"""Create a context message containing audio. """Create a context message containing audio.
@@ -172,21 +179,26 @@ class LLMContext:
audio_frames: List of audio frame objects to include. audio_frames: List of audio frame objects to include.
text: Optional text to include with the audio. text: Optional text to include with the audio.
""" """
sample_rate = audio_frames[0].sample_rate
num_channels = audio_frames[0].num_channels
content = [] def encode_audio():
content.append({"type": "text", "text": text}) sample_rate = audio_frames[0].sample_rate
data = b"".join(frame.audio for frame in audio_frames) num_channels = audio_frames[0].num_channels
with io.BytesIO() as buffer: content = []
with wave.open(buffer, "wb") as wf: content.append({"type": "text", "text": text})
wf.setsampwidth(2) data = b"".join(frame.audio for frame in audio_frames)
wf.setnchannels(num_channels)
wf.setframerate(sample_rate)
wf.writeframes(data)
encoded_audio = base64.b64encode(buffer.getvalue()).decode("utf-8") with io.BytesIO() as buffer:
with wave.open(buffer, "wb") as wf:
wf.setsampwidth(2)
wf.setnchannels(num_channels)
wf.setframerate(sample_rate)
wf.writeframes(data)
encoded_audio = base64.b64encode(buffer.getvalue()).decode("utf-8")
return encoded_audio
encoded_audio = asyncio.to_thread(encode_audio)
content.append( content.append(
{ {