From 02b014318629e86e3a1aa435ca78fde34241676d Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 26 Oct 2025 19:10:45 +0300 Subject: [PATCH] Enhance AIImages mod by adding cancellation support for image generation, improving user experience with localized strings for cancellation actions in English and Russian. Refactor service integration for better dependency management and update AIImages.dll to reflect these changes. --- Assemblies/AIImages.dll | Bin 69120 -> 83968 bytes Languages/English/Keyed/AIImages.xml | 4 + Languages/Russian/Keyed/AIImages.xml | 4 + Source/AIImages/AIImagesMod.cs | 99 +++++- Source/AIImages/Helpers/AsyncHelper.cs | 145 +++++++++ .../Services/IStableDiffusionApiService.cs | 26 +- Source/AIImages/Services/ServiceContainer.cs | 83 +++++ ...ervice.cs => StableDiffusionNetAdapter.cs} | 142 +++++++-- Source/AIImages/UI/AIImagesSettingsUI.cs | 227 +++++++------- .../AIImages/Validation/SettingsValidator.cs | 287 ++++++++++++++++++ Source/AIImages/Window_AIImage.cs | 131 ++++++-- 11 files changed, 974 insertions(+), 174 deletions(-) create mode 100644 Source/AIImages/Helpers/AsyncHelper.cs create mode 100644 Source/AIImages/Services/ServiceContainer.cs rename Source/AIImages/Services/{StableDiffusionApiService.cs => StableDiffusionNetAdapter.cs} (64%) create mode 100644 Source/AIImages/Validation/SettingsValidator.cs diff --git a/Assemblies/AIImages.dll b/Assemblies/AIImages.dll index 8dbc70794504ea1c3ebe9ba6bec02e98a9dddbf6..0bda9d2393cbf3d152886c8c9790136d8310debe 100644 GIT binary patch literal 83968 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 a@%cOYx8Rszpf-Er?Y$z{2^uL3+3~cwZ%d! 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 diff --git a/Languages/English/Keyed/AIImages.xml b/Languages/English/Keyed/AIImages.xml index bc0560e..f19385a 100644 --- a/Languages/English/Keyed/AIImages.xml +++ b/Languages/English/Keyed/AIImages.xml @@ -34,9 +34,13 @@ Generate Image Generating... + Cancel Generation Generating image, please wait... Image generated successfully! Generation failed + Generation cancelled by user + Cancelling generation... + Generation error Image saved to: {0} No image generated yet.\nClick "Generate Image" to start. diff --git a/Languages/Russian/Keyed/AIImages.xml b/Languages/Russian/Keyed/AIImages.xml index cd277ab..3a753e5 100644 --- a/Languages/Russian/Keyed/AIImages.xml +++ b/Languages/Russian/Keyed/AIImages.xml @@ -34,9 +34,13 @@ Сгенерировать изображение Генерация... + Отменить генерацию Генерируется изображение, пожалуйста подождите... Изображение успешно сгенерировано! Ошибка генерации + Генерация отменена пользователем + Отмена генерации... + Ошибка генерации Изображение сохранено в: {0} Изображение еще не сгенерировано.\nНажмите "Сгенерировать изображение" для начала. diff --git a/Source/AIImages/AIImagesMod.cs b/Source/AIImages/AIImagesMod.cs index 8700b50..dfffe35 100644 --- a/Source/AIImages/AIImagesMod.cs +++ b/Source/AIImages/AIImagesMod.cs @@ -1,39 +1,91 @@ +using System; using AIImages.Services; using AIImages.Settings; +using AIImages.Validation; using HarmonyLib; +using RimWorld; using UnityEngine; using Verse; namespace AIImages { ///

- /// Main mod class with settings support + /// Main mod class with settings support and dependency injection /// public class AIImagesMod : Mod { - public static AIImagesModSettings Settings { get; private set; } + private static AIImagesMod _instance = null!; + private readonly ServiceContainer _serviceContainer; - // Singleton сервисы - public static IPawnDescriptionService PawnDescriptionService { get; private set; } - public static IPromptGeneratorService PromptGeneratorService { get; private set; } - public static IStableDiffusionApiService ApiService { get; private set; } + /// + /// Глобальный экземпляр мода (для доступа из других классов) + /// + public static AIImagesMod Instance + { + get + { + if (_instance == null) + { + throw new InvalidOperationException( + "[AI Images] Mod instance not initialized. This should not happen." + ); + } + return _instance; + } + private set => _instance = value; + } + + /// + /// Настройки мода + /// + public static AIImagesModSettings Settings => Instance._serviceContainer.Settings; + + /// + /// Контейнер сервисов с dependency injection + /// + public static ServiceContainer Services => Instance._serviceContainer; public AIImagesMod(ModContentPack content) : base(content) { - Settings = GetSettings(); + Instance = this; - // Инициализируем сервисы - PawnDescriptionService = new PawnDescriptionService(); - PromptGeneratorService = new AdvancedPromptGenerator(); - ApiService = new StableDiffusionApiService(Settings.savePath); + var settings = GetSettings(); - Log.Message("[AI Images] Mod initialized successfully with settings"); + // Валидируем настройки при загрузке + var validationResult = SettingsValidator.Validate(settings); + if (!validationResult.IsValid) + { + Log.Warning( + $"[AI Images] Settings validation failed:\n{validationResult.GetErrorsAsString()}" + ); + } + + if (validationResult.HasWarnings) + { + Log.Warning( + $"[AI Images] Settings validation warnings:\n{validationResult.GetWarningsAsString()}" + ); + } + + // Создаем контейнер сервисов + try + { + _serviceContainer = new ServiceContainer(settings); + Log.Message("[AI Images] Mod initialized successfully with dependency injection"); + } + catch (Exception ex) + { + Log.Error( + $"[AI Images] Failed to initialize ServiceContainer: {ex.Message}\n{ex.StackTrace}" + ); + throw; + } } public override void DoSettingsWindowContents(Rect inRect) { - AIImagesSettingsUI.DoSettingsWindowContents(inRect, Settings); + AIImagesSettingsUI.DoSettingsWindowContents(inRect, Settings, _serviceContainer); base.DoSettingsWindowContents(inRect); } @@ -41,6 +93,27 @@ namespace AIImages { return "AI Images"; } + + /// + /// Вызывается при выгрузке мода + /// + public override void WriteSettings() + { + base.WriteSettings(); + + // Валидируем настройки перед сохранением + var validationResult = SettingsValidator.Validate(Settings); + if (!validationResult.IsValid) + { + Log.Warning( + $"[AI Images] Saving settings with validation errors:\n{validationResult.GetErrorsAsString()}" + ); + Messages.Message( + "AI Images: Some settings have validation errors. Check the log.", + MessageTypeDefOf.CautionInput + ); + } + } } /// diff --git a/Source/AIImages/Helpers/AsyncHelper.cs b/Source/AIImages/Helpers/AsyncHelper.cs new file mode 100644 index 0000000..2e787ed --- /dev/null +++ b/Source/AIImages/Helpers/AsyncHelper.cs @@ -0,0 +1,145 @@ +using System; +using System.Threading.Tasks; +using RimWorld; +using Verse; + +namespace AIImages.Helpers +{ + /// + /// Вспомогательный класс для правильной обработки асинхронных операций в RimWorld + /// Предотвращает fire-and-forget паттерн и обеспечивает централизованную обработку ошибок + /// + public static class AsyncHelper + { + /// + /// Выполняет асинхронную задачу с обработкой ошибок + /// + public static async Task RunAsync(Func taskFunc, string operationName = "Operation") + { + try + { + await taskFunc(); + } + catch (OperationCanceledException) + { + Log.Message($"[AI Images] {operationName} was cancelled"); + Messages.Message($"{operationName} was cancelled", MessageTypeDefOf.RejectInput); + } + catch (Exception ex) + { + Log.Error($"[AI Images] Error in {operationName}: {ex.Message}\n{ex.StackTrace}"); + Messages.Message( + $"Error in {operationName}: {ex.Message}", + MessageTypeDefOf.RejectInput + ); + } + } + + /// + /// Выполняет асинхронную задачу с обработкой ошибок и callback при успехе + /// + public static async Task RunAsync( + Func> taskFunc, + Action onSuccess, + string operationName = "Operation" + ) + { + try + { + T result = await taskFunc(); + onSuccess?.Invoke(result); + } + catch (OperationCanceledException) + { + Log.Message($"[AI Images] {operationName} was cancelled"); + Messages.Message($"{operationName} was cancelled", MessageTypeDefOf.RejectInput); + } + catch (Exception ex) + { + Log.Error($"[AI Images] Error in {operationName}: {ex.Message}\n{ex.StackTrace}"); + Messages.Message( + $"Error in {operationName}: {ex.Message}", + MessageTypeDefOf.RejectInput + ); + } + } + + /// + /// Выполняет асинхронную задачу с полным контролем: onSuccess, onError, onCancel + /// + public static async Task RunAsync( + Func> taskFunc, + Action onSuccess, + Action onError = null, + Action onCancel = null, + string operationName = "Operation" + ) + { + try + { + T result = await taskFunc(); + onSuccess?.Invoke(result); + } + catch (OperationCanceledException) + { + Log.Message($"[AI Images] {operationName} was cancelled"); + + if (onCancel != null) + { + onCancel(); + } + else + { + Messages.Message( + $"{operationName} was cancelled", + MessageTypeDefOf.RejectInput + ); + } + } + catch (Exception ex) + { + Log.Error($"[AI Images] Error in {operationName}: {ex.Message}\n{ex.StackTrace}"); + + if (onError != null) + { + onError(ex); + } + else + { + Messages.Message( + $"Error in {operationName}: {ex.Message}", + MessageTypeDefOf.RejectInput + ); + } + } + } + + /// + /// Безопасно выполняет Task без ожидания результата, с логированием ошибок + /// Используется когда нужен fire-and-forget, но с обработкой ошибок + /// + public static void FireAndForget(Task task, string operationName = "Background operation") + { + if (task == null) + return; + + task.ContinueWith( + t => + { + if (t.IsFaulted && t.Exception != null) + { + var ex = t.Exception.GetBaseException(); + Log.Error( + $"[AI Images] Error in {operationName}: {ex.Message}\n{ex.StackTrace}" + ); + } + else if (t.IsCanceled) + { + Log.Message($"[AI Images] {operationName} was cancelled"); + } + }, + TaskScheduler.Default + ); + } + } +} diff --git a/Source/AIImages/Services/IStableDiffusionApiService.cs b/Source/AIImages/Services/IStableDiffusionApiService.cs index 3e6617a..e47ef7f 100644 --- a/Source/AIImages/Services/IStableDiffusionApiService.cs +++ b/Source/AIImages/Services/IStableDiffusionApiService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using AIImages.Models; @@ -12,26 +13,41 @@ namespace AIImages.Services /// /// Генерирует изображение на основе запроса /// - Task GenerateImageAsync(GenerationRequest request); + Task GenerateImageAsync( + GenerationRequest request, + CancellationToken cancellationToken = default + ); /// /// Проверяет доступность API /// - Task CheckApiAvailability(string apiEndpoint); + Task CheckApiAvailability( + string apiEndpoint, + CancellationToken cancellationToken = default + ); /// /// Получает список доступных моделей с API /// - Task> GetAvailableModels(string apiEndpoint); + Task> GetAvailableModels( + string apiEndpoint, + CancellationToken cancellationToken = default + ); /// /// Получает список доступных сэмплеров /// - Task> GetAvailableSamplers(string apiEndpoint); + Task> GetAvailableSamplers( + string apiEndpoint, + CancellationToken cancellationToken = default + ); /// /// Получает список доступных schedulers /// - Task> GetAvailableSchedulers(string apiEndpoint); + Task> GetAvailableSchedulers( + string apiEndpoint, + CancellationToken cancellationToken = default + ); } } diff --git a/Source/AIImages/Services/ServiceContainer.cs b/Source/AIImages/Services/ServiceContainer.cs new file mode 100644 index 0000000..d2dde2b --- /dev/null +++ b/Source/AIImages/Services/ServiceContainer.cs @@ -0,0 +1,83 @@ +using System; +using AIImages.Settings; +using Verse; + +namespace AIImages.Services +{ + /// + /// Простой DI контейнер для управления зависимостями мода + /// Используется вместо статического Service Locator паттерна + /// + public class ServiceContainer : IDisposable + { + private bool _disposed; + + // Сервисы + public IPawnDescriptionService PawnDescriptionService { get; } + public IPromptGeneratorService PromptGeneratorService { get; } + public IStableDiffusionApiService ApiService { get; private set; } + + // Настройки + public AIImagesModSettings Settings { get; } + + public ServiceContainer(AIImagesModSettings settings) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + Settings = settings; + + // Инициализируем сервисы с внедрением зависимостей + PawnDescriptionService = new PawnDescriptionService(); + PromptGeneratorService = new AdvancedPromptGenerator(); + + // Создаем API сервис с текущими настройками + RecreateApiService(); + + Log.Message("[AI Images] ServiceContainer initialized successfully"); + } + + /// + /// Пересоздает API сервис с новыми настройками (например, когда изменился endpoint) + /// + public void RecreateApiService() + { + // Освобождаем старый сервис, если он был + if (ApiService is IDisposable disposable) + { + disposable.Dispose(); + } + + // Создаем новый с актуальными настройками + ApiService = new StableDiffusionNetAdapter(Settings.apiEndpoint, Settings.savePath); + + Log.Message($"[AI Images] API service recreated with endpoint: {Settings.apiEndpoint}"); + } + + /// + /// Проверяет, нужно ли пересоздать API сервис (например, если изменился endpoint) + /// + public bool ShouldRecreateApiService(string newEndpoint) + { + // Можно добавить более сложную логику проверки + return Settings.apiEndpoint != newEndpoint; + } + + public void Dispose() + { + if (_disposed) + return; + + // Освобождаем ресурсы API сервиса + if (ApiService is IDisposable disposable) + { + disposable.Dispose(); + } + + _disposed = true; + Log.Message("[AI Images] ServiceContainer disposed"); + } + } +} diff --git a/Source/AIImages/Services/StableDiffusionApiService.cs b/Source/AIImages/Services/StableDiffusionNetAdapter.cs similarity index 64% rename from Source/AIImages/Services/StableDiffusionApiService.cs rename to Source/AIImages/Services/StableDiffusionNetAdapter.cs index 3bb9373..413c424 100644 --- a/Source/AIImages/Services/StableDiffusionApiService.cs +++ b/Source/AIImages/Services/StableDiffusionNetAdapter.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Http; using System.Text; +using System.Threading; using System.Threading.Tasks; using AIImages.Models; using Newtonsoft.Json; @@ -11,29 +13,60 @@ using Verse; namespace AIImages.Services { /// - /// Сервис для работы с Stable Diffusion API (AUTOMATIC1111 WebUI) + /// Адаптер для Stable Diffusion API (AUTOMATIC1111 WebUI) + /// TODO: В будущем можно мигрировать на библиотеку StableDiffusionNet когда API будет полностью совместимо /// - public class StableDiffusionApiService : IStableDiffusionApiService + public class StableDiffusionNetAdapter : IStableDiffusionApiService, IDisposable { - private readonly HttpClient httpClient; - private readonly string saveFolderPath; - - public StableDiffusionApiService(string savePath = "AIImages/Generated") + // Shared HttpClient для предотвращения socket exhaustion + // См: https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines + private static readonly HttpClient _sharedHttpClient = new HttpClient { - httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(5) }; + Timeout = TimeSpan.FromMinutes(5), + }; + + private readonly string _apiEndpoint; + private readonly string _saveFolderPath; + private bool _disposed; + + public StableDiffusionNetAdapter(string apiEndpoint, string savePath = "AIImages/Generated") + { + if (string.IsNullOrEmpty(apiEndpoint)) + { + throw new ArgumentException( + "API endpoint cannot be null or empty", + nameof(apiEndpoint) + ); + } + + _apiEndpoint = apiEndpoint; // Определяем путь для сохранения - saveFolderPath = Path.Combine(GenFilePaths.SaveDataFolderPath, savePath); + _saveFolderPath = Path.Combine(GenFilePaths.SaveDataFolderPath, savePath); // Создаем папку, если не существует - if (!Directory.Exists(saveFolderPath)) + if (!Directory.Exists(_saveFolderPath)) { - Directory.CreateDirectory(saveFolderPath); + Directory.CreateDirectory(_saveFolderPath); } + + Log.Message( + $"[AI Images] StableDiffusion adapter initialized with endpoint: {apiEndpoint}" + ); } - public async Task GenerateImageAsync(GenerationRequest request) + public async Task GenerateImageAsync( + GenerationRequest request, + CancellationToken cancellationToken = default + ) { + ThrowIfDisposed(); + + if (request == null) + { + return GenerationResult.Failure("Request cannot be null"); + } + try { Log.Message( @@ -59,9 +92,13 @@ namespace AIImages.Services 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); + // Отправляем запрос с поддержкой cancellation + string endpoint = $"{_apiEndpoint}/sdapi/v1/txt2img"; + HttpResponseMessage response = await _sharedHttpClient.PostAsync( + endpoint, + content, + cancellationToken + ); if (!response.IsSuccessStatusCode) { @@ -85,7 +122,7 @@ namespace AIImages.Services // Сохраняем изображение string fileName = $"pawn_{DateTime.Now:yyyyMMdd_HHmmss}.png"; - string fullPath = Path.Combine(saveFolderPath, fileName); + string fullPath = Path.Combine(_saveFolderPath, fileName); await File.WriteAllBytesAsync(fullPath, imageData); Log.Message($"[AI Images] Image generated successfully and saved to: {fullPath}"); @@ -94,8 +131,14 @@ namespace AIImages.Services } catch (TaskCanceledException) { + Log.Warning("[AI Images] Request timeout. Generation took too long."); return GenerationResult.Failure("Request timeout. Generation took too long."); } + catch (OperationCanceledException) + { + Log.Message("[AI Images] Image generation was cancelled"); + return GenerationResult.Failure("Generation cancelled"); + } catch (HttpRequestException ex) { Log.Error($"[AI Images] HTTP error: {ex.Message}"); @@ -108,12 +151,20 @@ namespace AIImages.Services } } - public async Task CheckApiAvailability(string apiEndpoint) + public async Task CheckApiAvailability( + string apiEndpoint, + CancellationToken cancellationToken = default + ) { + ThrowIfDisposed(); + try { string endpoint = $"{apiEndpoint}/sdapi/v1/sd-models"; - HttpResponseMessage response = await httpClient.GetAsync(endpoint); + HttpResponseMessage response = await _sharedHttpClient.GetAsync( + endpoint, + cancellationToken + ); return response.IsSuccessStatusCode; } catch (Exception ex) @@ -123,12 +174,20 @@ namespace AIImages.Services } } - public async Task> GetAvailableModels(string apiEndpoint) + public async Task> GetAvailableModels( + string apiEndpoint, + CancellationToken cancellationToken = default + ) { + ThrowIfDisposed(); + try { string endpoint = $"{apiEndpoint}/sdapi/v1/sd-models"; - HttpResponseMessage response = await httpClient.GetAsync(endpoint); + HttpResponseMessage response = await _sharedHttpClient.GetAsync( + endpoint, + cancellationToken + ); if (!response.IsSuccessStatusCode) return new List(); @@ -155,12 +214,20 @@ namespace AIImages.Services } } - public async Task> GetAvailableSamplers(string apiEndpoint) + public async Task> GetAvailableSamplers( + string apiEndpoint, + CancellationToken cancellationToken = default + ) { + ThrowIfDisposed(); + try { string endpoint = $"{apiEndpoint}/sdapi/v1/samplers"; - HttpResponseMessage response = await httpClient.GetAsync(endpoint); + HttpResponseMessage response = await _sharedHttpClient.GetAsync( + endpoint, + cancellationToken + ); if (!response.IsSuccessStatusCode) return GetDefaultSamplers(); @@ -187,12 +254,20 @@ namespace AIImages.Services } } - public async Task> GetAvailableSchedulers(string apiEndpoint) + public async Task> GetAvailableSchedulers( + string apiEndpoint, + CancellationToken cancellationToken = default + ) { + ThrowIfDisposed(); + try { string endpoint = $"{apiEndpoint}/sdapi/v1/schedulers"; - HttpResponseMessage response = await httpClient.GetAsync(endpoint); + HttpResponseMessage response = await _sharedHttpClient.GetAsync( + endpoint, + cancellationToken + ); if (!response.IsSuccessStatusCode) return GetDefaultSchedulers(); @@ -258,8 +333,25 @@ namespace AIImages.Services }; } + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(StableDiffusionNetAdapter)); + } + } + + public void Dispose() + { + if (_disposed) + return; + + // Не dispose shared HttpClient - он используется глобально + _disposed = true; + } + // Вспомогательные классы для десериализации JSON ответов -#pragma warning disable S3459, S1144 // Properties set by JSON deserializer +#pragma warning disable S3459, S1144, IDE1006 // Properties set by JSON deserializer private sealed class Txt2ImgResponse { public string[] images { get; set; } @@ -280,6 +372,6 @@ namespace AIImages.Services { public string name { get; set; } } -#pragma warning restore S3459, S1144 +#pragma warning restore S3459, S1144, IDE1006 } } diff --git a/Source/AIImages/UI/AIImagesSettingsUI.cs b/Source/AIImages/UI/AIImagesSettingsUI.cs index d09ffc1..2d10b37 100644 --- a/Source/AIImages/UI/AIImagesSettingsUI.cs +++ b/Source/AIImages/UI/AIImagesSettingsUI.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using AIImages.Helpers; using AIImages.Models; +using AIImages.Services; using AIImages.Settings; using RimWorld; using UnityEngine; @@ -19,7 +21,11 @@ namespace AIImages private static string widthBuffer; private static string heightBuffer; - public static void DoSettingsWindowContents(Rect inRect, AIImagesModSettings settings) + public static void DoSettingsWindowContents( + Rect inRect, + AIImagesModSettings settings, + ServiceContainer serviceContainer + ) { InitializeBuffers(settings); @@ -29,7 +35,7 @@ namespace AIImages Widgets.BeginScrollView(inRect, ref scrollPosition, viewRect); listingStandard.Begin(viewRect); - DrawApiSettings(listingStandard, settings); + DrawApiSettings(listingStandard, settings, serviceContainer); DrawGenerationSettings(listingStandard, settings); DrawSamplerSchedulerSettings(listingStandard, settings); DrawPromptsSettings(listingStandard, settings); @@ -51,7 +57,8 @@ namespace AIImages private static void DrawApiSettings( Listing_Standard listingStandard, - AIImagesModSettings settings + AIImagesModSettings settings, + ServiceContainer serviceContainer ) { listingStandard.Label( @@ -61,18 +68,36 @@ namespace AIImages ); listingStandard.GapLine(); + string oldEndpoint = settings.apiEndpoint; listingStandard.Label("AIImages.Settings.ApiEndpoint".Translate() + ":"); settings.apiEndpoint = listingStandard.TextEntry(settings.apiEndpoint); listingStandard.Gap(8f); + // Если endpoint изменился, пересоздаем API сервис + if (oldEndpoint != settings.apiEndpoint) + { + try + { + serviceContainer.RecreateApiService(); + } + catch (Exception ex) + { + Log.Error($"[AI Images] Failed to recreate API service: {ex.Message}"); + Messages.Message( + "Failed to update API endpoint. Check the log.", + MessageTypeDefOf.RejectInput + ); + } + } + if (listingStandard.ButtonText("AIImages.Settings.TestConnection".Translate())) { - _ = TestApiConnection(settings.apiEndpoint); + TestApiConnection(serviceContainer.ApiService, settings.apiEndpoint); } if (listingStandard.ButtonText("AIImages.Settings.LoadFromApi".Translate())) { - _ = LoadAllFromApi(settings); + LoadAllFromApi(serviceContainer.ApiService, settings); } DrawModelDropdown(listingStandard, settings); @@ -343,120 +368,112 @@ namespace AIImages settings.savePath = listingStandard.TextEntry(settings.savePath); } - private static async System.Threading.Tasks.Task TestApiConnection(string endpoint) + private static void TestApiConnection( + IStableDiffusionApiService apiService, + string endpoint + ) { - try - { - Log.Message($"[AI Images] Testing connection to {endpoint}..."); - bool available = await AIImagesMod.ApiService.CheckApiAvailability(endpoint); + _ = AsyncHelper.RunAsync( + async () => + { + Log.Message($"[AI Images] Testing connection to {endpoint}..."); + bool available = await apiService.CheckApiAvailability(endpoint); - if (available) - { - Messages.Message( - "AIImages.Settings.ConnectionSuccess".Translate(), - MessageTypeDefOf.PositiveEvent - ); - } - else - { - Messages.Message( - "AIImages.Settings.ConnectionFailed".Translate(), - MessageTypeDefOf.RejectInput - ); - } - } - catch (Exception ex) - { - Messages.Message($"Error: {ex.Message}", MessageTypeDefOf.RejectInput); - } + if (available) + { + Messages.Message( + "AIImages.Settings.ConnectionSuccess".Translate(), + MessageTypeDefOf.PositiveEvent + ); + } + else + { + Messages.Message( + "AIImages.Settings.ConnectionFailed".Translate(), + MessageTypeDefOf.RejectInput + ); + } + }, + "API Connection Test" + ); } - private static async System.Threading.Tasks.Task LoadAllFromApi( + private static void LoadAllFromApi( + IStableDiffusionApiService apiService, AIImagesModSettings settings ) { - try - { - Log.Message("[AI Images] Loading models, samplers and schedulers from API..."); - - // Загружаем модели - var models = await AIImagesMod.ApiService.GetAvailableModels(settings.apiEndpoint); - settings.availableModels = models; - - // Загружаем семплеры - var samplers = await AIImagesMod.ApiService.GetAvailableSamplers( - settings.apiEndpoint - ); - settings.availableSamplers = samplers; - - // Загружаем schedulers - var schedulers = await AIImagesMod.ApiService.GetAvailableSchedulers( - settings.apiEndpoint - ); - settings.availableSchedulers = schedulers; - - int totalCount = models.Count + samplers.Count + schedulers.Count; - if (totalCount > 0) + _ = AsyncHelper.RunAsync( + async () => { - Messages.Message( - "AIImages.Settings.AllLoaded".Translate( - models.Count, - samplers.Count, - schedulers.Count - ), - MessageTypeDefOf.PositiveEvent - ); + Log.Message("[AI Images] Loading models, samplers and schedulers from API..."); - // Автовыбор модели - if ( - ( - string.IsNullOrEmpty(settings.selectedModel) - || !models.Contains(settings.selectedModel) - ) - && models.Count > 0 - ) - { - settings.selectedModel = models[0]; - } + // Загружаем модели + var models = await apiService.GetAvailableModels(settings.apiEndpoint); + settings.availableModels = models; - // Автовыбор семплера - if ( - ( - string.IsNullOrEmpty(settings.selectedSampler) - || !samplers.Contains(settings.selectedSampler) - ) - && samplers.Count > 0 - ) - { - settings.selectedSampler = samplers[0]; - } + // Загружаем семплеры + var samplers = await apiService.GetAvailableSamplers(settings.apiEndpoint); + settings.availableSamplers = samplers; - // Автовыбор scheduler - if ( - ( - string.IsNullOrEmpty(settings.selectedScheduler) - || !schedulers.Contains(settings.selectedScheduler) - ) - && schedulers.Count > 0 - ) + // Загружаем schedulers + var schedulers = await apiService.GetAvailableSchedulers(settings.apiEndpoint); + settings.availableSchedulers = schedulers; + + int totalCount = models.Count + samplers.Count + schedulers.Count; + if (totalCount > 0) { - settings.selectedScheduler = schedulers[0]; + ShowSuccessMessage(models.Count, samplers.Count, schedulers.Count); + AutoSelectDefaults(settings, models, samplers, schedulers); } - } - else - { - Messages.Message( - "AIImages.Settings.NothingLoaded".Translate(), - MessageTypeDefOf.RejectInput - ); - } - } - catch (Exception ex) + else + { + Messages.Message( + "AIImages.Settings.NothingLoaded".Translate(), + MessageTypeDefOf.RejectInput + ); + } + }, + "Load API Data" + ); + } + + private static void ShowSuccessMessage(int modelCount, int samplerCount, int schedulerCount) + { + Messages.Message( + "AIImages.Settings.AllLoaded".Translate(modelCount, samplerCount, schedulerCount), + MessageTypeDefOf.PositiveEvent + ); + } + + private static void AutoSelectDefaults( + AIImagesModSettings settings, + List models, + List samplers, + List schedulers + ) + { + AutoSelectIfNeeded(ref settings.selectedModel, models, settings.selectedModel); + AutoSelectIfNeeded(ref settings.selectedSampler, samplers, settings.selectedSampler); + AutoSelectIfNeeded( + ref settings.selectedScheduler, + schedulers, + settings.selectedScheduler + ); + } + + private static void AutoSelectIfNeeded( + ref string selectedValue, + List availableValues, + string currentValue + ) + { + bool needsSelection = + string.IsNullOrEmpty(currentValue) || !availableValues.Contains(currentValue); + + if (needsSelection && availableValues.Count > 0) { - Messages.Message( - $"Error loading from API: {ex.Message}", - MessageTypeDefOf.RejectInput - ); + selectedValue = availableValues[0]; } } } diff --git a/Source/AIImages/Validation/SettingsValidator.cs b/Source/AIImages/Validation/SettingsValidator.cs new file mode 100644 index 0000000..0f85e8e --- /dev/null +++ b/Source/AIImages/Validation/SettingsValidator.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections.Generic; +using AIImages.Settings; + +namespace AIImages.Validation +{ + /// + /// Валидатор для настроек мода AI Images + /// + public static class SettingsValidator + { + /// + /// Валидирует все настройки и возвращает список ошибок + /// + public static ValidationResult Validate(AIImagesModSettings settings) + { + var result = new ValidationResult(); + + if (settings == null) + { + result.AddError("Settings object is null"); + return result; + } + + // Валидация API endpoint + ValidateApiEndpoint(settings.apiEndpoint, result); + + // Валидация размеров изображения + ValidateImageDimensions(settings.width, settings.height, result); + + // Валидация steps + ValidateSteps(settings.steps, result); + + // Валидация CFG scale + ValidateCfgScale(settings.cfgScale, result); + + // Валидация sampler и scheduler + ValidateSamplerAndScheduler( + settings.selectedSampler, + settings.selectedScheduler, + result + ); + + // Валидация пути сохранения + ValidateSavePath(settings.savePath, result); + + return result; + } + + private static void ValidateApiEndpoint(string endpoint, ValidationResult result) + { + if (string.IsNullOrWhiteSpace(endpoint)) + { + result.AddError("API endpoint cannot be empty"); + return; + } + + if (!Uri.TryCreate(endpoint, UriKind.Absolute, out Uri uri)) + { + result.AddError($"Invalid API endpoint format: {endpoint}"); + return; + } + + if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) + { + result.AddWarning($"API endpoint should use HTTP or HTTPS protocol: {endpoint}"); + } + + // Проверка на localhost/127.0.0.1 + if ( + uri.Host != "localhost" + && uri.Host != "127.0.0.1" + && !uri.Host.StartsWith("192.168.") + ) + { + result.AddWarning( + "API endpoint is not pointing to a local address. Make sure the API is accessible." + ); + } + } + + private static void ValidateImageDimensions(int width, int height, ValidationResult result) + { + const int minDimension = 64; + const int maxDimension = 2048; + const int recommendedMin = 512; + const int recommendedMax = 1024; + + if (width < minDimension || width > maxDimension) + { + result.AddError( + $"Width must be between {minDimension} and {maxDimension}. Current: {width}" + ); + } + + if (height < minDimension || height > maxDimension) + { + result.AddError( + $"Height must be between {minDimension} and {maxDimension}. Current: {height}" + ); + } + + // Проверка кратности 8 (рекомендация для Stable Diffusion) + if (width % 8 != 0) + { + result.AddWarning( + $"Width should be divisible by 8 for optimal results. Current: {width}" + ); + } + + if (height % 8 != 0) + { + result.AddWarning( + $"Height should be divisible by 8 for optimal results. Current: {height}" + ); + } + + // Предупреждения о производительности + if (width > recommendedMax || height > recommendedMax) + { + result.AddWarning( + $"Large image dimensions ({width}x{height}) may result in slow generation and high memory usage" + ); + } + + if (width < recommendedMin || height < recommendedMin) + { + result.AddWarning( + $"Small image dimensions ({width}x{height}) may result in lower quality" + ); + } + } + + private static void ValidateSteps(int steps, ValidationResult result) + { + const int minSteps = 1; + const int maxSteps = 150; + const int recommendedMin = 20; + const int recommendedMax = 50; + + if (steps < minSteps || steps > maxSteps) + { + result.AddError( + $"Steps must be between {minSteps} and {maxSteps}. Current: {steps}" + ); + } + + if (steps < recommendedMin) + { + result.AddWarning( + $"Low steps value ({steps}) may result in lower quality. Recommended: {recommendedMin}-{recommendedMax}" + ); + } + + if (steps > recommendedMax) + { + result.AddWarning( + $"High steps value ({steps}) may result in slow generation with minimal quality improvement" + ); + } + } + + private static void ValidateCfgScale(float cfgScale, ValidationResult result) + { + const float minCfg = 1.0f; + const float maxCfg = 30.0f; + const float recommendedMin = 5.0f; + const float recommendedMax = 15.0f; + + if (cfgScale < minCfg || cfgScale > maxCfg) + { + result.AddError( + $"CFG Scale must be between {minCfg} and {maxCfg}. Current: {cfgScale}" + ); + } + + if (cfgScale < recommendedMin) + { + result.AddWarning( + $"Low CFG scale ({cfgScale}) may ignore prompt. Recommended: {recommendedMin}-{recommendedMax}" + ); + } + + if (cfgScale > recommendedMax) + { + result.AddWarning( + $"High CFG scale ({cfgScale}) may result in over-saturated or distorted images" + ); + } + } + + private static void ValidateSamplerAndScheduler( + string sampler, + string scheduler, + ValidationResult result + ) + { + if (string.IsNullOrWhiteSpace(sampler)) + { + result.AddWarning("Sampler is not selected. A default sampler will be used."); + } + + if (string.IsNullOrWhiteSpace(scheduler)) + { + result.AddWarning("Scheduler is not selected. A default scheduler will be used."); + } + } + + private static void ValidateSavePath(string savePath, ValidationResult result) + { + if (string.IsNullOrWhiteSpace(savePath)) + { + result.AddError("Save path cannot be empty"); + return; + } + + // Проверка на недопустимые символы в пути + char[] invalidChars = System.IO.Path.GetInvalidPathChars(); + if (savePath.IndexOfAny(invalidChars) >= 0) + { + result.AddError($"Save path contains invalid characters: {savePath}"); + } + } + } + + /// + /// Результат валидации настроек + /// + public class ValidationResult + { + private readonly List _errors = new List(); + private readonly List _warnings = new List(); + + public bool IsValid => _errors.Count == 0; + public bool HasWarnings => _warnings.Count > 0; + public IReadOnlyList Errors => _errors; + public IReadOnlyList Warnings => _warnings; + + public void AddError(string error) + { + if (!string.IsNullOrWhiteSpace(error)) + { + _errors.Add(error); + } + } + + public void AddWarning(string warning) + { + if (!string.IsNullOrWhiteSpace(warning)) + { + _warnings.Add(warning); + } + } + + public string GetErrorsAsString() + { + return string.Join("\n", _errors); + } + + public string GetWarningsAsString() + { + return string.Join("\n", _warnings); + } + + public string GetAllMessagesAsString() + { + var messages = new List(); + + if (_errors.Count > 0) + { + messages.Add("Errors:"); + messages.AddRange(_errors); + } + + if (_warnings.Count > 0) + { + if (messages.Count > 0) + messages.Add(""); + + messages.Add("Warnings:"); + messages.AddRange(_warnings); + } + + return string.Join("\n", messages); + } + } +} diff --git a/Source/AIImages/Window_AIImage.cs b/Source/AIImages/Window_AIImage.cs index 6dfca8a..68093a3 100644 --- a/Source/AIImages/Window_AIImage.cs +++ b/Source/AIImages/Window_AIImage.cs @@ -1,6 +1,9 @@ using System; using System.Linq; +using System.Threading; +using AIImages.Helpers; using AIImages.Models; +using AIImages.Services; using RimWorld; using UnityEngine; using Verse; @@ -31,6 +34,12 @@ namespace AIImages private Texture2D generatedImage; private bool isGenerating = false; private string generationStatus = ""; + private CancellationTokenSource cancellationTokenSource; + + // Сервисы (получаем через DI) + private readonly IPawnDescriptionService pawnDescriptionService; + private readonly IPromptGeneratorService promptGeneratorService; + private readonly IStableDiffusionApiService apiService; public Window_AIImage(Pawn pawn) { @@ -42,6 +51,12 @@ namespace AIImages this.draggable = true; this.preventCameraMotion = false; + // Получаем сервисы через DI контейнер + var services = AIImagesMod.Services; + pawnDescriptionService = services.PawnDescriptionService; + promptGeneratorService = services.PromptGeneratorService; + apiService = services.ApiService; + // Извлекаем данные персонажа RefreshPawnData(); } @@ -57,10 +72,27 @@ namespace AIImages /// private void RefreshPawnData() { - appearanceData = AIImagesMod.PawnDescriptionService.ExtractAppearanceData(pawn); + appearanceData = pawnDescriptionService.ExtractAppearanceData(pawn); generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings(); } + /// + /// Освобождает ресурсы при закрытии окна + /// + public override void PreClose() + { + base.PreClose(); + + // Отменяем генерацию, если она идет + if (isGenerating && cancellationTokenSource != null) + { + cancellationTokenSource.Cancel(); + } + + // Освобождаем CancellationTokenSource + cancellationTokenSource?.Dispose(); + } + /// /// Обновляет текущую пешку в окне /// @@ -101,24 +133,28 @@ namespace AIImages } /// - /// Асинхронная генерация изображения + /// Асинхронная генерация изображения с поддержкой отмены /// - private async System.Threading.Tasks.Task GenerateImage() + private async System.Threading.Tasks.Task GenerateImageAsync() { if (isGenerating) return; + // Создаем новый CancellationTokenSource + cancellationTokenSource?.Dispose(); + cancellationTokenSource = new CancellationTokenSource(); + isGenerating = true; generationStatus = "AIImages.Generation.InProgress".Translate(); try { // Генерируем промпты - string positivePrompt = AIImagesMod.PromptGeneratorService.GeneratePositivePrompt( + string positivePrompt = promptGeneratorService.GeneratePositivePrompt( appearanceData, generationSettings ); - string negativePrompt = AIImagesMod.PromptGeneratorService.GenerateNegativePrompt( + string negativePrompt = promptGeneratorService.GenerateNegativePrompt( generationSettings ); @@ -137,8 +173,11 @@ namespace AIImages Model = AIImagesMod.Settings.apiEndpoint, }; - // Генерируем изображение - var result = await AIImagesMod.ApiService.GenerateImageAsync(request); + // Генерируем изображение с поддержкой отмены + var result = await apiService.GenerateImageAsync( + request, + cancellationTokenSource.Token + ); if (result.Success) { @@ -159,10 +198,19 @@ namespace AIImages Messages.Message(generationStatus, MessageTypeDefOf.RejectInput); } } + catch (OperationCanceledException) + { + generationStatus = "AIImages.Generation.Cancelled".Translate(); + Log.Message("[AI Images] Generation cancelled by user"); + } catch (Exception ex) { generationStatus = $"Error: {ex.Message}"; Log.Error($"[AI Images] Generation error: {ex}"); + Messages.Message( + $"AIImages.Generation.Error".Translate() + ": {ex.Message}", + MessageTypeDefOf.RejectInput + ); } finally { @@ -170,6 +218,26 @@ namespace AIImages } } + /// + /// Запускает генерацию изображения (обертка для безопасного fire-and-forget) + /// + private void StartGeneration() + { + AsyncHelper.FireAndForget(GenerateImageAsync(), "Image Generation"); + } + + /// + /// Отменяет генерацию изображения + /// + private void CancelGeneration() + { + if (isGenerating && cancellationTokenSource != null) + { + cancellationTokenSource.Cancel(); + generationStatus = "AIImages.Generation.Cancelling".Translate(); + } + } + public override void DoWindowContents(Rect inRect) { float curY = 0f; @@ -229,9 +297,7 @@ namespace AIImages contentY += 35f; Text.Font = GameFont.Small; - string appearanceText = AIImagesMod.PawnDescriptionService.GetAppearanceDescription( - pawn - ); + string appearanceText = pawnDescriptionService.GetAppearanceDescription(pawn); float appearanceHeight = Text.CalcHeight(appearanceText, scrollViewRect.width - 30f); Widgets.Label( new Rect(20f, contentY, scrollViewRect.width - 30f, appearanceHeight), @@ -252,7 +318,7 @@ namespace AIImages contentY += 35f; Text.Font = GameFont.Small; - string apparelText = AIImagesMod.PawnDescriptionService.GetApparelDescription(pawn); + string apparelText = pawnDescriptionService.GetApparelDescription(pawn); float apparelHeight = Text.CalcHeight(apparelText, scrollViewRect.width - 30f); Widgets.Label( new Rect(20f, contentY, scrollViewRect.width - 30f, apparelHeight), @@ -308,18 +374,33 @@ namespace AIImages curY += statusHeight + 10f; } - // Кнопка генерации + // Кнопка генерации/отмены Text.Font = GameFont.Small; - if ( - Widgets.ButtonText( - new Rect(rect.x, rect.y + curY, rect.width, 35f), - isGenerating - ? "AIImages.Generation.Generating".Translate() - : "AIImages.Generation.Generate".Translate() - ) && !isGenerating - ) + if (isGenerating) { - _ = GenerateImage(); + // Показываем кнопку отмены во время генерации + if ( + Widgets.ButtonText( + new Rect(rect.x, rect.y + curY, rect.width, 35f), + "AIImages.Generation.Cancel".Translate() + ) + ) + { + CancelGeneration(); + } + } + else + { + // Показываем кнопку генерации + if ( + Widgets.ButtonText( + new Rect(rect.x, rect.y + curY, rect.width, 35f), + "AIImages.Generation.Generate".Translate() + ) + ) + { + StartGeneration(); + } } curY += 40f; @@ -333,7 +414,7 @@ namespace AIImages // Получаем промпт Text.Font = GameFont.Tiny; - string promptText = AIImagesMod.PromptGeneratorService.GeneratePositivePrompt( + string promptText = promptGeneratorService.GeneratePositivePrompt( appearanceData, generationSettings ); @@ -411,9 +492,7 @@ namespace AIImages height += 35f; // Текст внешности - string appearanceText = AIImagesMod.PawnDescriptionService.GetAppearanceDescription( - pawn - ); + string appearanceText = pawnDescriptionService.GetAppearanceDescription(pawn); height += Text.CalcHeight(appearanceText, 400f) + 20f; // Разделитель @@ -423,7 +502,7 @@ namespace AIImages height += 35f; // Текст одежды - string apparelText = AIImagesMod.PawnDescriptionService.GetApparelDescription(pawn); + string apparelText = pawnDescriptionService.GetApparelDescription(pawn); height += Text.CalcHeight(apparelText, 400f) + 20f; // Дополнительный отступ