Enhance AIImages mod with settings support and improved UI for image generation. Update localized strings in English and Russian for better clarity. Refactor code for better organization and maintainability.

This commit is contained in:
Leonid Pershin
2025-10-26 18:09:30 +03:00
parent 990c8695b7
commit 0f60721162
17 changed files with 1907 additions and 335 deletions

View File

@@ -0,0 +1,353 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AIImages.Models;
using RimWorld;
using Verse;
namespace AIImages.Services
{
/// <summary>
/// Продвинутый генератор промптов для Stable Diffusion
/// </summary>
public class AdvancedPromptGenerator : IPromptGeneratorService
{
private static readonly Dictionary<string, string> TraitToMood = new Dictionary<
string,
string
>
{
{ "Kind", "warm smile, gentle expression, friendly eyes" },
{ "Bloodlust", "intense gaze, fierce expression, aggressive posture" },
{ "Psychopath", "cold eyes, emotionless face, calculating expression" },
{ "Pessimist", "sad eyes, worried expression, downcast gaze" },
{ "Optimist", "bright smile, hopeful expression, cheerful demeanor" },
{ "Nervous", "anxious expression, tense posture, worried eyes" },
{ "Careful", "focused expression, attentive gaze, composed posture" },
{ "Brave", "confident expression, determined gaze, strong posture" },
{ "Wimp", "fearful eyes, nervous expression, timid posture" },
{ "Greedy", "cunning expression, calculating eyes, sly smile" },
{ "Jealous", "envious gaze, bitter expression, tense face" },
{ "Ascetic", "serene expression, calm demeanor, peaceful eyes" },
{ "Beautiful", "stunning features, attractive appearance, graceful" },
{ "Ugly", "rough features, weathered appearance" },
{ "Pretty", "attractive features, pleasant appearance, charming" },
};
private static readonly Dictionary<ArtStyle, string> ArtStylePrompts = new Dictionary<
ArtStyle,
string
>
{
{ ArtStyle.Realistic, "photorealistic, hyperrealistic, realistic photo, photography" },
{ ArtStyle.SemiRealistic, "semi-realistic, detailed illustration, realistic art" },
{ ArtStyle.Anime, "anime style, manga style, anime character" },
{
ArtStyle.ConceptArt,
"concept art, digital art, artstation, professional concept design"
},
{ ArtStyle.DigitalPainting, "digital painting, painterly, brush strokes, artistic" },
{ ArtStyle.OilPainting, "oil painting, traditional painting, canvas, fine art" },
{ ArtStyle.Sketch, "pencil sketch, hand drawn, sketch art, line art" },
{ ArtStyle.CellShaded, "cell shaded, flat colors, toon shading, stylized" },
};
private static readonly Dictionary<ShotType, string> ShotTypePrompts = new Dictionary<
ShotType,
string
>
{
{ ShotType.Portrait, "portrait, head and shoulders" },
{ ShotType.HalfBody, "half body shot, waist up" },
{ ShotType.FullBody, "full body, full length" },
{ ShotType.CloseUp, "close up, face focus, detailed face" },
{ ShotType.ThreeQuarter, "three-quarter view, 3/4 view" },
};
public string GeneratePositivePrompt(
PawnAppearanceData appearanceData,
StableDiffusionSettings settings
)
{
if (appearanceData == null)
return "portrait of a person";
StringBuilder prompt = new StringBuilder();
// 1. Художественный стиль
if (ArtStylePrompts.TryGetValue(settings.ArtStyle, out string stylePrompt))
{
prompt.Append(stylePrompt);
prompt.Append(", ");
}
// 2. Тип кадра
if (ShotTypePrompts.TryGetValue(settings.ShotType, out string shotPrompt))
{
prompt.Append(shotPrompt);
prompt.Append(" of ");
}
// 3. Базовое описание (возраст и пол)
prompt.Append(GetAgeAndGenderDescription(appearanceData));
prompt.Append(", ");
// 4. Тип тела
string bodyType = GetBodyTypeDescription(appearanceData.BodyType);
if (!string.IsNullOrEmpty(bodyType))
{
prompt.Append(bodyType);
prompt.Append(", ");
}
// 5. Цвет кожи
string skinTone = ColorDescriptionService.GetSkinToneDescription(
appearanceData.SkinColor
);
prompt.Append(skinTone);
prompt.Append(", ");
// 6. Волосы
string hairDescription = GetHairDescription(appearanceData);
if (!string.IsNullOrEmpty(hairDescription))
{
prompt.Append(hairDescription);
prompt.Append(", ");
}
// 7. Настроение и выражение на основе черт характера
string moodDescription = GetMoodFromTraits(appearanceData.Traits);
if (!string.IsNullOrEmpty(moodDescription))
{
prompt.Append(moodDescription);
prompt.Append(", ");
}
// 8. Одежда
string apparelDescription = GetApparelDescription(appearanceData.Apparel);
if (!string.IsNullOrEmpty(apparelDescription))
{
prompt.Append(apparelDescription);
prompt.Append(", ");
}
// 9. Базовый пользовательский промпт (если указан)
if (!string.IsNullOrEmpty(settings.PositivePrompt))
{
prompt.Append(settings.PositivePrompt);
prompt.Append(", ");
}
// 10. Качественные теги
prompt.Append(GetQualityTags(settings.ArtStyle));
return prompt.ToString().Trim().TrimEnd(',');
}
public string GenerateNegativePrompt(StableDiffusionSettings settings)
{
StringBuilder negativePrompt = new StringBuilder();
// Базовые негативные промпты
negativePrompt.Append(
"ugly, deformed, low quality, blurry, bad anatomy, worst quality, "
);
negativePrompt.Append(
"mutated, disfigured, bad proportions, extra limbs, missing limbs, "
);
// Специфичные для стиля негативы
if (
settings.ArtStyle == ArtStyle.Realistic
|| settings.ArtStyle == ArtStyle.SemiRealistic
)
{
negativePrompt.Append("cartoon, anime, painting, drawing, illustration, ");
}
else if (settings.ArtStyle == ArtStyle.Anime)
{
negativePrompt.Append("realistic, photo, photography, 3d, ");
}
// Пользовательский негативный промпт
if (!string.IsNullOrEmpty(settings.NegativePrompt))
{
negativePrompt.Append(settings.NegativePrompt);
}
return negativePrompt.ToString().Trim().TrimEnd(',');
}
public string GetFullPromptDescription(
PawnAppearanceData appearanceData,
StableDiffusionSettings settings
)
{
StringBuilder description = new StringBuilder();
description.AppendLine("=== Positive Prompt ===");
description.AppendLine(GeneratePositivePrompt(appearanceData, settings));
description.AppendLine();
description.AppendLine("=== Negative Prompt ===");
description.AppendLine(GenerateNegativePrompt(settings));
description.AppendLine();
description.AppendLine("=== Technical Parameters ===");
description.AppendLine($"Steps: {settings.Steps}");
description.AppendLine($"CFG Scale: {settings.CfgScale}");
description.AppendLine($"Size: {settings.Width}x{settings.Height}");
description.AppendLine($"Sampler: {settings.Sampler}");
description.AppendLine(
$"Seed: {(settings.Seed == -1 ? "Random" : settings.Seed.ToString())}"
);
return description.ToString();
}
private string GetAgeAndGenderDescription(PawnAppearanceData data)
{
string ageGroup = data.Age switch
{
< 18 => "young",
< 25 => "young adult",
< 35 => "adult",
< 50 => "middle-aged",
< 65 => "mature",
_ => "elderly",
};
string genderLabel = data.Gender switch
{
Gender.Male => "man",
Gender.Female => "woman",
_ => "person",
};
return $"{ageGroup} {genderLabel}";
}
private string GetBodyTypeDescription(string bodyType)
{
if (string.IsNullOrEmpty(bodyType))
return "";
return bodyType.ToLower() switch
{
"thin" => "slender build, lean physique",
"hulk" => "muscular build, strong physique, athletic body",
"fat" => "heavyset build, stocky physique",
"female" => "feminine build",
"male" => "masculine build",
_ => "average build",
};
}
private string GetHairDescription(PawnAppearanceData data)
{
if (string.IsNullOrEmpty(data.HairStyle))
return "";
StringBuilder hair = new StringBuilder();
// Цвет волос
string hairColor = ColorDescriptionService.GetHairColorDescription(data.HairColor);
hair.Append(hairColor);
hair.Append(" ");
// Стиль прически (упрощаем сложные названия)
string style = data
.HairStyle.ToLower()
.Replace("_", " ")
.Replace("shaved", "very short")
.Replace("mohawk", "mohawk hairstyle");
hair.Append(style);
hair.Append(" hair");
return hair.ToString();
}
private string GetMoodFromTraits(List<Trait> traits)
{
if (traits == null || !traits.Any())
return "neutral expression";
// Ищем черты, которые влияют на внешний вид
foreach (var trait in traits)
{
string traitDefName = trait.def.defName;
if (TraitToMood.TryGetValue(traitDefName, out string mood))
{
return mood;
}
}
return "calm expression";
}
private string GetApparelDescription(List<ApparelData> apparel)
{
if (apparel == null || !apparel.Any())
return "simple clothes";
StringBuilder apparelDesc = new StringBuilder("wearing ");
// Берем топ 5 наиболее заметных предметов одежды
var visibleApparel = apparel.Take(5).ToList();
List<string> items = new List<string>();
foreach (var item in visibleApparel)
{
StringBuilder itemDesc = new StringBuilder();
// Цвет (если не белый)
if (item.Color != UnityEngine.Color.white)
{
string colorDesc = ColorDescriptionService.GetApparelColorDescription(
item.Color
);
itemDesc.Append(colorDesc);
itemDesc.Append(" ");
}
// Материал
if (!string.IsNullOrEmpty(item.Material))
{
itemDesc.Append(item.Material.ToLower());
itemDesc.Append(" ");
}
// Название предмета
itemDesc.Append(item.Label.ToLower());
items.Add(itemDesc.ToString());
}
apparelDesc.Append(string.Join(", ", items));
return apparelDesc.ToString();
}
private string GetQualityTags(ArtStyle style)
{
var baseTags = "highly detailed, professional, masterpiece, best quality";
if (style == ArtStyle.Realistic || style == ArtStyle.SemiRealistic)
{
return $"{baseTags}, professional photography, 8k uhd, dslr, high quality, sharp focus";
}
else if (style == ArtStyle.Anime)
{
return $"{baseTags}, anime masterpiece, high resolution, vibrant colors";
}
else if (style == ArtStyle.ConceptArt)
{
return $"{baseTags}, trending on artstation, professional digital art";
}
else
{
return baseTags;
}
}
}
}

