From 8d910128cedf8b96560db2ff734c2a85babafb8e Mon Sep 17 00:00:00 2001 From: Yair Chen Date: Sun, 14 May 2023 07:05:11 +0300 Subject: [PATCH] Added support for ImageEdit Request --- OpenAI_API/EndpointBase.cs | 47 +++++++++---- OpenAI_API/Images/IImageEditEndpoint.cs | 17 +++++ OpenAI_API/Images/ImageEditEndpoint.cs | 36 ++++++++++ OpenAI_API/Images/ImageEditRequest.cs | 90 ++++++++++++++++++++++++ OpenAI_API/OpenAIAPI.cs | 10 ++- OpenAI_Tests/ImageEditEndpointTests.cs | 76 ++++++++++++++++++++ OpenAI_Tests/OpenAI_Tests.csproj | 7 ++ OpenAI_Tests/images/EditImage.png | Bin 0 -> 20506 bytes 8 files changed, 266 insertions(+), 17 deletions(-) create mode 100644 OpenAI_API/Images/IImageEditEndpoint.cs create mode 100644 OpenAI_API/Images/ImageEditEndpoint.cs create mode 100644 OpenAI_API/Images/ImageEditRequest.cs create mode 100644 OpenAI_Tests/ImageEditEndpointTests.cs create mode 100644 OpenAI_Tests/images/EditImage.png diff --git a/OpenAI_API/EndpointBase.cs b/OpenAI_API/EndpointBase.cs index 792727a..2a24045 100644 --- a/OpenAI_API/EndpointBase.cs +++ b/OpenAI_API/EndpointBase.cs @@ -1,10 +1,13 @@ using Newtonsoft.Json; +using OpenAI_API.Moderation; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; using System.Security.Authentication; using System.Text; using System.Threading.Tasks; @@ -119,21 +122,35 @@ private async Task HttpRequestRaw(string url = null, HttpMe if (postData != null) { - if (postData is HttpContent) - { - req.Content = postData as HttpContent; - } - else - { - string jsonContent = JsonConvert.SerializeObject(postData, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); - var stringContent = new StringContent(jsonContent, UnicodeEncoding.UTF8, "application/json"); - req.Content = stringContent; - } - } - response = await client.SendAsync(req, streaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); - - if (response.IsSuccessStatusCode) - { + if (postData is HttpContent) + { + req.Content = postData as HttpContent; + response = await client.SendAsync(req, streaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); + } + else if (postData is OpenAI_API.Images.ImageEditRequest) + { + var data = postData as Images.ImageEditRequest; + byte[] byes_array = File.ReadAllBytes(data.Image); + + MultipartFormDataContent formData = new MultipartFormDataContent + { + { new ByteArrayContent(byes_array, 0, byes_array.Length), "image", "image.png"}, + { new StringContent(data.Prompt), "prompt" }, + }; + + response = await client.PostAsync(url, formData); + } + else + { + string jsonContent = JsonConvert.SerializeObject(postData, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); + var stringContent = new StringContent(jsonContent, UnicodeEncoding.UTF8, "application/json"); + req.Content = stringContent; + response = await client.SendAsync(req, streaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); + } + } + + if (response.IsSuccessStatusCode) + { return response; } else diff --git a/OpenAI_API/Images/IImageEditEndpoint.cs b/OpenAI_API/Images/IImageEditEndpoint.cs new file mode 100644 index 0000000..db93c08 --- /dev/null +++ b/OpenAI_API/Images/IImageEditEndpoint.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace OpenAI_API.Images +{ + /// + /// An interface for . Given a prompt, the model will generate a new image. + /// + public interface IImageEditEndpoint + { + /// + /// Ask the API to Creates an image given a prompt. + /// + /// Request to be send + /// Asynchronously returns the image result. Look in its + Task EditImageAsync(ImageEditRequest request); + } +} \ No newline at end of file diff --git a/OpenAI_API/Images/ImageEditEndpoint.cs b/OpenAI_API/Images/ImageEditEndpoint.cs new file mode 100644 index 0000000..bdf28de --- /dev/null +++ b/OpenAI_API/Images/ImageEditEndpoint.cs @@ -0,0 +1,36 @@ +using OpenAI_API.Models; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace OpenAI_API.Images +{ + /// + /// Given a prompt, the model will generate a new image. + /// + public class ImageEditEndpoint : EndpointBase, IImageEditEndpoint + { + /// + /// The name of the endpoint, which is the final path segment in the API URL. For example, "image". + /// + protected override string Endpoint { get { return "images/edits"; } } + + /// + /// Constructor of the api endpoint. Rather than instantiating this yourself, access it through an instance of as . + /// + /// + internal ImageEditEndpoint(OpenAIAPI api) : base(api) { } + + + /// + /// Ask the API to Creates an image given a prompt. + /// + /// Request to be send + /// Asynchronously returns the image result. Look in its + public async Task EditImageAsync(ImageEditRequest request) + { + return await HttpPost(postData: request); + } + } +} diff --git a/OpenAI_API/Images/ImageEditRequest.cs b/OpenAI_API/Images/ImageEditRequest.cs new file mode 100644 index 0000000..7fc2edc --- /dev/null +++ b/OpenAI_API/Images/ImageEditRequest.cs @@ -0,0 +1,90 @@ +using Newtonsoft.Json; +using OpenAI_API.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OpenAI_API.Images +{ + /// + /// Represents a request to the Images API. Mostly matches the parameters in the OpenAI docs, although some have been renamed or expanded into single/multiple properties for ease of use. + /// + public class ImageEditRequest + { + /// + /// The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask is not provided, image must have transparency, which will be used as the mask. + /// + [JsonProperty("image")] + public string Image { get; set; } + + /// + /// An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where image should be edited. Must be a valid PNG file, less than 4MB, and have the same. + /// + [JsonProperty("mask")] + public string Mask { get; set; } + + /// + /// A text description of the desired image(s). The maximum length is 1000 characters. + /// + [JsonProperty("prompt")] + public string Prompt { get; set; } + + + [JsonProperty("n")] + public int? NumOfImages { get; set; } = 1; + + /// + /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. Defauls to 1024x1024 + /// + [JsonProperty("size"), JsonConverter(typeof(ImageSize.ImageSizeJsonConverter))] + public ImageSize Size { get; set; } + + /// + /// The format in which the generated images are returned. Must be one of url or b64_json. Defaults to Url. + /// + [JsonProperty("response_format"), JsonConverter(typeof(ImageResponseFormat.ImageResponseJsonConverter))] + public ImageResponseFormat ResponseFormat { get; set; } + + /// + /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. Optional. + /// + [JsonProperty("user")] + public string User { get; set; } + + + /// + /// Cretes a new, empty + /// + public ImageEditRequest() + { + + } + + /// + /// Creates a new with the specified parameters + /// + /// + /// A text description of the desired image(s). The maximum length is 1000 characters. + /// How many different choices to request for each prompt. Defaults to 1. + /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. + /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. + /// The format in which the generated images are returned. Must be one of url or b64_json. + public ImageEditRequest( + string image, + string prompt, + int? numOfImages = 1, + ImageSize size = null, + string user = null, + ImageResponseFormat responseFormat = null) + { + this.Image = image; + this.Prompt = prompt; + this.NumOfImages = numOfImages; + this.User = user; + this.Size = size ?? ImageSize._1024; + this.ResponseFormat = responseFormat ?? ImageResponseFormat.Url; + } + + } +} diff --git a/OpenAI_API/OpenAIAPI.cs b/OpenAI_API/OpenAIAPI.cs index f1e2bda..5d1db34 100644 --- a/OpenAI_API/OpenAIAPI.cs +++ b/OpenAI_API/OpenAIAPI.cs @@ -50,6 +50,7 @@ public OpenAIAPI(APIAuthentication apiKeys = null) Chat = new ChatEndpoint(this); Moderation = new ModerationEndpoint(this); ImageGenerations = new ImageGenerationEndpoint(this); + ImageEdit= new ImageEditEndpoint(this); } /// @@ -100,6 +101,11 @@ public static OpenAIAPI ForAzure(string YourResourceName, string deploymentId, A /// /// The API lets you do operations with images. Given a prompt and/or an input image, the model will generate a new image. /// - public IImageGenerationEndpoint ImageGenerations { get; } - } + public IImageGenerationEndpoint ImageGenerations { get; } + + /// + /// The API lets you do operations with images. Given a prompt and an input image, the model will edit a new image. + /// + public IImageEditEndpoint ImageEdit { get; } + } } diff --git a/OpenAI_Tests/ImageEditEndpointTests.cs b/OpenAI_Tests/ImageEditEndpointTests.cs new file mode 100644 index 0000000..67de5fb --- /dev/null +++ b/OpenAI_Tests/ImageEditEndpointTests.cs @@ -0,0 +1,76 @@ +using NUnit.Framework; +using OpenAI_API.Images; +using OpenAI_API.Moderation; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Text; + +namespace OpenAI_Tests +{ + public class ImageEditEndpointTests + { + [SetUp] + public void Setup() + { + OpenAI_API.APIAuthentication.Default = new OpenAI_API.APIAuthentication(Environment.GetEnvironmentVariable("TEST_OPENAI_SECRET_KEY")); + } + + private string GetImage(string path) + { + byte[] imageArray = System.IO.File.ReadAllBytes(path); + string base64ImageRepresentation = Convert.ToBase64String(imageArray); + return base64ImageRepresentation; + } + + [Test] + public void EditImage() + { + string imageFilepath = Path.Combine(AppContext.BaseDirectory, "images\\EditImage.png"); + string prompt = "add flowers, digital art"; + + Assert.That(File.Exists(imageFilepath)); + EditImage(imageFilepath, prompt); + } + + private void EditImage(string imageFilepath, string prompt) + { + var api = new OpenAI_API.OpenAIAPI(); + + Assert.IsNotNull(api.ImageEdit); + Assert.IsTrue(File.Exists(imageFilepath)); + var imageEditRequest = new ImageEditRequest(imageFilepath, prompt, 1, ImageSize._256); + + ImageResult results = null; + + try + { + results = api.ImageEdit.EditImageAsync(imageEditRequest).Result; + } + catch (Exception ex) + { + } + + Assert.IsNotNull(results); + if (results.CreatedUnixTime.HasValue) + { + Assert.NotZero(results.CreatedUnixTime.Value); + Assert.NotNull(results.Created); + Assert.Greater(results.Created.Value, new DateTime(2018, 1, 1)); + Assert.Less(results.Created.Value, DateTime.Now.AddDays(1)); + } + else + { + Assert.Null(results.Created); + } + + Assert.NotZero(results.Data.Count); + Assert.NotNull(results.Data.First().Url); + Assert.That(results.Data.First().Url.Length > 0); + Assert.That(results.Data.First().Url.StartsWith("https://")); + } + } +} diff --git a/OpenAI_Tests/OpenAI_Tests.csproj b/OpenAI_Tests/OpenAI_Tests.csproj index f26766e..af295d2 100644 --- a/OpenAI_Tests/OpenAI_Tests.csproj +++ b/OpenAI_Tests/OpenAI_Tests.csproj @@ -22,6 +22,13 @@ PreserveNewest + + Always + + + + + diff --git a/OpenAI_Tests/images/EditImage.png b/OpenAI_Tests/images/EditImage.png new file mode 100644 index 0000000000000000000000000000000000000000..eaac01efafb451e4daa7c5be4a0bce8d67dbb7e9 GIT binary patch literal 20506 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 WzWV9x^109N!hK^)BZR@@=l=yqxpr{? literal 0 HcmV?d00001