Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.NET Add Style and Quality Parameters Support with Execution Settings #4

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
Expand Down Expand Up @@ -94,9 +95,72 @@ public Task<string> GenerateImageAsync(string description, int width, int height
}

/// <inheritdoc/>
public Task<IReadOnlyList<ImageContent>> GetImageContentsAsync(TextContent input, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default)
public async Task<IReadOnlyList<ImageContent>> GetImageContentsAsync(
TextContent input,
PromptExecutionSettings? executionSettings = null,
Kernel? kernel = null,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
// Ensure the input is valid
Verify.NotNull(input);

// Convert the generic execution settings to OpenAI-specific settings
var imageSettings = OpenAITextToImageExecutionSettings.FromExecutionSettings(executionSettings);

// Determine the size of the image based on the width and height settings
var size = (imageSettings.Width, imageSettings.Height) switch
{
(256, 256) => "256x256",
(512, 512) => "512x512",
(1024, 1024) => "1024x1024",
(1792, 1024) => "1792x1024",
(1024, 1792) => "1024x1792",
_ => throw new NotSupportedException($"The provided size is not supported: {imageSettings.Width}x{imageSettings.Height}")
};

// Validate quality and style
var supportedQualities = new[] { "standard", "hd" };
var supportedStyles = new[] { "vivid", "natural" };

if (!string.IsNullOrEmpty(imageSettings.Quality) && !supportedQualities.Contains(imageSettings.Quality))
{
throw new NotSupportedException($"The provided quality '{imageSettings.Quality}' is not supported.");
}

if (!string.IsNullOrEmpty(imageSettings.Style) && !supportedStyles.Contains(imageSettings.Style))
{
throw new NotSupportedException($"The provided style '{imageSettings.Style}' is not supported.");
}

// Create the request body for the image generation
var requestBody = JsonSerializer.Serialize(new TextToImageRequest
{
Model = imageSettings.ModelId ?? this._modelId,
Prompt = input.Text ?? string.Empty,
Size = size,
Count = imageSettings.ImageCount ?? 1,
Quality = imageSettings.Quality ?? "standard",
Style = imageSettings.Style ?? "vivid"
});

// Execute the request using the core client and return Image objects
var imageStrings = await this._core.ExecuteImageGenerationRequestAsync(OpenAIEndpoint, requestBody, x => x.Url ?? x.AsBase64, cancellationToken).ConfigureAwait(false);

// Convert the strings to ImageContent objects
var images = new List<ImageContent>();
foreach (var imageString in imageStrings)
{
if (Uri.TryCreate(imageString, UriKind.Absolute, out var uriResult) && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps))
{
images.Add(new ImageContent(uriResult));
}
else
{
images.Add(new ImageContent($"data:;base64,{imageString}"));
}
}

return images.AsReadOnly();
}

private async Task<string> GenerateImageAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,16 @@ internal sealed class TextToImageRequest
/// </summary>
[JsonPropertyName("response_format")]
public string Format { get; set; } = "url";

/// <summary>
/// Image quality, "standard" or "hd"
/// </summary>
[JsonPropertyName("quality")]
public string Quality { get; set; } = "standard";

/// <summary>
/// Image style, "vivid" or "natural"
/// </summary>
[JsonPropertyName("style")]
public string Style { get; set; } = "vivid";
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Moq;
using Xunit;
Expand Down Expand Up @@ -81,6 +83,122 @@ public async Task GenerateImageWorksCorrectlyAsync(int width, int height, bool e
}
}

[Fact]
public async Task GetImageContentsAsyncWithValidInputReturnsImageContentsAsync()
{
// Arrange
var service = new OpenAITextToImageService("api-key", "organization", "dall-e-3", this._httpClient);
Assert.Equal("dall-e-3", service.Attributes["ModelId"]);

this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent("""
{
"created": 1702575371,
"data": [
{
"url": "https://image-url"
}
]
}
""", Encoding.UTF8, "application/json")
};

var input = new TextContent("A cute baby sea otter");
var executionSettings = new OpenAITextToImageExecutionSettings
{
Width = 1024,
Height = 1024,
Quality = "hd",
Style = "natural",
ImageCount = 1
};

// Act
var result = await service.GetImageContentsAsync(input, executionSettings);

// Assert
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal(new Uri("https://image-url"), result[0].Uri);
}

[Theory]
[InlineData(1024, 1024, "hd", "natural", 1, "https://image-url", false)]
[InlineData(123, 456, "hd", "natural", 1, "", true)]
[InlineData(1024, 1024, "hd", "natural", 2, "https://image-url1|https://image-url2", false)]
[InlineData(1024, 1024, "ultra", "natural", 1, "", true)]
[InlineData(1024, 1024, "hd", "artistic", 1, "", true)]
public async Task GetImageContentsReturnsExpectedResultsAsync(
int width,
int height,
string quality,
string style,
int imageCount,
string expectedUrls,
bool expectException)
{
// Arrange
var service = new OpenAITextToImageService("api-key", "organization", "dall-e-3", this._httpClient);

if (!expectException)
{
var urls = expectedUrls.Split('|').Select(url =>
{
return url.StartsWith("http", StringComparison.OrdinalIgnoreCase) ?
$"{{ \"url\": \"{url}\" }}" :
$"{{ \"b64_json\": \"{url}\" }}";
});
var jsonResponse = $"{{ \"created\": 1702575371, \"data\": [ {string.Join(",", urls)} ] }}";

this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(jsonResponse, Encoding.UTF8, "application/json")
};
}

var input = new TextContent("A picturesque landscape");
var executionSettings = new OpenAITextToImageExecutionSettings
{
Width = width,
Height = height,
Quality = quality,
Style = style,
ImageCount = imageCount
};

// Act & Assert
if (expectException)
{
await Assert.ThrowsAsync<NotSupportedException>(async () =>
{
await service.GetImageContentsAsync(input, executionSettings);
});
}
else
{
var result = await service.GetImageContentsAsync(input, executionSettings);

Assert.NotNull(result);
Assert.Equal(imageCount, result.Count);

var expectedUrlList = expectedUrls.Split('|').ToList();
for (int i = 0; i < result.Count; i++)
{
if (Uri.TryCreate(expectedUrlList[i], UriKind.Absolute, out var uriResult) &&
(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps))
{
Assert.Equal(uriResult, result[i].Uri);
}
else
{
Assert.StartsWith("data:;base64,", result[i].DataUri);
Assert.Contains(expectedUrlList[i], result[i].DataUri);
}
}
}
}

public void Dispose()
{
this._httpClient.Dispose();
Expand Down
Loading