View File

@@ -0,0 +1,165 @@
using UnityEngine;
namespace AIImages.Services
{
/// <summary>
/// Сервис для умного определения цветов (вместо RGB значений)
/// </summary>
public static class ColorDescriptionService
{
/// <summary>
/// Получает текстовое описание цвета волос
/// </summary>
public static string GetHairColorDescription(Color color)
{
float h,
s,
v;
Color.RGBToHSV(color, out h, out s, out v);
// Проверяем на оттенки серого
if (s < 0.15f)
{
return GetGrayscaleDescription(v);
}
// Определяем оттенок
string hueDescription = GetHueDescription(h);
string brightnessModifier = GetBrightnessModifier(v, s);
return $"{brightnessModifier}{hueDescription}".Trim();
}
/// <summary>
/// Получает текстовое описание цвета кожи
/// </summary>
public static string GetSkinToneDescription(Color color)
{
// Вычисляем яркость
float brightness = (color.r + color.g + color.b) / 3f;
// Определяем оттенок кожи
if (brightness >= 0.85f)
return "very fair skin";
else if (brightness >= 0.75f)
return "fair skin";
else if (brightness >= 0.65f)
return "light skin";
else if (brightness >= 0.55f)
return "medium skin";
else if (brightness >= 0.45f)
return "olive skin";
else if (brightness >= 0.35f)
return "tan skin";
else if (brightness >= 0.25f)
return "brown skin";
else if (brightness >= 0.15f)
return "dark brown skin";
else
return "very dark skin";
}
/// <summary>
/// Получает описание цвета одежды
/// </summary>
public static string GetApparelColorDescription(Color color)
{
float h,
s,
v;
Color.RGBToHSV(color, out h, out s, out v);
// Проверяем на оттенки серого
if (s < 0.1f)
{
return GetGrayscaleDescription(v);
}
// Определяем оттенок
string hueDescription = GetHueDescription(h);
// Модификаторы насыщенности и яркости
string saturationModifier = "";
if (s < 0.3f)
saturationModifier = "pale ";
else if (s > 0.8f)
saturationModifier = "vivid ";
string brightnessModifier = "";
if (v < 0.3f)
brightnessModifier = "dark ";
else if (v > 0.8f)
brightnessModifier = "bright ";
return $"{brightnessModifier}{saturationModifier}{hueDescription}".Trim();
}
private static string GetGrayscaleDescription(float value)
{
if (value >= 0.95f)
return "white";
else if (value >= 0.8f)
return "light gray";
else if (value >= 0.6f)
return "gray";
else if (value >= 0.4f)
return "dark gray";
else if (value >= 0.2f)
return "charcoal";
else
return "black";
}
private static string GetHueDescription(float hue)
{
// Hue from 0 to 1
if (hue < 0.05f || hue >= 0.95f)
return "red";
if (hue < 0.083f)
return "orange-red";
if (hue < 0.15f)
return "orange";
if (hue < 0.19f)
return "golden";
if (hue < 0.22f)
return "yellow";
if (hue < 0.35f)
return "yellow-green";
if (hue < 0.45f)
return "green";
if (hue < 0.52f)
return "cyan";
if (hue < 0.58f)
return "light blue";
if (hue < 0.65f)
return "blue";
if (hue < 0.72f)
return "deep blue";
if (hue < 0.78f)
return "purple";
if (hue < 0.85f)
return "violet";
if (hue < 0.92f)
return "magenta";
return "pink";
}
private static string GetBrightnessModifier(float value, float saturation)
{
// Для волос используем специальные термины
if (value < 0.2f)
return "jet black ";
else if (value < 0.3f)
return "black ";
else if (value < 0.45f)
return "dark ";
else if (value > 0.85f && saturation < 0.3f)
return "platinum ";
else if (value > 0.75f)
return "light ";
else
return "";
}
}
}

