LLMContext: create_image_message/create_audio_message are now async
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user