Skip to content
Merged
1 change: 1 addition & 0 deletions packages/capabilities/image-generation/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Issues = "https://github.com/withceleste/celeste-python/issues"
celeste-ai = { workspace = true }
celeste-bfl = { workspace = true }
celeste-google = { workspace = true }
celeste-openai = { workspace = true }

[project.entry-points."celeste.packages"]
image-generation = "celeste_image_generation:register_package"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ def _init_request(self, inputs: ImageGenerationInput) -> dict[str, Any]:

def _parse_usage(self, response_data: dict[str, Any]) -> ImageGenerationUsage:
"""Parse usage from response."""
predictions = response_data.get("predictions", [])
return ImageGenerationUsage(num_images=len(predictions))
usage = super()._parse_usage(response_data)
return ImageGenerationUsage(**usage)

def _parse_content(
self,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
"""OpenAI client implementation for image generation."""

import base64
from collections.abc import AsyncIterator
from typing import Any, Unpack

import httpx
from celeste_openai.images.client import OpenAIImagesClient

from celeste.artifacts import ImageArtifact
from celeste.mime_types import ApplicationMimeType
from celeste.parameters import ParameterMapper
from celeste_image_generation.client import ImageGenerationClient
from celeste_image_generation.io import (
Expand All @@ -17,12 +15,11 @@
)
from celeste_image_generation.parameters import ImageGenerationParameters

from . import config
from .parameters import OPENAI_PARAMETER_MAPPERS
from .streaming import OpenAIImageGenerationStream


class OpenAIImageGenerationClient(ImageGenerationClient):
class OpenAIImageGenerationClient(OpenAIImagesClient, ImageGenerationClient):
"""OpenAI client for image generation."""

@classmethod
Expand All @@ -44,19 +41,17 @@ def _init_request(self, inputs: ImageGenerationInput) -> dict[str, Any]:

def _parse_usage(self, response_data: dict[str, Any]) -> ImageGenerationUsage:
"""Parse usage from response."""
return ImageGenerationUsage()
usage = super()._parse_usage(response_data)
return ImageGenerationUsage(**usage)

def _parse_content(
self,
response_data: dict[str, Any],
**parameters: Unpack[ImageGenerationParameters],
) -> ImageArtifact:
"""Parse content from response."""
data = response_data.get("data", [])
if not data:
msg = "No image data in response"
raise ValueError(msg)

# Use mixin's _parse_content to get data array
data = super()._parse_content(response_data)
image_data = data[0]

b64_json = image_data.get("b64_json")
Expand All @@ -73,62 +68,13 @@ def _parse_content(

def _parse_finish_reason(
self, response_data: dict[str, Any]
) -> ImageGenerationFinishReason | None:
"""Parse finish reason from response."""
return None

def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]:
"""Build metadata dictionary from response data."""
metadata = super()._build_metadata(response_data)
# Add provider-specific parsed fields
if response_data.get("data") and response_data["data"]:
revised_prompt = response_data["data"][0].get("revised_prompt")
if revised_prompt:
metadata["revised_prompt"] = revised_prompt
return metadata

async def _make_request(
self,
request_body: dict[str, Any],
**parameters: Unpack[ImageGenerationParameters],
) -> httpx.Response:
"""Make HTTP request(s) and return response object."""
headers = {
**self.auth.get_headers(),
"Content-Type": ApplicationMimeType.JSON,
}

return await self.http_client.post(
f"{config.BASE_URL}{config.ENDPOINT}",
headers=headers,
json_body=request_body,
)
) -> ImageGenerationFinishReason:
"""OpenAI Images API doesn't provide finish reasons."""
return ImageGenerationFinishReason(reason=None)

def _stream_class(self) -> type[OpenAIImageGenerationStream]:
"""Return the Stream class for this client."""
return OpenAIImageGenerationStream

def _make_stream_request(
self,
request_body: dict[str, Any],
**parameters: Unpack[ImageGenerationParameters],
) -> AsyncIterator[dict[str, Any]]:
"""Make HTTP streaming request and return async iterator of events."""
request_body["stream"] = True

if "partial_images" not in request_body:
request_body["partial_images"] = 1

headers = {
**self.auth.get_headers(),
"Content-Type": ApplicationMimeType.JSON,
}

return self.http_client.stream_post(
f"{config.BASE_URL}{config.STREAM_ENDPOINT}",
headers=headers,
json_body=request_body,
)


__all__ = ["OpenAIImageGenerationClient"]

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,113 +1,30 @@
"""OpenAI parameter mappers for image generation."""
"""OpenAI Images parameter mappers for image generation."""

from celeste_openai.images.parameters import (
PartialImagesMapper as _PartialImagesMapper,
)
from celeste_openai.images.parameters import (
QualityMapper as _QualityMapper,
)
from celeste_openai.images.parameters import (
SizeMapper as _SizeMapper,
)

from typing import Any

from celeste import Model
from celeste.parameters import ParameterMapper
from celeste_image_generation.parameters import ImageGenerationParameter


class AspectRatioMapper(ParameterMapper):
"""Map aspect_ratio parameter to OpenAI's size parameter."""

class AspectRatioMapper(_SizeMapper):
name = ImageGenerationParameter.ASPECT_RATIO