View File

@@ -0,0 +1,26 @@
using AIImages.Models;
using Verse;
namespace AIImages.Services
{
/// <summary>
/// Интерфейс сервиса для извлечения данных о внешности персонажа
/// </summary>
public interface IPawnDescriptionService
{
/// <summary>
/// Извлекает данные о внешности персонажа
/// </summary>
PawnAppearanceData ExtractAppearanceData(Pawn pawn);
/// <summary>
/// Получает текстовое описание внешности для отображения в UI
/// </summary>
string GetAppearanceDescription(Pawn pawn);
/// <summary>
/// Получает текстовое описание одежды для отображения в UI
/// </summary>
string GetApparelDescription(Pawn pawn);
}
}

View File

@@ -0,0 +1,31 @@
using AIImages.Models;
namespace AIImages.Services
{
/// <summary>
/// Интерфейс сервиса для генерации промптов Stable Diffusion
/// </summary>
public interface IPromptGeneratorService
{
/// <summary>
/// Генерирует позитивный промпт на основе данных о персонаже
/// </summary>
string GeneratePositivePrompt(
PawnAppearanceData appearanceData,
StableDiffusionSettings settings
);
/// <summary>
/// Генерирует негативный промпт на основе настроек
/// </summary>
string GenerateNegativePrompt(StableDiffusionSettings settings);
/// <summary>
/// Получает полное описание промпта (позитивный + негативный) для отображения
/// </summary>
string GetFullPromptDescription(
PawnAppearanceData appearanceData,
StableDiffusionSettings settings
);
}
}

