Implement portrait generation and retrieval for people in the school system. Added new API endpoints for generating and fetching portraits, including support for avatar and full-body images. Updated the client-side to handle portrait states and display options. Enhanced the person card UI to include tabs for overview and portrait, with appropriate localization strings. Updated solution files to include new test projects. Adjusted server configuration for SwarmUI integration.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HSchool.Server.Game;
|
||||
|
||||
internal sealed class SwarmUiClient
|
||||
{
|
||||
private static readonly JsonSerializerOptions Json = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
private readonly HttpClient _http;
|
||||
private readonly SwarmUiOptions _options;
|
||||
private readonly ILogger<SwarmUiClient> _logger;
|
||||
private string? _sessionId;
|
||||
|
||||
public SwarmUiClient(HttpClient http, IOptions<SwarmUiOptions> options, ILogger<SwarmUiClient> logger)
|
||||
{
|
||||
_http = http;
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool IsConfigured => !string.IsNullOrWhiteSpace(_options.BaseUrl);
|
||||
|
||||
public async Task<byte[]> GenerateAsync(
|
||||
string prompt,
|
||||
string negativePrompt,
|
||||
SwarmUiSettings settings,
|
||||
PortraitKind kind,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!IsConfigured)
|
||||
{
|
||||
throw new InvalidOperationException("SwarmUI is not configured.");
|
||||
}
|
||||
|
||||
return await RunWithSessionAsync(async () =>
|
||||
{
|
||||
var preset = kind == PortraitKind.Avatar ? settings.Avatar : settings.FullBody;
|
||||
var body = new Dictionary<string, object?>
|
||||
{
|
||||
["session_id"] = _sessionId,
|
||||
["images"] = 1,
|
||||
["donotsave"] = true,
|
||||
["prompt"] = prompt,
|
||||
["negativeprompt"] = negativePrompt,
|
||||
["model"] = settings.Model,
|
||||
["steps"] = settings.Steps,
|
||||
["cfgscale"] = settings.CfgScale,
|
||||
["width"] = preset.Width,
|
||||
["height"] = preset.Height,
|
||||
["seed"] = settings.Seed,
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(settings.Sampler))
|
||||
{
|
||||
body["sampler"] = settings.Sampler;
|
||||
}
|
||||
|
||||
if (settings.ClipSkip > 0)
|
||||
{
|
||||
body["clipskip"] = settings.ClipSkip;
|
||||
}
|
||||
|
||||
using var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
|
||||
using var response = await _http.PostAsync("/API/GenerateText2Image", content, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
|
||||
if (root.TryGetProperty("error_id", out var errorId)
|
||||
&& errorId.GetString() == "invalid_session_id")
|
||||
{
|
||||
throw new SwarmUiSessionInvalidException();
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("error", out var error))
|
||||
{
|
||||
throw new InvalidOperationException($"SwarmUI error: {error.GetString()}");
|
||||
}
|
||||
|
||||
if (!root.TryGetProperty("images", out var images) || images.GetArrayLength() == 0)
|
||||
{
|
||||
throw new InvalidOperationException("SwarmUI returned no images.");
|
||||
}
|
||||
|
||||
var first = images[0].GetString() ?? throw new InvalidOperationException("SwarmUI image entry was empty.");
|
||||
return await ReadImageAsync(first, cancellationToken);
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<byte[]> ReadImageAsync(string reference, CancellationToken cancellationToken)
|
||||
{
|
||||
if (reference.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var comma = reference.IndexOf(',', StringComparison.Ordinal);
|
||||
if (comma < 0)
|
||||
{
|
||||
throw new InvalidOperationException("Malformed data URL from SwarmUI.");
|
||||
}
|
||||
|
||||
return Convert.FromBase64String(reference[(comma + 1)..]);
|
||||
}
|
||||
|
||||
var path = reference.StartsWith('/') ? reference : $"/{reference}";
|
||||
using var response = await _http.GetAsync(path, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<T> RunWithSessionAsync<T>(Func<Task<T>> call, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_sessionId))
|
||||
{
|
||||
await RefreshSessionAsync(cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await call();
|
||||
}
|
||||
catch (SwarmUiSessionInvalidException)
|
||||
{
|
||||
await RefreshSessionAsync(cancellationToken);
|
||||
return await call();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshSessionAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var content = new StringContent("{}", Encoding.UTF8, "application/json");
|
||||
using var response = await _http.PostAsync("/API/GetNewSession", content, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
using var document = JsonDocument.Parse(json);
|
||||
_sessionId = document.RootElement.GetProperty("session_id").GetString()
|
||||
?? throw new InvalidOperationException("SwarmUI did not return a session_id.");
|
||||
_logger.LogDebug("SwarmUI session refreshed.");
|
||||
}
|
||||
|
||||
internal sealed class SwarmUiSessionInvalidException : Exception;
|
||||
}
|
||||
|
||||
internal static class SwarmUiClientRegistration
|
||||
{
|
||||
public static IServiceCollection AddSwarmUi(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services
|
||||
.AddOptions<SwarmUiOptions>()
|
||||
.Bind(configuration.GetSection(SwarmUiOptions.SectionName))
|
||||
.Validate(options => options.TimeoutSeconds is > 0 and <= 3600, "SwarmUi:TimeoutSeconds must be between 1 and 3600.")
|
||||
.ValidateOnStart();
|
||||
|
||||
services.AddHttpClient<SwarmUiClient>((sp, client) =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<SwarmUiOptions>>().Value;
|
||||
if (!string.IsNullOrWhiteSpace(options.BaseUrl))
|
||||
{
|
||||
client.BaseAddress = new Uri(options.BaseUrl.TrimEnd('/') + "/");
|
||||
client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.Authorization))
|
||||
{
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.Authorization);
|
||||
}
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user