def map(
self,
request: dict[str, Any],
value: object,
model: Model,
) -> dict[str, Any]:
"""Transform aspect_ratio into provider request.

Maps unified aspect_ratio parameter to OpenAI's size format.
Values are OpenAI's native size strings (e.g., "1024x1024", "1792x1024").
Coercion from ratio format ("16:9") to size format can be added later.

Args:
request: Provider request dictionary to modify.
value: The aspect_ratio value (OpenAI size string).
model: Model instance with parameter constraints.

Returns:
Modified request dictionary with size parameter.
"""
validated_value = self._validate_value(value, model)
if validated_value is None:
return request

# Transform to provider-specific request format (size parameter)
request["size"] = validated_value
return request


class PartialImagesMapper(ParameterMapper):
"""Map partial_images parameter for streaming."""

class PartialImagesMapper(_PartialImagesMapper):
name = ImageGenerationParameter.PARTIAL_IMAGES

def map(
self,
request: dict[str, Any],
value: object,
model: Model,
) -> dict[str, Any]:
"""Transform partial_images into provider request.

Controls number of partial images during streaming (0-3).

Args:
request: Provider request dictionary to modify.
value: The partial_images value (0-3).
model: Model instance with parameter constraints.

Returns:
Modified request dictionary with partial_images parameter.
"""
validated_value = self._validate_value(value, model)
if validated_value is None:
return request

# Transform to provider-specific request format (top-level field)
request["partial_images"] = validated_value
return request


class QualityMapper(ParameterMapper):
"""Map quality parameter"""

class QualityMapper(_QualityMapper):
name = ImageGenerationParameter.QUALITY

def map(
self,
request: dict[str, Any],
value: object,
model: Model,
) -> dict[str, Any]:
"""Transform quality into provider request.

Controls image quality/detail level.
- DALL-E 3: "standard" or "hd"
- gpt-image-1: "low", "medium", "high", or "auto"
- gpt-image-1-mini: "low", "medium", "high", or "auto"
- DALL-E 2: Not supported (no constraint in model)

Args:
request: Provider request dictionary to modify.
value: The quality value.
model: Model instance with parameter constraints.

Returns:
Modified request dictionary with quality parameter.
"""
validated_value = self._validate_value(value, model)
if validated_value is None:
return request

# Transform to provider-specific request format (top-level field)
request["quality"] = validated_value
return request


OPENAI_PARAMETER_MAPPERS: list[ParameterMapper] = [
AspectRatioMapper(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"""Integration tests for image generation capability."""
"""Image generation integration test module."""
1 change: 1 addition & 0 deletions packages/capabilities/speech-generation/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Issues = "https://github.com/withceleste/celeste-python/issues"

[tool.uv.sources]
celeste-ai = { workspace = true }
celeste-openai = { workspace = true }

[project.entry-points."celeste.packages"]
speech-generation = "celeste_speech_generation:register_package"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,12 @@ def _parse_content(
"""Parse content from provider response."""

def _create_inputs(
self,
*args: str,
text: str | None = None,
**parameters: Unpack[SpeechGenerationParameters],
self, *args: str, **parameters: Unpack[SpeechGenerationParameters]
) -> SpeechGenerationInput:
"""Map positional arguments to Input type."""
if args:
return SpeechGenerationInput(text=args[0])
text: str | None = parameters.get("text")
if text is None:
msg = "text is required (either as positional argument or keyword argument)"
raise ValidationError(msg)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@


class VoiceConstraint(Constraint):
"""Voice constraint - value must be a valid voice ID or name from the provided voices.

Accepts both voice IDs and names. If a name is provided, returns the corresponding ID.
"""
"""Voice constraint - value must be a valid voice ID from the provided voices."""

voices: list[Voice] = Field(min_length=1)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Input and output types for speech generation."""

from celeste.artifacts import AudioArtifact
from celeste.io import Chunk, Input, Output, Usage
from celeste.io import Chunk, FinishReason, Input, Output, Usage


class SpeechGenerationInput(Input):
Expand All @@ -17,23 +17,26 @@ class SpeechGenerationUsage(Usage):
"""


class SpeechGenerationFinishReason(FinishReason):
"""Finish reason for speech generation."""


class SpeechGenerationOutput(Output[AudioArtifact]):
"""Output with audio artifact content."""


class SpeechGenerationChunk(Chunk[bytes]):
"""Typed chunk for speech generation streaming.

Note: Unlike TextGenerationChunk, this class intentionally omits a finish_reason
field. TTS providers stream raw audio bytes without completion signals - the
stream simply ends when audio generation is complete.
Speech streaming sends raw bytes without finish_reason.
"""

usage: SpeechGenerationUsage | None = None


__all__ = [
"SpeechGenerationChunk",
"SpeechGenerationFinishReason",
"SpeechGenerationInput",
"SpeechGenerationOutput",
"SpeechGenerationUsage",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ class SpeechGenerationParameter(StrEnum):

VOICE = "voice"
SPEED = "speed"
RESPONSE_FORMAT = "response_format"
OUTPUT_FORMAT = "output_format"
PROMPT = "prompt"
LANGUAGE = "language"


class SpeechGenerationParameters(Parameters):
"""Parameters for speech generation."""

voice: str | None
speed: float | None
response_format: str | None
output_format: str | None
prompt: str | None
Loading
Loading