View File

@@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using AIImages.Models;
namespace AIImages.Services
{
/// <summary>
/// Интерфейс сервиса для работы с Stable Diffusion API
/// </summary>
public interface IStableDiffusionApiService
{
/// <summary>
/// Генерирует изображение на основе запроса
/// </summary>
Task<GenerationResult> GenerateImageAsync(GenerationRequest request);
/// <summary>
/// Проверяет доступность API
/// </summary>
Task<bool> CheckApiAvailability(string apiEndpoint);
/// <summary>
/// Получает список доступных моделей с API
/// </summary>
Task<List<string>> GetAvailableModels(string apiEndpoint);
/// <summary>
/// Получает список доступных сэмплеров
/// </summary>
Task<List<string>> GetAvailableSamplers(string apiEndpoint);
}
}

View File

@@ -0,0 +1,183 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AIImages.Models;
using RimWorld;
using Verse;
namespace AIImages.Services
{
/// <summary>
/// Сервис для извлечения и описания данных о внешности персонажа
/// </summary>
public class PawnDescriptionService : IPawnDescriptionService
{
public PawnAppearanceData ExtractAppearanceData(Pawn pawn)
{
if (pawn?.story == null)
return null;
var data = new PawnAppearanceData
{
Name = pawn.Name?.ToStringShort ?? "Unknown",
Gender = pawn.gender,
Age = pawn.ageTracker.AgeBiologicalYears,
BodyType = pawn.story.bodyType?.defName,
SkinColor = pawn.story.SkinColor,
HairStyle = pawn.story.hairDef?.label,
HairColor = pawn.story.HairColor,
};
// Извлекаем черты характера
if (pawn.story.traits?.allTraits != null)
{
data.Traits.AddRange(pawn.story.traits.allTraits);
}
// Извлекаем одежду
if (pawn.apparel?.WornApparel != null)
{
foreach (var apparel in pawn.apparel.WornApparel)
{
var apparelData = new ApparelData
{
Label = apparel.def.label,
Material = apparel.Stuff?.label,
Color = apparel.DrawColor,
LayerType = apparel.def.apparel?.LastLayer.ToString(),
Durability = apparel.HitPoints,
MaxDurability = apparel.MaxHitPoints,
};
if (apparel.TryGetQuality(out QualityCategory quality))
{
apparelData.Quality = quality;
}
data.Apparel.Add(apparelData);
}
}
return data;
}
public string GetAppearanceDescription(Pawn pawn)
{
if (pawn?.story == null)
return "AIImages.Appearance.NoInfo".Translate();
StringBuilder sb = new StringBuilder();
// Пол
sb.AppendLine("AIImages.Appearance.Gender".Translate(pawn.gender.GetLabel()));
// Возраст
sb.AppendLine("AIImages.Appearance.Age".Translate(pawn.ageTracker.AgeBiologicalYears));
// Тип тела
if (pawn.story.bodyType != null)
{
sb.AppendLine(
"AIImages.Appearance.BodyType".Translate(pawn.story.bodyType.defName)
);
}
// Цвет кожи (с умным описанием)
if (pawn.story.SkinColor != null)
{
string skinDescription = ColorDescriptionService.GetSkinToneDescription(
pawn.story.SkinColor
);
sb.AppendLine("AIImages.Appearance.SkinTone".Translate(skinDescription));
}
// Волосы
if (pawn.story.hairDef != null)
{
sb.AppendLine("AIImages.Appearance.Hairstyle".Translate(pawn.story.hairDef.label));
if (pawn.story.HairColor != null)
{
string hairColorDescription = ColorDescriptionService.GetHairColorDescription(
pawn.story.HairColor
);
sb.AppendLine(
"AIImages.Appearance.HairColorDesc".Translate(hairColorDescription)
);
}
}
// Черты характера
if (pawn.story.traits?.allTraits != null && pawn.story.traits.allTraits.Any())
{
sb.AppendLine("\n" + "AIImages.Appearance.Traits".Translate());
foreach (var trait in pawn.story.traits.allTraits)
{
sb.AppendLine($" • {trait.LabelCap}");
}
}
return sb.ToString();
}
public string GetApparelDescription(Pawn pawn)
{
if (pawn?.apparel == null)
return "AIImages.Apparel.NoInfo".Translate();
StringBuilder sb = new StringBuilder();
List<Apparel> wornApparel = pawn.apparel.WornApparel;
if (wornApparel == null || !wornApparel.Any())
{
sb.AppendLine("AIImages.Apparel.NoClothes".Translate());
}
else
{
sb.AppendLine("AIImages.Apparel.ListHeader".Translate(wornApparel.Count) + "\n");
foreach (Apparel apparel in wornApparel)
{
FormatApparelItem(sb, apparel);
}
}
return sb.ToString();
}
private void FormatApparelItem(StringBuilder sb, Apparel apparel)
{
sb.AppendLine($"• {apparel.LabelCap}");
if (apparel.TryGetQuality(out QualityCategory quality))
{
sb.AppendLine("AIImages.Apparel.Quality".Translate(quality.GetLabel()));
}
if (apparel.Stuff != null)
{
sb.AppendLine("AIImages.Apparel.Material".Translate(apparel.Stuff.LabelCap));
}
if (apparel.HitPoints < apparel.MaxHitPoints)
{
int percentage = (int)((float)apparel.HitPoints / apparel.MaxHitPoints * 100);
sb.AppendLine(
"AIImages.Apparel.Durability".Translate(
apparel.HitPoints,
apparel.MaxHitPoints,
percentage
)
);
}
if (apparel.DrawColor != UnityEngine.Color.white)
{
string colorDesc = ColorDescriptionService.GetApparelColorDescription(
apparel.DrawColor
);
sb.AppendLine("AIImages.Apparel.ColorDesc".Translate(colorDesc));
}
sb.AppendLine();
}
}
}

