Implement portrait prompt API and UI enhancements. Added a new endpoint to fetch portrait prompts, updated the client to handle prompt display, and improved localization for prompt-related text. Enhanced styling for prompt elements in the UI.
This commit is contained in:
@@ -270,6 +270,41 @@ internal static class SchoolEndpoints
|
||||
})
|
||||
.WithName("GetSchoolPersonPortrait");
|
||||
|
||||
schools.MapGet("/{id:int}/people/{personId}/portrait/prompt", async (
|
||||
int id,
|
||||
string personId,
|
||||
string? kind,
|
||||
string? promptExtra,
|
||||
PortraitService portraits,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(personId) || personId.Length > 64)
|
||||
{
|
||||
return Problem(StatusCodes.Status400BadRequest, "invalid-query", "The person id is not valid.");
|
||||
}
|
||||
|
||||
if (!PortraitKindParser.TryParse(kind, out var portraitKind))
|
||||
{
|
||||
return Problem(StatusCodes.Status400BadRequest, "invalid-query", "kind must be avatar, custom, or full.");
|
||||
}
|
||||
|
||||
var result = await portraits.BuildPromptAsync(id, personId, portraitKind, promptExtra, cancellationToken);
|
||||
return result.Outcome switch
|
||||
{
|
||||
PortraitPromptBuildOutcome.Succeeded => Results.Ok(new PortraitPromptResponse(
|
||||
PortraitKindParser.ToApiValue(result.Kind),
|
||||
result.Positive,
|
||||
result.Negative,
|
||||
result.PromptExtra)),
|
||||
PortraitPromptBuildOutcome.InvalidPrompt =>
|
||||
Problem(StatusCodes.Status400BadRequest, "invalid-body", "Custom portraits need a non-empty promptExtra up to 2000 characters."),
|
||||
PortraitPromptBuildOutcome.UnknownPerson =>
|
||||
Problem(StatusCodes.Status404NotFound, "unknown-person", "That person is not in the school."),
|
||||
_ => Problem(StatusCodes.Status404NotFound, "unknown-school", "That school does not exist."),
|
||||
};
|
||||
})
|
||||
.WithName("GetSchoolPersonPortraitPrompt");
|
||||
|
||||
schools.MapPost("/{id:int}/people/{personId}/portrait", async (
|
||||
int id,
|
||||
string personId,
|
||||
|
||||
@@ -50,6 +50,38 @@ internal sealed class PortraitService(
|
||||
return outcome.Error;
|
||||
}
|
||||
|
||||
public async Task<PortraitPromptBuildResult> BuildPromptAsync(
|
||||
int schoolId,
|
||||
string personId,
|
||||
PortraitKind kind,
|
||||
string? promptExtra,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var resolved = ResolveCustomPromptExtra(schoolId, personId, kind, promptExtra);
|
||||
if (kind == PortraitKind.Custom && resolved is null)
|
||||
{
|
||||
return PortraitPromptBuildResult.InvalidPrompt;
|
||||
}
|
||||
|
||||
var outcome = await LookupPersonAsync(schoolId, personId, cancellationToken);
|
||||
if (outcome.Error == PersonLookupError.UnknownPerson)
|
||||
{
|
||||
return PortraitPromptBuildResult.UnknownPerson;
|
||||
}
|
||||
|
||||
if (outcome.Error != PersonLookupError.None || outcome.Card is null)
|
||||
{
|
||||
return PortraitPromptBuildResult.UnknownSchool;
|
||||
}
|
||||
|
||||
var (positive, negative) = PortraitPromptBuilder.Build(outcome.Card, settings, kind, resolved);
|
||||
return PortraitPromptBuildResult.Succeeded(
|
||||
kind,
|
||||
positive,
|
||||
negative,
|
||||
kind == PortraitKind.Custom ? resolved : null);
|
||||
}
|
||||
|
||||
public async Task<PortraitGenerationResult> GenerateAsync(
|
||||
int schoolId,
|
||||
string personId,
|
||||
@@ -76,14 +108,7 @@ internal sealed class PortraitService(
|
||||
}
|
||||
}
|
||||
|
||||
var command = new GameCommand.GetPerson(
|
||||
schoolId,
|
||||
personId,
|
||||
PromptLocale,
|
||||
NewCompletion<PersonCardResult>());
|
||||
commands.Enqueue(command);
|
||||
|
||||
var outcome = await command.Result.Task.WaitAsync(PersonLookupTimeout, cancellationToken);
|
||||
var outcome = await LookupPersonAsync(schoolId, personId, cancellationToken);
|
||||
if (outcome.Error == PersonLookupError.UnknownPerson)
|
||||
{
|
||||
return PortraitGenerationResult.UnknownPerson;
|
||||
@@ -126,10 +151,83 @@ internal sealed class PortraitService(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<PersonCardResult> LookupPersonAsync(
|
||||
int schoolId,
|
||||
string personId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var command = new GameCommand.GetPerson(
|
||||
schoolId,
|
||||
personId,
|
||||
PromptLocale,
|
||||
NewCompletion<PersonCardResult>());
|
||||
commands.Enqueue(command);
|
||||
return await command.Result.Task.WaitAsync(PersonLookupTimeout, cancellationToken);
|
||||
}
|
||||
|
||||
private string? ResolveCustomPromptExtra(int schoolId, string personId, PortraitKind kind, string? promptExtra)
|
||||
{
|
||||
if (kind != PortraitKind.Custom)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
promptExtra = promptExtra?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(promptExtra))
|
||||
{
|
||||
promptExtra = store.TryReadCustomPortraitPrompt(schoolId, personId)?.Trim();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(promptExtra) || promptExtra.Length > MaxCustomPromptLength)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return promptExtra;
|
||||
}
|
||||
|
||||
private static TaskCompletionSource<T> NewCompletion<T>() =>
|
||||
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
|
||||
internal enum PortraitPromptBuildOutcome
|
||||
{
|
||||
Succeeded,
|
||||
UnknownSchool,
|
||||
UnknownPerson,
|
||||
InvalidPrompt,
|
||||
}
|
||||
|
||||
internal sealed record PortraitPromptBuildResult(
|
||||
PortraitPromptBuildOutcome Outcome,
|
||||
PortraitKind Kind,
|
||||
string Positive,
|
||||
string Negative,
|
||||
string? PromptExtra)
|
||||
{
|
||||
public static PortraitPromptBuildResult UnknownSchool { get; } =
|
||||
new(PortraitPromptBuildOutcome.UnknownSchool, default, string.Empty, string.Empty, null);
|
||||
|
||||
public static PortraitPromptBuildResult UnknownPerson { get; } =
|
||||
new(PortraitPromptBuildOutcome.UnknownPerson, default, string.Empty, string.Empty, null);
|
||||
|
||||
public static PortraitPromptBuildResult InvalidPrompt { get; } =
|
||||
new(PortraitPromptBuildOutcome.InvalidPrompt, default, string.Empty, string.Empty, null);
|
||||
|
||||
public static PortraitPromptBuildResult Succeeded(
|
||||
PortraitKind kind,
|
||||
string positive,
|
||||
string negative,
|
||||
string? promptExtra) =>
|
||||
new(PortraitPromptBuildOutcome.Succeeded, kind, positive, negative, promptExtra);
|
||||
}
|
||||
|
||||
internal sealed record PortraitPromptResponse(
|
||||
string Kind,
|
||||
string Positive,
|
||||
string Negative,
|
||||
string? PromptExtra);
|
||||
|
||||
internal enum PortraitGenerationOutcome
|
||||
{
|
||||
Succeeded,
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"model": "DreamShaper_XL_-_Lightning_DPM++_SDE.safetensors",
|
||||
"steps": 4,
|
||||
"cfgScale": 2,
|
||||
"clipSkip": 2,
|
||||
"sampler": "dpmpp_sde",
|
||||
"scheduler": "karras",
|
||||
"seed": 3346112079,
|
||||
"positive": "cinematic photo, realist detail, detailed character expressions, amazing quality, analog film grain, school portrait photograph, neutral background, natural lighting, realistic, sharp focus",
|
||||
"negative": "(low quality, worst quality:1.4), cgi, text, signature, watermark, extra limbs, nsfw, nude, naked, explicit, blurry, deformed, bad anatomy, logo",
|
||||
"avatar": {
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"positive": "close up, head and shoulders portrait, facing the camera, upper body visible."
|
||||
},
|
||||
"custom": {
|
||||
"width": 896,
|
||||
"height": 1152
|
||||
},
|
||||
"fullBody": {
|
||||
"width": 896,
|
||||
"height": 1152,
|
||||
"positive": "full body standing portrait, head to toe visible, neutral pose, current outfit clearly visible."
|
||||
}
|
||||
}
|
||||
{
|
||||
"model": "DreamShaper_XL_-_Lightning_DPM++_SDE.safetensors",
|
||||
"steps": 4,
|
||||
"cfgScale": 2,
|
||||
"clipSkip": 2,
|
||||
"sampler": "dpmpp_sde",
|
||||
"scheduler": "karras",
|
||||
"seed": 3346112079,
|
||||
"positive": "cinematic photo, realist detail, detailed character expressions, amazing quality, analog film grain, school portrait photograph, neutral background, natural lighting, realistic, sharp focus",
|
||||
"negative": "(low quality, worst quality:1.4), cgi, text, signature, watermark, extra limbs, censored, blurry, deformed, bad anatomy, logo",
|
||||
"avatar": {
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"positive": "close up, head and shoulders portrait, facing the camera, upper body visible."
|
||||
},
|
||||
"custom": {
|
||||
"width": 896,
|
||||
"height": 1152
|
||||
},
|
||||
"fullBody": {
|
||||
"width": 896,
|
||||
"height": 1152,
|
||||
"positive": "full body standing portrait, head to toe visible, neutral pose, current outfit clearly visible."
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user