diff --git a/Assemblies/AIImages.dll b/Assemblies/AIImages.dll index 1045dbb..8b8bc0f 100644 Binary files a/Assemblies/AIImages.dll and b/Assemblies/AIImages.dll differ diff --git a/Languages/English/Keyed/AIImages.xml b/Languages/English/Keyed/AIImages.xml index 28a0bce..3e9adf6 100644 --- a/Languages/English/Keyed/AIImages.xml +++ b/Languages/English/Keyed/AIImages.xml @@ -45,10 +45,16 @@ API Endpoint Test Connection Load Available Models + Model + No model selected + Load models first + Load Samplers & Schedulers Successfully connected to API! Failed to connect to API. Check endpoint and ensure Stable Diffusion WebUI is running. Loaded {0} models from API No models found. Check API connection. + Loaded {0} samplers and {1} schedulers from API + No samplers or schedulers found. Check API connection. Generation Settings Configure image generation parameters Art Style @@ -58,6 +64,7 @@ Width Height Sampler + Schedule Type Prompts Base prompts that will be added to all generations Base Positive Prompt diff --git a/Languages/Russian/Keyed/AIImages.xml b/Languages/Russian/Keyed/AIImages.xml index fcdd1d7..33070b5 100644 --- a/Languages/Russian/Keyed/AIImages.xml +++ b/Languages/Russian/Keyed/AIImages.xml @@ -45,10 +45,16 @@ Адрес API Проверить соединение Загрузить доступные модели + Модель + Модель не выбрана + Сначала загрузите модели + Загрузить сэмплеры и планировщики Успешное подключение к API! Не удалось подключиться к API. Проверьте адрес и убедитесь, что Stable Diffusion WebUI запущен. Загружено {0} моделей из API Модели не найдены. Проверьте подключение к API. + Загружено {0} сэмплеров и {1} планировщиков из API + Сэмплеры и планировщики не найдены. Проверьте подключение к API. Настройки генерации Настройка параметров генерации изображений Художественный стиль @@ -58,6 +64,7 @@ Ширина Высота Сэмплер + Тип планировщика Промпты Базовые промпты, которые будут добавлены ко всем генерациям Базовый позитивный промпт diff --git a/Source/AIImages/Models/GenerationRequest.cs b/Source/AIImages/Models/GenerationRequest.cs index 3843729..057949d 100644 --- a/Source/AIImages/Models/GenerationRequest.cs +++ b/Source/AIImages/Models/GenerationRequest.cs @@ -12,6 +12,7 @@ namespace AIImages.Models public int Width { get; set; } public int Height { get; set; } public string Sampler { get; set; } + public string Scheduler { get; set; } public int Seed { get; set; } public string Model { get; set; } } diff --git a/Source/AIImages/Models/StableDiffusionSettings.cs b/Source/AIImages/Models/StableDiffusionSettings.cs index 634d918..90ffef8 100644 --- a/Source/AIImages/Models/StableDiffusionSettings.cs +++ b/Source/AIImages/Models/StableDiffusionSettings.cs @@ -12,6 +12,7 @@ namespace AIImages.Models public int Width { get; set; } public int Height { get; set; } public string Sampler { get; set; } + public string Scheduler { get; set; } public int Seed { get; set; } public string Model { get; set; } public ArtStyle ArtStyle { get; set; } @@ -24,6 +25,7 @@ namespace AIImages.Models Width = 512; Height = 768; Sampler = "Euler a"; + Scheduler = "Automatic"; Seed = -1; // Случайный seed ArtStyle = ArtStyle.Realistic; PositivePrompt = ""; diff --git a/Source/AIImages/Services/IStableDiffusionApiService.cs b/Source/AIImages/Services/IStableDiffusionApiService.cs index 247175c..3e6617a 100644 --- a/Source/AIImages/Services/IStableDiffusionApiService.cs +++ b/Source/AIImages/Services/IStableDiffusionApiService.cs @@ -28,5 +28,10 @@ namespace AIImages.Services /// Получает список доступных сэмплеров /// Task> GetAvailableSamplers(string apiEndpoint); + + /// + /// Получает список доступных schedulers + /// + Task> GetAvailableSchedulers(string apiEndpoint); } } diff --git a/Source/AIImages/Services/StableDiffusionApiService.cs b/Source/AIImages/Services/StableDiffusionApiService.cs index 34a409a..3bb9373 100644 --- a/Source/AIImages/Services/StableDiffusionApiService.cs +++ b/Source/AIImages/Services/StableDiffusionApiService.cs @@ -50,6 +50,7 @@ namespace AIImages.Services width = request.Width, height = request.Height, sampler_name = request.Sampler, + scheduler = request.Scheduler, seed = request.Seed, save_images = false, send_images = true, @@ -186,6 +187,38 @@ namespace AIImages.Services } } + public async Task> GetAvailableSchedulers(string apiEndpoint) + { + try + { + string endpoint = $"{apiEndpoint}/sdapi/v1/schedulers"; + HttpResponseMessage response = await httpClient.GetAsync(endpoint); + + if (!response.IsSuccessStatusCode) + return GetDefaultSchedulers(); + + string jsonResponse = await response.Content.ReadAsStringAsync(); + var schedulers = JsonConvert.DeserializeObject>(jsonResponse); + + var schedulerNames = new List(); + if (schedulers != null) + { + foreach (var scheduler in schedulers) + { + schedulerNames.Add(scheduler.name); + } + } + + Log.Message($"[AI Images] Found {schedulerNames.Count} schedulers"); + return schedulerNames; + } + catch (Exception ex) + { + Log.Warning($"[AI Images] Failed to load schedulers: {ex.Message}"); + return GetDefaultSchedulers(); + } + } + private List GetDefaultSamplers() { return new List @@ -212,6 +245,19 @@ namespace AIImages.Services }; } + private List GetDefaultSchedulers() + { + return new List + { + "Automatic", + "Uniform", + "Karras", + "Exponential", + "Polyexponential", + "SGM Uniform", + }; + } + // Вспомогательные классы для десериализации JSON ответов #pragma warning disable S3459, S1144 // Properties set by JSON deserializer private sealed class Txt2ImgResponse @@ -229,6 +275,11 @@ namespace AIImages.Services { public string name { get; set; } } + + private sealed class SdScheduler + { + public string name { get; set; } + } #pragma warning restore S3459, S1144 } } diff --git a/Source/AIImages/Settings/AIImagesModSettings.cs b/Source/AIImages/Settings/AIImagesModSettings.cs index 388b62f..e081f14 100644 --- a/Source/AIImages/Settings/AIImagesModSettings.cs +++ b/Source/AIImages/Settings/AIImagesModSettings.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using AIImages.Models; using Verse; @@ -13,6 +14,17 @@ namespace AIImages.Settings public string apiEndpoint = "http://127.0.0.1:7860"; public string selectedModel = ""; public string selectedSampler = "Euler a"; + public string selectedScheduler = "Automatic"; + + // Кэшированные списки из API (не сохраняются) + [Unsaved] + public List availableModels = new List(); + + [Unsaved] + public List availableSamplers = new List(); + + [Unsaved] + public List availableSchedulers = new List(); // Настройки генерации public int steps = 30; @@ -42,6 +54,7 @@ namespace AIImages.Settings Scribe_Values.Look(ref apiEndpoint, "apiEndpoint", "http://127.0.0.1:7860"); Scribe_Values.Look(ref selectedModel, "selectedModel", ""); Scribe_Values.Look(ref selectedSampler, "selectedSampler", "Euler a"); + Scribe_Values.Look(ref selectedScheduler, "selectedScheduler", "Automatic"); Scribe_Values.Look(ref steps, "steps", 30); Scribe_Values.Look(ref cfgScale, "cfgScale", 7.5f); @@ -79,6 +92,7 @@ namespace AIImages.Settings Width = width, Height = height, Sampler = selectedSampler, + Scheduler = selectedScheduler, Seed = seed, Model = selectedModel, ArtStyle = artStyle, diff --git a/Source/AIImages/UI/AIImagesSettingsUI.cs b/Source/AIImages/UI/AIImagesSettingsUI.cs index 6c7cd7f..3efd7c5 100644 --- a/Source/AIImages/UI/AIImagesSettingsUI.cs +++ b/Source/AIImages/UI/AIImagesSettingsUI.cs @@ -21,13 +21,7 @@ namespace AIImages public static void DoSettingsWindowContents(Rect inRect, AIImagesModSettings settings) { - // Инициализируем буферы при первом вызове - if (string.IsNullOrEmpty(stepsBuffer)) - { - stepsBuffer = settings.steps.ToString(); - widthBuffer = settings.width.ToString(); - heightBuffer = settings.height.ToString(); - } + InitializeBuffers(settings); Listing_Standard listingStandard = new Listing_Standard(); Rect viewRect = new Rect(0f, 0f, inRect.width - 20f, 1200f); @@ -35,7 +29,31 @@ namespace AIImages Widgets.BeginScrollView(inRect, ref scrollPosition, viewRect); listingStandard.Begin(viewRect); - // === API Settings === + DrawApiSettings(listingStandard, settings); + DrawGenerationSettings(listingStandard, settings); + DrawSamplerSchedulerSettings(listingStandard, settings); + DrawPromptsSettings(listingStandard, settings); + DrawOptionsSettings(listingStandard, settings); + + listingStandard.End(); + Widgets.EndScrollView(); + } + + private static void InitializeBuffers(AIImagesModSettings settings) + { + if (string.IsNullOrEmpty(stepsBuffer)) + { + stepsBuffer = settings.steps.ToString(); + widthBuffer = settings.width.ToString(); + heightBuffer = settings.height.ToString(); + } + } + + private static void DrawApiSettings( + Listing_Standard listingStandard, + AIImagesModSettings settings + ) + { listingStandard.Label( "AIImages.Settings.ApiSection".Translate(), -1f, @@ -47,21 +65,66 @@ namespace AIImages settings.apiEndpoint = listingStandard.TextEntry(settings.apiEndpoint); listingStandard.Gap(8f); - // Кнопка проверки подключения if (listingStandard.ButtonText("AIImages.Settings.TestConnection".Translate())) { _ = TestApiConnection(settings.apiEndpoint); } - // Кнопка загрузки моделей if (listingStandard.ButtonText("AIImages.Settings.LoadModels".Translate())) { _ = LoadModelsFromApi(settings); } - listingStandard.Gap(12f); + DrawModelDropdown(listingStandard, settings); - // === Generation Settings === + if (listingStandard.ButtonText("AIImages.Settings.LoadSamplersSchedulers".Translate())) + { + _ = LoadSamplersAndSchedulers(settings); + } + + listingStandard.Gap(12f); + } + + private static void DrawModelDropdown( + Listing_Standard listingStandard, + AIImagesModSettings settings + ) + { + if ( + listingStandard.ButtonTextLabeled( + "AIImages.Settings.Model".Translate(), + string.IsNullOrEmpty(settings.selectedModel) + ? "AIImages.Settings.NoModelSelected".Translate() + : settings.selectedModel + ) + ) + { + List modelOptions = new List(); + if (!settings.availableModels.Any()) + { + modelOptions.Add( + new FloatMenuOption("AIImages.Settings.LoadModelsFirst".Translate(), null) + ); + } + else + { + foreach (string model in settings.availableModels) + { + string localModel = model; + modelOptions.Add( + new FloatMenuOption(model, () => settings.selectedModel = localModel) + ); + } + } + Find.WindowStack.Add(new FloatMenu(modelOptions)); + } + } + + private static void DrawGenerationSettings( + Listing_Standard listingStandard, + AIImagesModSettings settings + ) + { listingStandard.Label( "AIImages.Settings.GenerationSection".Translate(), -1f, @@ -69,7 +132,28 @@ namespace AIImages ); listingStandard.GapLine(); - // Art Style + DrawArtStyleDropdown(listingStandard, settings); + listingStandard.Gap(8f); + + listingStandard.Label("AIImages.Settings.Steps".Translate() + $": {settings.steps}"); + settings.steps = (int)listingStandard.Slider(settings.steps, 1, 150); + listingStandard.Gap(8f); + + listingStandard.Label( + "AIImages.Settings.CfgScale".Translate() + $": {settings.cfgScale:F1}" + ); + settings.cfgScale = listingStandard.Slider(settings.cfgScale, 1f, 30f); + listingStandard.Gap(8f); + + DrawSizeSettings(listingStandard, settings); + listingStandard.Gap(12f); + } + + private static void DrawArtStyleDropdown( + Listing_Standard listingStandard, + AIImagesModSettings settings + ) + { if ( listingStandard.ButtonTextLabeled( "AIImages.Settings.ArtStyle".Translate(), @@ -87,22 +171,13 @@ namespace AIImages } Find.WindowStack.Add(new FloatMenu(styleOptions)); } + } - listingStandard.Gap(8f); - - // Steps - listingStandard.Label("AIImages.Settings.Steps".Translate() + $": {settings.steps}"); - settings.steps = (int)listingStandard.Slider(settings.steps, 1, 150); - listingStandard.Gap(8f); - - // CFG Scale - listingStandard.Label( - "AIImages.Settings.CfgScale".Translate() + $": {settings.cfgScale:F1}" - ); - settings.cfgScale = listingStandard.Slider(settings.cfgScale, 1f, 30f); - listingStandard.Gap(8f); - - // Width + private static void DrawSizeSettings( + Listing_Standard listingStandard, + AIImagesModSettings settings + ) + { listingStandard.Label("AIImages.Settings.Width".Translate() + ":"); widthBuffer = listingStandard.TextEntry(widthBuffer); if (int.TryParse(widthBuffer, out int width)) @@ -110,7 +185,6 @@ namespace AIImages settings.width = Mathf.Clamp(width, 64, 2048); } - // Height listingStandard.Label("AIImages.Settings.Height".Translate() + ":"); heightBuffer = listingStandard.TextEntry(heightBuffer); if (int.TryParse(heightBuffer, out int height)) @@ -118,41 +192,119 @@ namespace AIImages settings.height = Mathf.Clamp(height, 64, 2048); } - // Common size presets + DrawSizePresets(listingStandard, settings); + } + + private static void DrawSizePresets( + Listing_Standard listingStandard, + AIImagesModSettings settings + ) + { listingStandard.Gap(4f); - Rect presetRect = listingStandard.GetRect(30f); - if (Widgets.ButtonText(new Rect(presetRect.x, presetRect.y, 80f, 30f), "512x512")) + Rect presetRect1 = listingStandard.GetRect(30f); + DrawPresetButton(presetRect1, 0f, "512x512", 512, 512, settings); + DrawPresetButton(presetRect1, 85f, "512x768", 512, 768, settings); + DrawPresetButton(presetRect1, 170f, "768x768", 768, 768, settings); + + listingStandard.Gap(4f); + Rect presetRect2 = listingStandard.GetRect(30f); + DrawPresetButton(presetRect2, 0f, "896x1152", 896, 1152, settings, 90f); + DrawPresetButton(presetRect2, 95f, "1024x1024", 1024, 1024, settings, 90f); + } + + private static void DrawPresetButton( + Rect rect, + float xOffset, + string label, + int width, + int height, + AIImagesModSettings settings, + float buttonWidth = 80f + ) + { + if (Widgets.ButtonText(new Rect(rect.x + xOffset, rect.y, buttonWidth, 30f), label)) { - settings.width = 512; - settings.height = 512; - widthBuffer = "512"; - heightBuffer = "512"; - } - if (Widgets.ButtonText(new Rect(presetRect.x + 85f, presetRect.y, 80f, 30f), "512x768")) - { - settings.width = 512; - settings.height = 768; - widthBuffer = "512"; - heightBuffer = "768"; + settings.width = width; + settings.height = height; + widthBuffer = width.ToString(); + heightBuffer = height.ToString(); } + } + + private static void DrawSamplerSchedulerSettings( + Listing_Standard listingStandard, + AIImagesModSettings settings + ) + { + DrawSamplerDropdown(listingStandard, settings); + DrawSchedulerDropdown(listingStandard, settings); + listingStandard.Gap(12f); + } + + private static void DrawSamplerDropdown( + Listing_Standard listingStandard, + AIImagesModSettings settings + ) + { if ( - Widgets.ButtonText(new Rect(presetRect.x + 170f, presetRect.y, 80f, 30f), "768x768") + listingStandard.ButtonTextLabeled( + "AIImages.Settings.Sampler".Translate(), + settings.selectedSampler + ) ) { - settings.width = 768; - settings.height = 768; - widthBuffer = "768"; - heightBuffer = "768"; + List samplerOptions = new List(); + var availableSamplers = settings.availableSamplers.Any() + ? settings.availableSamplers + : new List { settings.selectedSampler }; + + foreach (string sampler in availableSamplers) + { + string localSampler = sampler; + samplerOptions.Add( + new FloatMenuOption(sampler, () => settings.selectedSampler = localSampler) + ); + } + Find.WindowStack.Add(new FloatMenu(samplerOptions)); } + } - listingStandard.Gap(12f); + private static void DrawSchedulerDropdown( + Listing_Standard listingStandard, + AIImagesModSettings settings + ) + { + if ( + listingStandard.ButtonTextLabeled( + "AIImages.Settings.Scheduler".Translate(), + settings.selectedScheduler + ) + ) + { + List schedulerOptions = new List(); + var availableSchedulers = settings.availableSchedulers.Any() + ? settings.availableSchedulers + : new List { settings.selectedScheduler }; - // Sampler - listingStandard.Label("AIImages.Settings.Sampler".Translate() + ":"); - settings.selectedSampler = listingStandard.TextEntry(settings.selectedSampler); - listingStandard.Gap(12f); + foreach (string scheduler in availableSchedulers) + { + string localScheduler = scheduler; + schedulerOptions.Add( + new FloatMenuOption( + scheduler, + () => settings.selectedScheduler = localScheduler + ) + ); + } + Find.WindowStack.Add(new FloatMenu(schedulerOptions)); + } + } - // === Prompts === + private static void DrawPromptsSettings( + Listing_Standard listingStandard, + AIImagesModSettings settings + ) + { listingStandard.Label( "AIImages.Settings.PromptsSection".Translate(), -1f, @@ -167,8 +319,13 @@ namespace AIImages listingStandard.Label("AIImages.Settings.BaseNegativePrompt".Translate() + ":"); settings.baseNegativePrompt = listingStandard.TextEntry(settings.baseNegativePrompt, 3); listingStandard.Gap(12f); + } - // === Options === + private static void DrawOptionsSettings( + Listing_Standard listingStandard, + AIImagesModSettings settings + ) + { listingStandard.Label("AIImages.Settings.OptionsSection".Translate()); listingStandard.GapLine(); @@ -187,12 +344,8 @@ namespace AIImages listingStandard.Gap(12f); - // Save path listingStandard.Label("AIImages.Settings.SavePath".Translate() + ":"); settings.savePath = listingStandard.TextEntry(settings.savePath); - - listingStandard.End(); - Widgets.EndScrollView(); } private static async System.Threading.Tasks.Task TestApiConnection(string endpoint) @@ -231,6 +384,7 @@ namespace AIImages { Log.Message("[AI Images] Loading models from API..."); var models = await AIImagesMod.ApiService.GetAvailableModels(settings.apiEndpoint); + settings.availableModels = models; if (models.Count > 0) { @@ -239,8 +393,13 @@ namespace AIImages MessageTypeDefOf.PositiveEvent ); - // Если модель не выбрана, выбираем первую - if (string.IsNullOrEmpty(settings.selectedModel) && models.Count > 0) + if ( + ( + string.IsNullOrEmpty(settings.selectedModel) + || !models.Contains(settings.selectedModel) + ) + && models.Count > 0 + ) { settings.selectedModel = models[0]; } @@ -261,5 +420,73 @@ namespace AIImages ); } } + + private static async System.Threading.Tasks.Task LoadSamplersAndSchedulers( + AIImagesModSettings settings + ) + { + try + { + Log.Message("[AI Images] Loading samplers and schedulers from API..."); + + var samplers = await AIImagesMod.ApiService.GetAvailableSamplers( + settings.apiEndpoint + ); + settings.availableSamplers = samplers; + + var schedulers = await AIImagesMod.ApiService.GetAvailableSchedulers( + settings.apiEndpoint + ); + settings.availableSchedulers = schedulers; + + int totalCount = samplers.Count + schedulers.Count; + if (totalCount > 0) + { + Messages.Message( + "AIImages.Settings.SamplersSchedulersLoaded".Translate( + samplers.Count, + schedulers.Count + ), + MessageTypeDefOf.PositiveEvent + ); + + if ( + ( + string.IsNullOrEmpty(settings.selectedSampler) + || !samplers.Contains(settings.selectedSampler) + ) + && samplers.Count > 0 + ) + { + settings.selectedSampler = samplers[0]; + } + + if ( + ( + string.IsNullOrEmpty(settings.selectedScheduler) + || !schedulers.Contains(settings.selectedScheduler) + ) + && schedulers.Count > 0 + ) + { + settings.selectedScheduler = schedulers[0]; + } + } + else + { + Messages.Message( + "AIImages.Settings.NoSamplersSchedulersFound".Translate(), + MessageTypeDefOf.RejectInput + ); + } + } + catch (Exception ex) + { + Messages.Message( + $"Error loading samplers/schedulers: {ex.Message}", + MessageTypeDefOf.RejectInput + ); + } + } } } diff --git a/Source/AIImages/Window_AIImage.cs b/Source/AIImages/Window_AIImage.cs index 8476fdd..0719248 100644 --- a/Source/AIImages/Window_AIImage.cs +++ b/Source/AIImages/Window_AIImage.cs @@ -131,6 +131,7 @@ namespace AIImages Width = generationSettings.Width, Height = generationSettings.Height, Sampler = generationSettings.Sampler, + Scheduler = generationSettings.Scheduler, Seed = generationSettings.Seed, Model = AIImagesMod.Settings.apiEndpoint, };