View File

@@ -0,0 +1,234 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using AIImages.Models;
using Newtonsoft.Json;
using Verse;
namespace AIImages.Services
{
/// <summary>
/// Сервис для работы с Stable Diffusion API (AUTOMATIC1111 WebUI)
/// </summary>
public class StableDiffusionApiService : IStableDiffusionApiService
{
private readonly HttpClient httpClient;
private readonly string saveFolderPath;
public StableDiffusionApiService(string savePath = "AIImages/Generated")
{
httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
// Определяем путь для сохранения
saveFolderPath = Path.Combine(GenFilePaths.SaveDataFolderPath, savePath);
// Создаем папку, если не существует
if (!Directory.Exists(saveFolderPath))
{
Directory.CreateDirectory(saveFolderPath);
}
}
public async Task<GenerationResult> GenerateImageAsync(GenerationRequest request)
{
try
{
Log.Message(
$"[AI Images] Starting image generation with prompt: {request.Prompt.Substring(0, Math.Min(50, request.Prompt.Length))}..."
);
// Формируем JSON запрос для AUTOMATIC1111 API
var apiRequest = new
{
prompt = request.Prompt,
negative_prompt = request.NegativePrompt,
steps = request.Steps,
cfg_scale = request.CfgScale,
width = request.Width,
height = request.Height,
sampler_name = request.Sampler,
seed = request.Seed,
save_images = false,
send_images = true,
};
string jsonRequest = JsonConvert.SerializeObject(apiRequest);
var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
// Отправляем запрос
string endpoint = $"{request.Model}/sdapi/v1/txt2img";
HttpResponseMessage response = await httpClient.PostAsync(endpoint, content);
if (!response.IsSuccessStatusCode)
{
string errorContent = await response.Content.ReadAsStringAsync();
Log.Error(
$"[AI Images] API request failed: {response.StatusCode} - {errorContent}"
);
return GenerationResult.Failure($"API Error: {response.StatusCode}");
}
string jsonResponse = await response.Content.ReadAsStringAsync();
var apiResponse = JsonConvert.DeserializeObject<Txt2ImgResponse>(jsonResponse);
if (apiResponse?.images == null || apiResponse.images.Length == 0)
{
return GenerationResult.Failure("No images returned from API");
}
// Декодируем изображение из base64
byte[] imageData = Convert.FromBase64String(apiResponse.images[0]);
// Сохраняем изображение
string fileName = $"pawn_{DateTime.Now:yyyyMMdd_HHmmss}.png";
string fullPath = Path.Combine(saveFolderPath, fileName);
await File.WriteAllBytesAsync(fullPath, imageData);
Log.Message($"[AI Images] Image generated successfully and saved to: {fullPath}");
return GenerationResult.SuccessResult(imageData, fullPath, request);
}
catch (TaskCanceledException)
{
return GenerationResult.Failure("Request timeout. Generation took too long.");
}
catch (HttpRequestException ex)
{
Log.Error($"[AI Images] HTTP error: {ex.Message}");
return GenerationResult.Failure($"Connection error: {ex.Message}");
}
catch (Exception ex)
{
Log.Error($"[AI Images] Unexpected error: {ex.Message}\n{ex.StackTrace}");
return GenerationResult.Failure($"Error: {ex.Message}");
}
}
public async Task<bool> CheckApiAvailability(string apiEndpoint)
{
try
{
string endpoint = $"{apiEndpoint}/sdapi/v1/sd-models";
HttpResponseMessage response = await httpClient.GetAsync(endpoint);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
Log.Warning($"[AI Images] API check failed: {ex.Message}");
return false;
}
}
public async Task<List<string>> GetAvailableModels(string apiEndpoint)
{
try
{
string endpoint = $"{apiEndpoint}/sdapi/v1/sd-models";
HttpResponseMessage response = await httpClient.GetAsync(endpoint);
if (!response.IsSuccessStatusCode)
return new List<string>();
string jsonResponse = await response.Content.ReadAsStringAsync();
var models = JsonConvert.DeserializeObject<List<SdModel>>(jsonResponse);
var modelNames = new List<string>();
if (models != null)
{
foreach (var model in models)
{
modelNames.Add(model.title ?? model.model_name);
}
}
Log.Message($"[AI Images] Found {modelNames.Count} models");
return modelNames;
}
catch (Exception ex)
{
Log.Error($"[AI Images] Failed to load models: {ex.Message}");
return new List<string>();
}
}
public async Task<List<string>> GetAvailableSamplers(string apiEndpoint)
{
try
{
string endpoint = $"{apiEndpoint}/sdapi/v1/samplers";
HttpResponseMessage response = await httpClient.GetAsync(endpoint);
if (!response.IsSuccessStatusCode)
return GetDefaultSamplers();
string jsonResponse = await response.Content.ReadAsStringAsync();
var samplers = JsonConvert.DeserializeObject<List<SdSampler>>(jsonResponse);
var samplerNames = new List<string>();
if (samplers != null)
{
foreach (var sampler in samplers)
{
samplerNames.Add(sampler.name);
}
}
Log.Message($"[AI Images] Found {samplerNames.Count} samplers");
return samplerNames;
}
catch (Exception ex)
{
Log.Warning($"[AI Images] Failed to load samplers: {ex.Message}");
return GetDefaultSamplers();
}
}
private List<string> GetDefaultSamplers()
{
return new List<string>
{
"Euler a",
"Euler",
"LMS",
"Heun",
"DPM2",
"DPM2 a",
"DPM++ 2S a",
"DPM++ 2M",
"DPM++ SDE",
"DPM fast",
"DPM adaptive",
"LMS Karras",
"DPM2 Karras",
"DPM2 a Karras",
"DPM++ 2S a Karras",
"DPM++ 2M Karras",
"DPM++ SDE Karras",
"DDIM",
"PLMS",
};
}
// Вспомогательные классы для десериализации JSON ответов
#pragma warning disable S3459, S1144 // Properties set by JSON deserializer
private sealed class Txt2ImgResponse
{
public string[] images { get; set; }
}
private sealed class SdModel
{
public string title { get; set; }
public string model_name { get; set; }
}
private sealed class SdSampler
{
public string name { get; set; }
}
#pragma warning restore S3459, S1144
}
}