mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
feat: multiple images in openai images/edits endpoint
This commit is contained in:
parent
3e764ec268
commit
2fa8f971e0
5 changed files with 460 additions and 60 deletions
|
|
@ -4,7 +4,7 @@ import TabItem from '@theme/TabItem';
|
|||
|
||||
# /images/edits
|
||||
|
||||
LiteLLM provides image editing functionality that maps to OpenAI's `/images/edits` API endpoint.
|
||||
LiteLLM provides image editing functionality that maps to OpenAI's `/images/edits` API endpoint. Now supports both single and multiple image editing.
|
||||
|
||||
| Feature | Supported | Notes |
|
||||
|---------|-----------|--------|
|
||||
|
|
@ -13,7 +13,7 @@ LiteLLM provides image editing functionality that maps to OpenAI's `/images/edit
|
|||
| End-user Tracking | ✅ | |
|
||||
| Fallbacks | ✅ | Works between supported models |
|
||||
| Loadbalancing | ✅ | Works between supported models |
|
||||
| Supported operations | Create image edits | |
|
||||
| Supported operations | Create image edits | Single and multiple images supported |
|
||||
| Supported LiteLLM SDK Versions | 1.63.8+ | |
|
||||
| Supported LiteLLM Proxy Versions | 1.71.1+ | |
|
||||
| Supported LLM providers | **OpenAI** | Currently only `openai` is supported |
|
||||
|
|
@ -41,6 +41,26 @@ response = litellm.image_edit(
|
|||
print(response)
|
||||
```
|
||||
|
||||
#### Multiple Images Edit
|
||||
```python showLineNumbers title="OpenAI Multiple Images Edit"
|
||||
import litellm
|
||||
|
||||
# Edit multiple images with a prompt
|
||||
response = litellm.image_edit(
|
||||
model="gpt-image-1",
|
||||
image=[
|
||||
open("image1.png", "rb"),
|
||||
open("image2.png", "rb"),
|
||||
open("image3.png", "rb")
|
||||
],
|
||||
prompt="Apply vintage filter to all images",
|
||||
n=1,
|
||||
size="1024x1024"
|
||||
)
|
||||
|
||||
print(response)
|
||||
```
|
||||
|
||||
#### Image Edit with Mask
|
||||
```python showLineNumbers title="OpenAI Image Edit with Mask"
|
||||
import litellm
|
||||
|
|
@ -80,6 +100,30 @@ response = asyncio.run(edit_image())
|
|||
print(response)
|
||||
```
|
||||
|
||||
#### Async Multiple Images Edit
|
||||
```python showLineNumbers title="Async OpenAI Multiple Images Edit"
|
||||
import litellm
|
||||
import asyncio
|
||||
|
||||
async def edit_multiple_images():
|
||||
response = await litellm.aimage_edit(
|
||||
model="gpt-image-1",
|
||||
image=[
|
||||
open("portrait1.png", "rb"),
|
||||
open("portrait2.png", "rb")
|
||||
],
|
||||
prompt="Add professional lighting to the portraits",
|
||||
n=1,
|
||||
size="1024x1024",
|
||||
response_format="url"
|
||||
)
|
||||
return response
|
||||
|
||||
# Run the async function
|
||||
response = asyncio.run(edit_multiple_images())
|
||||
print(response)
|
||||
```
|
||||
|
||||
#### Image Edit with Custom Parameters
|
||||
```python showLineNumbers title="OpenAI Image Edit with Custom Parameters"
|
||||
import litellm
|
||||
|
|
@ -163,6 +207,20 @@ curl -X POST "http://localhost:4000/v1/images/edits" \
|
|||
-F "response_format=url"
|
||||
```
|
||||
|
||||
#### cURL Multiple Images Example
|
||||
```bash showLineNumbers title="cURL Multiple Images Edit Request"
|
||||
curl -X POST "http://localhost:4000/v1/images/edits" \
|
||||
-H "Authorization: Bearer your-api-key" \
|
||||
-F "model=gpt-image-1" \
|
||||
-F "image=@image1.png" \
|
||||
-F "image=@image2.png" \
|
||||
-F "image=@image3.png" \
|
||||
-F "prompt=Apply artistic filter to all images" \
|
||||
-F "n=1" \
|
||||
-F "size=1024x1024" \
|
||||
-F "response_format=url"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import asyncio
|
||||
import contextvars
|
||||
from functools import partial
|
||||
from typing import Any, Coroutine, Dict, Literal, Optional, Union, cast, overload
|
||||
from typing import Any, Coroutine, Dict, Literal, Optional, Union, cast, overload, List
|
||||
|
||||
import httpx
|
||||
|
||||
|
|
@ -675,7 +675,7 @@ def image_variation(
|
|||
|
||||
@client
|
||||
def image_edit(
|
||||
image: FileTypes,
|
||||
image: Union[FileTypes, List[FileTypes]],
|
||||
prompt: str,
|
||||
model: Optional[str] = None,
|
||||
mask: Optional[str] = None,
|
||||
|
|
@ -703,6 +703,9 @@ def image_edit(
|
|||
litellm_call_id: Optional[str] = kwargs.get("litellm_call_id", None)
|
||||
_is_async = kwargs.pop("async_call", False) is True
|
||||
|
||||
#add images / or return a single image
|
||||
images = image if isinstance(image, list) else [image]
|
||||
|
||||
# get llm provider logic
|
||||
litellm_params = GenericLiteLLMParams(**kwargs)
|
||||
model, custom_llm_provider, _, _ = get_llm_provider(
|
||||
|
|
@ -751,7 +754,7 @@ def image_edit(
|
|||
# Call the handler with _is_async flag instead of directly calling the async handler
|
||||
return base_llm_http_handler.image_edit_handler(
|
||||
model=model,
|
||||
image=image,
|
||||
image=images,
|
||||
prompt=prompt,
|
||||
image_edit_provider_config=image_edit_provider_config,
|
||||
image_edit_optional_request_params=image_edit_request_params,
|
||||
|
|
@ -777,7 +780,7 @@ def image_edit(
|
|||
|
||||
@client
|
||||
async def aimage_edit(
|
||||
image: FileTypes,
|
||||
image: Union[FileTypes, List[FileTypes]],
|
||||
model: str,
|
||||
prompt: str,
|
||||
mask: Optional[str] = None,
|
||||
|
|
@ -817,9 +820,11 @@ async def aimage_edit(
|
|||
model=model, api_base=local_vars.get("base_url", None)
|
||||
)
|
||||
|
||||
images = image if isinstance(image, list) else [image]
|
||||
|
||||
func = partial(
|
||||
image_edit,
|
||||
image=image,
|
||||
image=images,
|
||||
prompt=prompt,
|
||||
mask=mask,
|
||||
model=model,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ from litellm.utils import ImageResponse
|
|||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.types.utils import StandardLoggingPayload
|
||||
|
||||
# Configure pytest marks to avoid warnings
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
class TestCustomLogger(CustomLogger):
|
||||
def __init__(self):
|
||||
self.standard_logging_payload: Optional[StandardLoggingPayload] = None
|
||||
|
|
@ -35,6 +38,8 @@ TEST_IMAGES = [
|
|||
open(os.path.join(pwd, "litellm_site.png"), "rb"),
|
||||
]
|
||||
|
||||
SINGLE_TEST_IMAGE = open(os.path.join(pwd, "ishaan_github.png"), "rb")
|
||||
|
||||
def get_test_images_as_bytesio():
|
||||
"""Helper function to get test images as BytesIO objects"""
|
||||
bytesio_images = []
|
||||
|
|
@ -501,3 +506,278 @@ def test_recraft_image_edit_config():
|
|||
assert files[0][0] == "image" # Field name (not image[] like OpenAI)
|
||||
assert files[0][1][1] == mock_image # Image data
|
||||
assert files[0][1][2] == "image/png" # Content type
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sync_mode", [True, False])
|
||||
@pytest.mark.flaky(retries=3, delay=2)
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_vs_single_image_edit(sync_mode):
|
||||
"""Test that both single and multiple image editing work correctly"""
|
||||
from litellm import image_edit, aimage_edit
|
||||
litellm._turn_on_debug()
|
||||
|
||||
try:
|
||||
prompt = "Add a soft blue tint to the image(s)"
|
||||
|
||||
# Test single image
|
||||
if sync_mode:
|
||||
single_result = image_edit(
|
||||
prompt=prompt,
|
||||
model="gpt-image-1",
|
||||
image=SINGLE_TEST_IMAGE,
|
||||
)
|
||||
else:
|
||||
single_result = await aimage_edit(
|
||||
prompt=prompt,
|
||||
model="gpt-image-1",
|
||||
image=SINGLE_TEST_IMAGE,
|
||||
)
|
||||
|
||||
print("Single image result:", single_result)
|
||||
ImageResponse.model_validate(single_result)
|
||||
|
||||
# Test multiple images
|
||||
if sync_mode:
|
||||
multiple_result = image_edit(
|
||||
prompt=prompt,
|
||||
model="gpt-image-1",
|
||||
image=TEST_IMAGES,
|
||||
)
|
||||
else:
|
||||
multiple_result = await aimage_edit(
|
||||
prompt=prompt,
|
||||
model="gpt-image-1",
|
||||
image=TEST_IMAGES,
|
||||
)
|
||||
|
||||
print("Multiple images result:", multiple_result)
|
||||
ImageResponse.model_validate(multiple_result)
|
||||
|
||||
# Both should return valid responses
|
||||
assert single_result is not None
|
||||
assert multiple_result is not None
|
||||
assert single_result.data is not None
|
||||
assert multiple_result.data is not None
|
||||
assert len(single_result.data) > 0
|
||||
assert len(multiple_result.data) > 0
|
||||
|
||||
except litellm.ContentPolicyViolationError as e:
|
||||
pytest.skip(f"Content policy violation: {e}")
|
||||
|
||||
|
||||
@pytest.mark.flaky(retries=3, delay=2)
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_image_edit_with_different_formats():
|
||||
"""Test multiple images editing with different file formats and types"""
|
||||
from litellm import aimage_edit
|
||||
litellm._turn_on_debug()
|
||||
|
||||
try:
|
||||
prompt = "Create a cohesive artistic style across all images"
|
||||
|
||||
# Test with mixed BytesIO and file objects
|
||||
mixed_images = [
|
||||
SINGLE_TEST_IMAGE, # File object
|
||||
get_test_images_as_bytesio()[1] # BytesIO object
|
||||
]
|
||||
|
||||
result = await aimage_edit(
|
||||
prompt=prompt,
|
||||
model="gpt-image-1",
|
||||
image=mixed_images,
|
||||
)
|
||||
|
||||
print("Mixed format images result:", result)
|
||||
ImageResponse.model_validate(result)
|
||||
|
||||
assert result is not None
|
||||
assert result.data is not None
|
||||
assert len(result.data) > 0
|
||||
|
||||
# Save result if available
|
||||
if result.data and result.data[0].b64_json:
|
||||
image_bytes = base64.b64decode(result.data[0].b64_json)
|
||||
with open("test_multiple_image_edit_mixed.png", "wb") as f:
|
||||
f.write(image_bytes)
|
||||
|
||||
except litellm.ContentPolicyViolationError as e:
|
||||
pytest.skip(f"Content policy violation: {e}")
|
||||
|
||||
|
||||
@pytest.mark.flaky(retries=3, delay=2)
|
||||
@pytest.mark.asyncio
|
||||
async def test_image_edit_array_handling():
|
||||
"""Test that the image parameter correctly handles both single items and arrays"""
|
||||
from litellm import aimage_edit
|
||||
|
||||
# Mock response
|
||||
mock_response = {
|
||||
"created": 1589478378,
|
||||
"data": [
|
||||
{
|
||||
"b64_json": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
class MockResponse:
|
||||
def __init__(self, json_data, status_code):
|
||||
self._json_data = json_data
|
||||
self.status_code = status_code
|
||||
self.text = json.dumps(json_data)
|
||||
|
||||
def json(self):
|
||||
return self._json_data
|
||||
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_post:
|
||||
mock_post.return_value = MockResponse(mock_response, 200)
|
||||
|
||||
prompt = "Test prompt"
|
||||
|
||||
# Test 1: Single image (should be converted to list internally)
|
||||
result1 = await aimage_edit(
|
||||
prompt=prompt,
|
||||
model="gpt-image-1",
|
||||
image=SINGLE_TEST_IMAGE,
|
||||
)
|
||||
|
||||
# Test 2: Multiple images (already a list)
|
||||
result2 = await aimage_edit(
|
||||
prompt=prompt,
|
||||
model="gpt-image-1",
|
||||
image=TEST_IMAGES,
|
||||
)
|
||||
|
||||
# Test 3: Empty list (should fail validation)
|
||||
with pytest.raises(Exception):
|
||||
await aimage_edit(
|
||||
prompt=prompt,
|
||||
model="gpt-image-1",
|
||||
image=[],
|
||||
)
|
||||
|
||||
# Both valid calls should succeed
|
||||
ImageResponse.model_validate(result1)
|
||||
ImageResponse.model_validate(result2)
|
||||
|
||||
# Verify that both calls were made to the API
|
||||
assert mock_post.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_openai_transformation_handles_multiple_images():
|
||||
"""Test that OpenAI transformation correctly handles multiple images in request"""
|
||||
from litellm.llms.openai.image_edit.transformation import OpenAIImageEditConfig
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
config = OpenAIImageEditConfig()
|
||||
|
||||
# Test with multiple images
|
||||
prompt = "Edit these images"
|
||||
images = [b"fake_image_1", b"fake_image_2", b"fake_image_3"]
|
||||
litellm_params = GenericLiteLLMParams(api_key="test_key")
|
||||
|
||||
data, files = config.transform_image_edit_request(
|
||||
model="gpt-image-1",
|
||||
prompt=prompt,
|
||||
image=images,
|
||||
image_edit_optional_request_params={"n": 1},
|
||||
litellm_params=litellm_params,
|
||||
headers={}
|
||||
)
|
||||
|
||||
# Check that data contains the prompt and parameters
|
||||
assert data["prompt"] == prompt
|
||||
assert data["model"] == "gpt-image-1"
|
||||
assert data["n"] == 1
|
||||
|
||||
# Check that files contains all images with correct field names
|
||||
assert len(files) == len(images)
|
||||
for i, file_entry in enumerate(files):
|
||||
assert file_entry[0] == "image[]" # OpenAI uses image[] for multiple files
|
||||
assert file_entry[1][1] == images[i] # Image data
|
||||
assert file_entry[1][2] == "image/png" # Content type
|
||||
|
||||
print(f"Successfully processed {len(images)} images in transformation")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_image_edit_parameter_validation():
|
||||
"""Test parameter validation with multiple images"""
|
||||
from litellm import aimage_edit
|
||||
|
||||
# Mock response
|
||||
mock_response = {
|
||||
"created": 1589478378,
|
||||
"data": [
|
||||
{
|
||||
"b64_json": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
class MockResponse:
|
||||
def __init__(self, json_data, status_code):
|
||||
self._json_data = json_data
|
||||
self.status_code = status_code
|
||||
self.text = json.dumps(json_data)
|
||||
|
||||
def json(self):
|
||||
return self._json_data
|
||||
|
||||
with patch(
|
||||
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_post:
|
||||
mock_post.return_value = MockResponse(mock_response, 200)
|
||||
|
||||
# Test with valid parameters
|
||||
result = await aimage_edit(
|
||||
prompt="Test prompt",
|
||||
model="gpt-image-1",
|
||||
image=TEST_IMAGES,
|
||||
n=1,
|
||||
size="1024x1024",
|
||||
response_format="b64_json"
|
||||
)
|
||||
|
||||
ImageResponse.model_validate(result)
|
||||
|
||||
# Verify the request was made with correct parameters
|
||||
mock_post.assert_called_once()
|
||||
call_args = mock_post.call_args
|
||||
|
||||
# Check that the request contains the expected data
|
||||
if 'data' in call_args.kwargs:
|
||||
form_data = call_args.kwargs['data']
|
||||
assert 'model' in form_data
|
||||
assert 'prompt' in form_data
|
||||
assert 'n' in form_data
|
||||
assert form_data['n'] == 1 # Could be int or string depending on implementation print("Parameter validation passed for multiple image edit")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_image_edit_error_handling():
|
||||
"""Test error handling with multiple images"""
|
||||
from litellm import aimage_edit
|
||||
|
||||
# Test with None image (should raise error)
|
||||
with pytest.raises(Exception):
|
||||
await aimage_edit(
|
||||
prompt="Test prompt",
|
||||
model="gpt-image-1",
|
||||
image=None,
|
||||
)
|
||||
|
||||
# Test with invalid model (should raise error)
|
||||
with pytest.raises(Exception):
|
||||
await aimage_edit(
|
||||
prompt="Test prompt",
|
||||
model="invalid-model",
|
||||
image=TEST_IMAGES,
|
||||
)
|
||||
|
||||
print("Error handling tests passed for multiple image edit")
|
||||
|
|
|
|||
|
|
@ -170,8 +170,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
const saved = sessionStorage.getItem('useApiSessionManagement');
|
||||
return saved ? JSON.parse(saved) : true; // Default to API session management
|
||||
});
|
||||
const [uploadedImage, setUploadedImage] = useState<File | null>(null);
|
||||
const [imagePreviewUrl, setImagePreviewUrl] = useState<string | null>(null);
|
||||
const [uploadedImages, setUploadedImages] = useState<File[]>([]);
|
||||
const [imagePreviewUrls, setImagePreviewUrls] = useState<string[]>([]);
|
||||
const [responsesUploadedImage, setResponsesUploadedImage] = useState<File | null>(null);
|
||||
const [responsesImagePreviewUrl, setResponsesImagePreviewUrl] = useState<string | null>(null);
|
||||
const [chatUploadedImage, setChatUploadedImage] = useState<File | null>(null);
|
||||
|
|
@ -468,18 +468,26 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
};
|
||||
|
||||
const handleImageUpload = (file: File) => {
|
||||
setUploadedImage(file);
|
||||
setUploadedImages(prev => [...prev, file]);
|
||||
const previewUrl = URL.createObjectURL(file);
|
||||
setImagePreviewUrl(previewUrl);
|
||||
setImagePreviewUrls(prev => [...prev, previewUrl]);
|
||||
return false; // Prevent default upload behavior
|
||||
};
|
||||
|
||||
const handleRemoveImage = () => {
|
||||
if (imagePreviewUrl) {
|
||||
URL.revokeObjectURL(imagePreviewUrl);
|
||||
const handleRemoveImage = (index: number) => {
|
||||
if (imagePreviewUrls[index]) {
|
||||
URL.revokeObjectURL(imagePreviewUrls[index]);
|
||||
}
|
||||
setUploadedImage(null);
|
||||
setImagePreviewUrl(null);
|
||||
setUploadedImages(prev => prev.filter((_, i) => i !== index));
|
||||
setImagePreviewUrls(prev => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleRemoveAllImages = () => {
|
||||
imagePreviewUrls.forEach(url => {
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
setUploadedImages([]);
|
||||
setImagePreviewUrls([]);
|
||||
};
|
||||
|
||||
const handleResponsesImageUpload = (file: File): false => {
|
||||
|
|
@ -516,8 +524,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
if (inputMessage.trim() === "") return;
|
||||
|
||||
// For image edits, require both image and prompt
|
||||
if (endpointType === EndpointType.IMAGE_EDITS && !uploadedImage) {
|
||||
NotificationsManager.fromBackend("Please upload an image for editing");
|
||||
if (endpointType === EndpointType.IMAGE_EDITS && uploadedImages.length === 0) {
|
||||
NotificationsManager.fromBackend("Please upload at least one image for editing");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -617,9 +625,9 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
);
|
||||
} else if (endpointType === EndpointType.IMAGE_EDITS) {
|
||||
// For image edits
|
||||
if (uploadedImage) {
|
||||
if (uploadedImages.length > 0) {
|
||||
await makeOpenAIImageEditsRequest(
|
||||
uploadedImage,
|
||||
uploadedImages.length === 1 ? uploadedImages[0] : uploadedImages,
|
||||
inputMessage,
|
||||
(imageUrl, model) => updateImageUI(imageUrl, model),
|
||||
selectedModel,
|
||||
|
|
@ -689,7 +697,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
abortControllerRef.current = null;
|
||||
// Clear image after successful request for image edits
|
||||
if (endpointType === EndpointType.IMAGE_EDITS) {
|
||||
handleRemoveImage();
|
||||
handleRemoveAllImages();
|
||||
}
|
||||
// Clear image after successful request for responses API
|
||||
if (endpointType === EndpointType.RESPONSES && responsesUploadedImage) {
|
||||
|
|
@ -708,7 +716,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
setChatHistory([]);
|
||||
setMessageTraceId(null);
|
||||
setResponsesSessionId(null); // Clear responses session ID
|
||||
handleRemoveImage(); // Clear any uploaded images for image edits
|
||||
handleRemoveAllImages(); // Clear any uploaded images for image edits
|
||||
handleRemoveResponsesImage(); // Clear any uploaded images for responses
|
||||
handleRemoveChatImage(); // Clear any uploaded images for chat completions
|
||||
sessionStorage.removeItem('chatHistory');
|
||||
|
|
@ -1075,7 +1083,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
{/* Image Upload Section for Image Edits */}
|
||||
{endpointType === EndpointType.IMAGE_EDITS && (
|
||||
<div className="mb-4">
|
||||
{!uploadedImage ? (
|
||||
{uploadedImages.length === 0 ? (
|
||||
<Dragger
|
||||
beforeUpload={handleImageUpload}
|
||||
accept="image/*"
|
||||
|
|
@ -1085,24 +1093,47 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
<p className="ant-upload-drag-icon">
|
||||
<PictureOutlined style={{ fontSize: '24px', color: '#666' }} />
|
||||
</p>
|
||||
<p className="ant-upload-text text-sm">Click or drag image to upload</p>
|
||||
<p className="ant-upload-text text-sm">Click or drag images to upload</p>
|
||||
<p className="ant-upload-hint text-xs text-gray-500">
|
||||
Support for PNG, JPG, JPEG formats
|
||||
Support for PNG, JPG, JPEG formats. Multiple images supported.
|
||||
</p>
|
||||
</Dragger>
|
||||
) : (
|
||||
<div className="relative inline-block">
|
||||
<img
|
||||
src={imagePreviewUrl || ''}
|
||||
alt="Upload preview"
|
||||
className="max-w-32 max-h-32 rounded-md border border-gray-200 object-cover"
|
||||
/>
|
||||
<button
|
||||
className="absolute top-1 right-1 bg-white shadow-sm border border-gray-200 rounded px-1 py-1 text-red-500 hover:bg-red-50 text-xs"
|
||||
onClick={handleRemoveImage}
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</button>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{uploadedImages.map((file, index) => (
|
||||
<div key={index} className="relative inline-block">
|
||||
<img
|
||||
src={imagePreviewUrls[index] || ''}
|
||||
alt={`Upload preview ${index + 1}`}
|
||||
className="max-w-32 max-h-32 rounded-md border border-gray-200 object-cover"
|
||||
/>
|
||||
<button
|
||||
className="absolute top-1 right-1 bg-white shadow-sm border border-gray-200 rounded px-1 py-1 text-red-500 hover:bg-red-50 text-xs"
|
||||
onClick={() => handleRemoveImage(index)}
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{/* Add more images button */}
|
||||
<div className="flex items-center justify-center w-32 h-32 border-2 border-dashed border-gray-300 rounded-md hover:border-gray-400 cursor-pointer"
|
||||
onClick={() => document.getElementById('additional-image-upload')?.click()}>
|
||||
<div className="text-center">
|
||||
<PictureOutlined style={{ fontSize: '24px', color: '#666' }} />
|
||||
<p className="text-xs text-gray-500 mt-1">Add more</p>
|
||||
</div>
|
||||
<input
|
||||
id="additional-image-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
files.forEach(file => handleImageUpload(file));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { getProxyBaseUrl } from "@/components/networking";
|
|||
import NotificationManager from "@/components/molecules/notifications_manager";
|
||||
|
||||
export async function makeOpenAIImageEditsRequest(
|
||||
imageFile: File,
|
||||
imageFiles: File | File[],
|
||||
prompt: string,
|
||||
updateUI: (imageUrl: string, model: string) => void,
|
||||
selectedModel: string,
|
||||
|
|
@ -28,34 +28,60 @@ export async function makeOpenAIImageEditsRequest(
|
|||
});
|
||||
|
||||
try {
|
||||
const response = await client.images.edit({
|
||||
model: selectedModel,
|
||||
image: imageFile,
|
||||
prompt: prompt,
|
||||
}, { signal });
|
||||
|
||||
console.log(response.data);
|
||||
// handle single and multiple images
|
||||
const imagesToProcess = Array.isArray(imageFiles) ? imageFiles : [imageFiles];
|
||||
|
||||
if (response.data && response.data[0]) {
|
||||
// Handle either URL or base64 data from response
|
||||
if (response.data[0].url) {
|
||||
// Use the URL directly
|
||||
updateUI(response.data[0].url, selectedModel);
|
||||
} else if (response.data[0].b64_json) {
|
||||
// Convert base64 to data URL format
|
||||
const base64Data = response.data[0].b64_json;
|
||||
updateUI(`data:image/png;base64,${base64Data}`, selectedModel);
|
||||
} else {
|
||||
throw new Error("No image data found in response");
|
||||
// For multiple images, we'll make separate API calls for each image
|
||||
// since OpenAI's edit endpoint processes one image at a time
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < imagesToProcess.length; i++) {
|
||||
const image = imagesToProcess[i];
|
||||
console.log(`Processing image ${i + 1} of ${imagesToProcess.length}`);
|
||||
|
||||
const response = await client.images.edit({
|
||||
model: selectedModel,
|
||||
image: image,
|
||||
prompt: prompt,
|
||||
}, { signal });
|
||||
|
||||
console.log(`Response for image ${i + 1}:`, response.data);
|
||||
|
||||
if (response.data && response.data[0]) {
|
||||
// Handle either URL or base64 data from response
|
||||
if (response.data[0].url) {
|
||||
// Use the URL directly
|
||||
updateUI(response.data[0].url, selectedModel);
|
||||
results.push(response.data[0].url);
|
||||
} else if (response.data[0].b64_json) {
|
||||
// Convert base64 to data URL format
|
||||
const base64Data = response.data[0].b64_json;
|
||||
const dataUrl = `data:image/png;base64,${base64Data}`;
|
||||
updateUI(dataUrl, selectedModel);
|
||||
results.push(dataUrl);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid response format");
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
if (results.length > 1) {
|
||||
NotificationManager.success(`Successfully processed ${results.length} images`);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error making image edit request:", error);
|
||||
|
||||
if (signal?.aborted) {
|
||||
console.log("Image edits request was cancelled");
|
||||
} else {
|
||||
NotificationManager.fromBackend(`Error occurred while editing image. Please try again. Error: ${error}`);
|
||||
let errorMessage = "Failed to edit image(s)";
|
||||
|
||||
if (error?.error?.message) {
|
||||
errorMessage = error.error.message;
|
||||
} else if (error?.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
NotificationManager.fromBackend(`Image edit failed: ${errorMessage}`);
|
||||
}
|
||||
throw error; // Re-throw to allow the caller to handle the error
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue