Compare commits
3 commits
renovate/l
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f883d94290 | |||
| 47b5f899e6 | |||
| d90db2933e |
6 changed files with 39 additions and 22 deletions
|
|
@ -91,9 +91,9 @@ def _collect_extra_params(target_name: str, config: ProjectConfig) -> dict[str,
|
||||||
if target_cfg.height is not None:
|
if target_cfg.height is not None:
|
||||||
params["height"] = target_cfg.height
|
params["height"] = target_cfg.height
|
||||||
if target_cfg.reference_images:
|
if target_cfg.reference_images:
|
||||||
params["reference_images"] = tuple(target_cfg.reference_images)
|
params["reference_images"] = list(target_cfg.reference_images)
|
||||||
if target_cfg.control_images:
|
if target_cfg.control_images:
|
||||||
params["control_images"] = tuple(target_cfg.control_images)
|
params["control_images"] = list(target_cfg.control_images)
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import io
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal, override
|
from typing import Literal, override
|
||||||
|
|
||||||
|
|
@ -128,7 +129,6 @@ class OpenAIImageProvider(Provider):
|
||||||
target_config.reference_images,
|
target_config.reference_images,
|
||||||
project_dir,
|
project_dir,
|
||||||
size,
|
size,
|
||||||
output_format,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
response = await _generate_new(
|
response = await _generate_new(
|
||||||
|
|
@ -183,17 +183,30 @@ async def _generate_edit(
|
||||||
reference_images: list[str],
|
reference_images: list[str],
|
||||||
project_dir: Path,
|
project_dir: Path,
|
||||||
size: _SIZE | None,
|
size: _SIZE | None,
|
||||||
output_format: str | None = None,
|
|
||||||
) -> ImagesResponse:
|
) -> ImagesResponse:
|
||||||
"""Generate an image using reference images via the edits endpoint.
|
"""Generate an image using reference images via the edits endpoint.
|
||||||
|
|
||||||
gpt-image-* models accept up to 16 images and return b64 by default
|
gpt-image-* models accept up to 16 images and return b64 by default
|
||||||
(they reject ``response_format``). DALL-E 2 accepts only one image.
|
(they reject ``response_format`` and ``output_format``).
|
||||||
|
DALL-E 2 accepts only one image.
|
||||||
"""
|
"""
|
||||||
images = [(project_dir / name).read_bytes() for name in reference_images]
|
raw_images = [(project_dir / name).read_bytes() for name in reference_images]
|
||||||
image: bytes | list[bytes] = images[0] if len(images) == 1 else images
|
|
||||||
|
|
||||||
if model.startswith("gpt-image-"):
|
if model.startswith("gpt-image-"):
|
||||||
|
# gpt-image-* models require file-like objects with a name attribute;
|
||||||
|
# raw bytes trigger the legacy multipart path that only accepts dall-e-2.
|
||||||
|
def _to_named_buf(data: bytes, name: str) -> io.BytesIO:
|
||||||
|
buf = io.BytesIO(data)
|
||||||
|
buf.name = name
|
||||||
|
return buf
|
||||||
|
|
||||||
|
file_images = [
|
||||||
|
_to_named_buf(data, name)
|
||||||
|
for data, name in zip(raw_images, reference_images, strict=True)
|
||||||
|
]
|
||||||
|
image: io.BytesIO | list[io.BytesIO] = (
|
||||||
|
file_images[0] if len(file_images) == 1 else file_images
|
||||||
|
)
|
||||||
kwargs: dict[str, object] = {
|
kwargs: dict[str, object] = {
|
||||||
"image": image,
|
"image": image,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
|
|
@ -202,12 +215,13 @@ async def _generate_edit(
|
||||||
}
|
}
|
||||||
if size is not None:
|
if size is not None:
|
||||||
kwargs["size"] = size
|
kwargs["size"] = size
|
||||||
if output_format is not None:
|
|
||||||
kwargs["output_format"] = output_format
|
|
||||||
return await client.images.edit(**kwargs) # pyright: ignore[reportCallIssue,reportArgumentType,reportUnknownVariableType]
|
return await client.images.edit(**kwargs) # pyright: ignore[reportCallIssue,reportArgumentType,reportUnknownVariableType]
|
||||||
|
|
||||||
|
dalle_image: bytes | list[bytes] = (
|
||||||
|
raw_images[0] if len(raw_images) == 1 else raw_images
|
||||||
|
)
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"image": image,
|
"image": dalle_image,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"model": model,
|
"model": model,
|
||||||
"n": 1,
|
"n": 1,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ dependencies = [
|
||||||
"httpx>=0.27.0",
|
"httpx>=0.27.0",
|
||||||
"mistralai>=1.0.0",
|
"mistralai>=1.0.0",
|
||||||
"networkx>=3.6.1",
|
"networkx>=3.6.1",
|
||||||
"openai>=2.21.0",
|
"openai>=2.25.0",
|
||||||
"pillow>=11.0.0",
|
"pillow>=11.0.0",
|
||||||
"pydantic>=2.12.5",
|
"pydantic>=2.12.5",
|
||||||
"pyyaml>=6.0",
|
"pyyaml>=6.0",
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ class TestCollectHelpers:
|
||||||
params = _collect_extra_params("out.png", config)
|
params = _collect_extra_params("out.png", config)
|
||||||
assert params["width"] == 512
|
assert params["width"] == 512
|
||||||
assert params["height"] == 256
|
assert params["height"] == 256
|
||||||
assert params["reference_images"] == ("ref.png",)
|
assert params["reference_images"] == ["ref.png"]
|
||||||
|
|
||||||
def test_collect_extra_params_empty(self, write_config: WriteConfig) -> None:
|
def test_collect_extra_params_empty(self, write_config: WriteConfig) -> None:
|
||||||
config = write_config({"targets": {"out.txt": {"prompt": "x"}}})
|
config = write_config({"targets": {"out.txt": {"prompt": "x"}}})
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ Mock-heavy tests produce many Any-typed expressions from MagicMock.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import io
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
|
@ -480,8 +481,10 @@ class TestOpenAIImageProvider:
|
||||||
)
|
)
|
||||||
|
|
||||||
call_args = mock_client.images.edit.call_args
|
call_args = mock_client.images.edit.call_args
|
||||||
# Single reference image should be passed as raw bytes
|
# gpt-image-* models pass a BytesIO with a name attribute
|
||||||
assert call_args.kwargs["image"] == b"reference data"
|
img_arg = call_args.kwargs["image"]
|
||||||
|
assert img_arg.read() == b"reference data"
|
||||||
|
assert hasattr(img_arg, "name")
|
||||||
|
|
||||||
output = project_dir / "out.png"
|
output = project_dir / "out.png"
|
||||||
assert output.exists()
|
assert output.exists()
|
||||||
|
|
@ -514,9 +517,9 @@ class TestOpenAIImageProvider:
|
||||||
)
|
)
|
||||||
|
|
||||||
call_args = mock_client.images.edit.call_args
|
call_args = mock_client.images.edit.call_args
|
||||||
# Multiple reference images should be passed as a list of bytes
|
# gpt-image-* models pass a list of BytesIO with name attributes
|
||||||
image_arg: list[bytes] = call_args.kwargs["image"]
|
image_arg: list[io.BytesIO] = call_args.kwargs["image"]
|
||||||
assert isinstance(image_arg, list)
|
assert isinstance(image_arg, list)
|
||||||
assert len(image_arg) == 2
|
assert len(image_arg) == 2
|
||||||
assert image_arg[0] == b"ref1 data"
|
assert image_arg[0].read() == b"ref1 data"
|
||||||
assert image_arg[1] == b"ref2 data"
|
assert image_arg[1].read() == b"ref2 data"
|
||||||
|
|
|
||||||
8
uv.lock
generated
8
uv.lock
generated
|
|
@ -184,7 +184,7 @@ requires-dist = [
|
||||||
{ name = "httpx", specifier = ">=0.27.0" },
|
{ name = "httpx", specifier = ">=0.27.0" },
|
||||||
{ name = "mistralai", specifier = ">=1.0.0" },
|
{ name = "mistralai", specifier = ">=1.0.0" },
|
||||||
{ name = "networkx", specifier = ">=3.6.1" },
|
{ name = "networkx", specifier = ">=3.6.1" },
|
||||||
{ name = "openai", specifier = ">=2.21.0" },
|
{ name = "openai", specifier = ">=2.25.0" },
|
||||||
{ name = "pillow", specifier = ">=11.0.0" },
|
{ name = "pillow", specifier = ">=11.0.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.12.5" },
|
{ name = "pydantic", specifier = ">=2.12.5" },
|
||||||
{ name = "pyyaml", specifier = ">=6.0" },
|
{ name = "pyyaml", specifier = ">=6.0" },
|
||||||
|
|
@ -386,7 +386,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "2.21.0"
|
version = "2.25.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio" },
|
{ name = "anyio" },
|
||||||
|
|
@ -398,9 +398,9 @@ dependencies = [
|
||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/92/e5/3d197a0947a166649f566706d7a4c8f7fe38f1fa7b24c9bcffe4c7591d44/openai-2.21.0.tar.gz", hash = "sha256:81b48ce4b8bbb2cc3af02047ceb19561f7b1dc0d4e52d1de7f02abfd15aa59b7", size = 644374, upload-time = "2026-02-14T00:12:01.577Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/43/2b/a442b206ed74908dd79e2ad1ef3ffaeae66422b1fb506af981f0ef671ba0/openai-2.25.0.tar.gz", hash = "sha256:c3e1965d83c333dbd341eb2c7c8aceb783c272cd57fc57353404d9a443634e29", size = 666577, upload-time = "2026-03-05T18:35:38.292Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/56/0a89092a453bb2c676d66abee44f863e742b2110d4dbb1dbcca3f7e5fc33/openai-2.21.0-py3-none-any.whl", hash = "sha256:0bc1c775e5b1536c294eded39ee08f8407656537ccc71b1004104fe1602e267c", size = 1103065, upload-time = "2026-02-14T00:11:59.603Z" },
|
{ url = "https://files.pythonhosted.org/packages/5b/e6/ca496976bacdd7f86d4fa69359c2d9b5a547d7acbf0ae5cac7bff107ff50/openai-2.25.0-py3-none-any.whl", hash = "sha256:7c7c01a0a4b69771a29913e28d06bc1e9cee8781b4d206bb56cb946d8d2fcb23", size = 1136347, upload-time = "2026-03-05T18:35:36.497Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue