diff --git a/CHANGELOG.md b/CHANGELOG.md index 7123cbfac..71d082e62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,23 @@ All notable changes to Stability Matrix will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html). -## [Unreleased - v2.15.8] +## v2.15.8 +### Added +- Added support for the civitai.red (mature-content) domain β€” NSFW CivitAI links now open and copy as civitai.red URLs, and pasting a civitai.red URL into the CivitAI model browser search works the same as a civitai.com URL +### Changed +- The CivitAI base model type filter now uses CivitAI's official `/api/v1/enums` endpoint, with fallbacks to the previous technique and a built-in list, so the filter stays populated even if the CivitAI response format changes or the service is unreachable ### Fixed - Fixed [#1608](https://github.com/LykosAI/StabilityMatrix/issues/1608) - Crash when cdn fetch fails due to error notification not being shown on UI Thread - thanks to @NeuralFault! +- Fixed CivitAI model browsing breaking during Discovery API outages β€” the browser now falls back to the direct CivitAI API when Discovery returns a server error, authentication failure, or times out +- Fixed SwarmUI user settings (theme, output format, server configuration, etc.) and any user-added backend entries being overwritten when the install flow ran over an existing install β€” `Settings.fds` and `Backends.fds` are now merged with their existing contents instead of being rewritten from a stale template +- Fixed pip requirements handling for environment-marker dependencies - thanks to @NeuralFault! +- Fixed [#1608](https://github.com/LykosAI/StabilityMatrix/issues/1608) - Crash when cdn fetch fails due to error notification not being shown on UI Thread - thanks to @NeuralFault! +- Fixed ComfyUI-Zluda inheriting `--enable-manager` from the base ComfyUI launch options, which blocked the bundled custom-node manager from initializing - thanks to @NeuralFault! +### Supporters +#### 🌟 Visionaries +Heaps of gratitude to our Visionaries β€” **Waterclouds**, **bluepopsicle**, **Ibixat**, **Droolguy**, **snotty**, **LG**, and **whudunit** β€” for sticking with us release after release. Your encouragement, your patience while we chase down those last bugs, and the sheer fact of you being here keeps us showing up at the keyboard. We're so glad you're part of this little corner of the internet with us. And big warm welcomes again to our newest Visionaries **MrMxyzptlk12836**, **Psilocyfer18731**, **KalAbaddon**, **RustCupcake**, and **moon_milky2843** β€” make yourselves at home, you're among friends! πŸ’› +#### πŸš€ Pioneers +And the Pioneer crew β€” what a lineup. A massive thank-you to **Szir777**, **[USA]TechDude**, **takyamtom**, **SinthCore**, **Commissar Lord Death**, **Ahmed S**, **SeraphOfSalem**, and **Jisuren** β€” your steady presence, kind words, and patience as we've shifted things around mean more than you know. A heartfelt welcome back to **Tigon**, who's returned to the Pioneer ranks after a little time away β€” so glad you're back. πŸŽ‰ And a special hello to **jweg79**, who's been quietly supporting us for a while and just decided to step up and join the Pioneer crew this round β€” so happy to have you here. To our newest Pioneers, an enormous welcome: **rwx14662**, **Hurbie53**, **ahnhj.al**, **drew.lukas**, **Firelight**, **joeto332987**, **Tuskaruho**, **Cjloha**, **Alligator1907**, **Bitti**, **damianpointdexter**, and **tmdcks**! We're absolutely thrilled to have you with us. (And to our anonymous Pioneer out there too, our thanks reaches you β€” we see you. πŸ’›) ## v2.15.7 ### Added diff --git a/Directory.Packages.props b/Directory.Packages.props index 164562047..14459b259 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,7 +24,7 @@ - + @@ -133,4 +133,4 @@ - \ No newline at end of file + diff --git a/StabilityMatrix.Avalonia/Services/CivitBaseModelTypeService.cs b/StabilityMatrix.Avalonia/Services/CivitBaseModelTypeService.cs index bf9ef25e3..6f9c5ca29 100644 --- a/StabilityMatrix.Avalonia/Services/CivitBaseModelTypeService.cs +++ b/StabilityMatrix.Avalonia/Services/CivitBaseModelTypeService.cs @@ -18,6 +18,65 @@ ILiteDbContext dbContext { private const string CacheId = "BaseModelTypes"; private static readonly TimeSpan CacheExpiration = TimeSpan.FromHours(24); + private const string LegacyBaseModelListProbe = "gimmethelist"; + private static readonly IReadOnlyList KnownVisibleBaseModelTypes = + [ + "Anima", + "AuraFlow", + "Chroma", + "CogVideoX", + "Flux.1 D", + "Flux.1 Kontext", + "Flux.1 Krea", + "Flux.1 S", + "Flux.2 D", + "Flux.2 Klein 4B", + "Flux.2 Klein 4B-base", + "Flux.2 Klein 9B", + "Flux.2 Klein 9B-base", + "Grok", + "HiDream", + "Hunyuan 1", + "Hunyuan Video", + "Illustrious", + "Kolors", + "LTXV", + "LTXV 2.3", + "LTXV2", + "Lumina", + "Mochi", + "NoobAI", + "Other", + "PixArt E", + "PixArt a", + "Pony", + "Pony V7", + "Qwen", + "Qwen 2", + "SD 1.4", + "SD 1.5", + "SD 1.5 Hyper", + "SD 1.5 LCM", + "SD 2.0", + "SD 2.1", + "SDXL 1.0", + "SDXL Hyper", + "SDXL Lightning", + "Upscaler", + "Wan Image 2.7", + "Wan Video 1.3B t2v", + "Wan Video 14B i2v 480p", + "Wan Video 14B i2v 720p", + "Wan Video 14B t2v", + "Wan Video 2.2 I2V-A14B", + "Wan Video 2.2 T2V-A14B", + "Wan Video 2.2 TI2V-5B", + "Wan Video 2.5 I2V", + "Wan Video 2.5 T2V", + "Wan Video 2.7", + "ZImageBase", + "ZImageTurbo", + ]; /// /// Gets the list of base model types, using cache if available and not expired @@ -39,15 +98,11 @@ public async Task> GetBaseModelTypes(bool forceRefresh = false, boo { if (civitBaseModels.Count <= 0) { - var baseModelsResponse = await civitApi.GetBaseModelList(); - var jsonContent = await baseModelsResponse.Content.ReadAsStringAsync(); - var baseModels = JsonNode.Parse(jsonContent); + civitBaseModels = + await TryGetBaseModelsFromEnumsEndpoint() ?? await TryGetLegacyBaseModelList() ?? []; - var innerJson = baseModels?["error"]?["message"]?.GetValue(); - var jArray = JsonNode.Parse(innerJson).AsArray(); - var baseModelValues = jArray[0]?["errors"]?[0]?[0]?["values"]?.AsArray(); - - civitBaseModels = baseModelValues?.GetValues().ToList() ?? []; + civitBaseModels = + civitBaseModels.Count > 0 ? civitBaseModels : GetKnownVisibleBaseModelTypes(); // Cache the results var cacheEntry = new CivitBaseModelTypeCacheEntry @@ -60,18 +115,7 @@ public async Task> GetBaseModelTypes(bool forceRefresh = false, boo await dbContext.UpsertCivitBaseModelTypeCacheEntry(cacheEntry); } - if (includeAllOption) - { - civitBaseModels.Insert(0, CivitBaseModelType.All.ToString()); - } - - // Filter and sort results - var filteredResults = civitBaseModels - .Where(s => !s.Equals("odor", StringComparison.OrdinalIgnoreCase)) - .OrderBy(s => s) - .ToList(); - - return filteredResults; + return NormalizeBaseModelTypes(civitBaseModels, includeAllOption); } catch (Exception e) { @@ -79,8 +123,10 @@ public async Task> GetBaseModelTypes(bool forceRefresh = false, boo // Return cached results if available, even if expired var expiredCache = await dbContext.GetCivitBaseModelTypeCacheEntry(CacheId); - return expiredCache?.ModelTypes - ?? Enum.GetValues().Select(b => b.GetStringValue()).ToList(); + return NormalizeBaseModelTypes( + expiredCache?.ModelTypes ?? GetKnownVisibleBaseModelTypes(), + includeAllOption + ); } } @@ -91,4 +137,82 @@ public void ClearCache() { dbContext.CivitBaseModelTypeCache.DeleteAllAsync(); } + + private async Task?> TryGetBaseModelsFromEnumsEndpoint() + { + try + { + var enumsResponse = await civitApi.GetEnums(); + var baseModels = enumsResponse?.ActiveBaseModel ?? enumsResponse?.BaseModel; + + if (baseModels is { Count: > 0 }) + { + return baseModels; + } + + logger.LogInformation( + "CivitAI enums endpoint returned no base models; falling back to legacy/base list" + ); + return null; + } + catch (Exception ex) + { + logger.LogInformation(ex, "CivitAI enums endpoint failed; falling back to legacy/base list"); + return null; + } + } + + private async Task?> TryGetLegacyBaseModelList() + { + var baseModelsResponse = await civitApi.GetBaseModelList(); + var jsonContent = await baseModelsResponse.Content.ReadAsStringAsync(); + return TryParseLegacyBaseModelList(jsonContent); + } + + private List? TryParseLegacyBaseModelList(string jsonContent) + { + var baseModels = JsonNode.Parse(jsonContent); + var innerJson = baseModels?["error"]?["message"]?.GetValue(); + if (string.IsNullOrWhiteSpace(innerJson)) + { + logger.LogInformation( + "CivitAI base model probe value '{Probe}' no longer returns the legacy validation payload; using built-in base model list", + LegacyBaseModelListProbe + ); + return null; + } + + var jArray = JsonNode.Parse(innerJson)?.AsArray(); + var baseModelValues = jArray?[0]?["errors"]?[0]?[0]?["values"]?.AsArray(); + return baseModelValues?.GetValues().ToList(); + } + + private static List GetKnownVisibleBaseModelTypes() + { + return KnownVisibleBaseModelTypes.ToList(); + } + + private static List NormalizeBaseModelTypes( + IEnumerable? baseModels, + bool includeAllOption + ) + { + var normalized = (baseModels ?? []) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Where(s => !s.Equals("odor", StringComparison.OrdinalIgnoreCase)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(s => s) + .ToList(); + + normalized.RemoveAll(s => + s.Equals(CivitBaseModelType.All.ToString(), StringComparison.OrdinalIgnoreCase) + ); + + if (includeAllOption) + { + normalized.Insert(0, CivitBaseModelType.All.ToString()); + } + + return normalized; + } } diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs index d8480cc34..5eccf5dc1 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs @@ -191,7 +191,7 @@ private void UpdateImage() [RelayCommand] private void OpenModel() { - ProcessRunner.OpenUrl($"https://civitai.com/models/{CivitModel.Id}"); + ProcessRunner.OpenUrl(CivitaiUrlHelper.GetModelUrl(CivitModel.Id, CivitModel.Nsfw)); } [RelayCommand] diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs index 0c60c7e4c..bdcd4bcf8 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs @@ -719,22 +719,18 @@ private async Task SearchModels(bool isInfiniteScroll = false) modelRequest.Sort = CivitSortMode.HighestRated; } } - else if (SearchQuery.StartsWith("https://civitai.com/models/")) + else if (CivitaiUrlHelper.TryParseModelId(SearchQuery, out var modelId)) { /* extract model ID from URL, could be one of: https://civitai.com/models/443821?modelVersionId=1957537 + https://civitai.red/models/443821?modelVersionId=1957537 https://civitai.com/models/443821/cyberrealistic-pony https://civitai.com/models/443821 */ - var modelId = SearchQuery - .Replace("https://civitai.com/models/", string.Empty) - .Split(['?', '/'], StringSplitOptions.RemoveEmptyEntries) - .FirstOrDefault(); - modelRequest.Period = CivitPeriod.AllTime; modelRequest.BaseModels = null; modelRequest.Types = null; - modelRequest.CommaSeparatedModelIds = modelId; + modelRequest.CommaSeparatedModelIds = modelId.ToString(); if (modelRequest.Sort is CivitSortMode.Favorites or CivitSortMode.Installed) { diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitDetailsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitDetailsPageViewModel.cs index d308160e8..e8143d9cc 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitDetailsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitDetailsPageViewModel.cs @@ -55,7 +55,7 @@ IModelImportService modelImportService ) : DisposableViewModelBase { [ObservableProperty] - [NotifyPropertyChangedFor(nameof(ShowInferenceDefaultsSection))] + [NotifyPropertyChangedFor(nameof(ShowInferenceDefaultsSection), nameof(CivitUrl))] public required partial CivitModel CivitModel { get; set; } [ObservableProperty] @@ -110,7 +110,8 @@ IModelImportService modelImportService nameof(ShortSha256), nameof(BaseModelType), nameof(ModelFileNameFormat), - nameof(IsEarlyAccess) + nameof(IsEarlyAccess), + nameof(CivitUrl) )] public partial ModelVersionViewModel? SelectedVersion { get; set; } @@ -172,7 +173,8 @@ IModelImportService modelImportService public bool IsEarlyAccess => SelectedVersion?.ModelVersion.IsEarlyAccess ?? false; - public string CivitUrl => $@"https://civitai.com/models/{CivitModel.Id}"; + public string CivitUrl => + CivitaiUrlHelper.GetModelUrl(CivitModel.Id, CivitModel.Nsfw, SelectedVersion?.ModelVersion.Id); public int DescriptionRowSpan => string.IsNullOrWhiteSpace(ModelVersionDescription) ? 3 : 1; diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFileViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFileViewModel.cs index 251cf3bdf..24b088811 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFileViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFileViewModel.cs @@ -133,7 +133,13 @@ private void OpenOnCivitAi() { if (CheckpointFile.ConnectedModelInfo?.ModelId == null) return; - ProcessRunner.OpenUrl($"https://civitai.com/models/{CheckpointFile.ConnectedModelInfo.ModelId}"); + ProcessRunner.OpenUrl( + CivitaiUrlHelper.GetModelUrl( + CheckpointFile.ConnectedModelInfo.ModelId.Value, + CheckpointFile.ConnectedModelInfo.Nsfw, + CheckpointFile.ConnectedModelInfo.VersionId + ) + ); } [RelayCommand] @@ -149,7 +155,11 @@ private Task CopyModelUrl() Task.CompletedTask, ConnectedModelSource.Civitai when CheckpointFile.ConnectedModelInfo.ModelId != null => App.Clipboard.SetTextAsync( - $"https://civitai.com/models/{CheckpointFile.ConnectedModelInfo.ModelId}" + CivitaiUrlHelper.GetModelUrl( + CheckpointFile.ConnectedModelInfo.ModelId.Value, + CheckpointFile.ConnectedModelInfo.Nsfw, + CheckpointFile.ConnectedModelInfo.VersionId + ) ), ConnectedModelSource.OpenModelDb => App.Clipboard.SetTextAsync( diff --git a/StabilityMatrix.Core/Api/CivitCompatApiManager.cs b/StabilityMatrix.Core/Api/CivitCompatApiManager.cs index 89986e078..f689add9e 100644 --- a/StabilityMatrix.Core/Api/CivitCompatApiManager.cs +++ b/StabilityMatrix.Core/Api/CivitCompatApiManager.cs @@ -1,5 +1,6 @@ ο»Ώusing Injectio.Attributes; using Microsoft.Extensions.Logging; +using Refit; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Services; @@ -22,13 +23,52 @@ ISettingsManager settingsManager public Task GetModels(CivitModelsRequest request) { - if (ShouldUseDiscoveryApi) + return GetModelsInternal(request); + } + + private async Task GetModelsInternal(CivitModelsRequest request) + { + if (!ShouldUseDiscoveryApi) { - logger.LogDebug($"Using Discovery API for {nameof(GetModels)}"); - return discoveryApi.GetModels(request, transcodeAnimToImage: true, transcodeVideoToImage: true); + return await civitApi.GetModels(request).ConfigureAwait(false); } - return civitApi.GetModels(request); + try + { + logger.LogDebug("Using Discovery API for {Method}", nameof(GetModels)); + return await discoveryApi + .GetModels(request, transcodeAnimToImage: true, transcodeVideoToImage: true) + .ConfigureAwait(false); + } + catch (ApiException ex) + when ((int)ex.StatusCode >= 500 || ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + logger.LogWarning( + ex, + "Discovery API failed for {Method} with {StatusCode}; falling back to direct CivitAI API", + nameof(GetModels), + ex.StatusCode + ); + return await civitApi.GetModels(request).ConfigureAwait(false); + } + catch (HttpRequestException ex) + { + logger.LogWarning( + ex, + "Discovery API request failed for {Method}; falling back to direct CivitAI API", + nameof(GetModels) + ); + return await civitApi.GetModels(request).ConfigureAwait(false); + } + catch (TaskCanceledException ex) + { + logger.LogWarning( + ex, + "Discovery API timed out for {Method}; falling back to direct CivitAI API", + nameof(GetModels) + ); + return await civitApi.GetModels(request).ConfigureAwait(false); + } } public Task GetModelById(int id) @@ -51,6 +91,11 @@ public Task GetModelVersionById(int id) return civitApi.GetModelVersionById(id); } + public Task GetEnums() + { + return civitApi.GetEnums(); + } + public Task GetBaseModelList() { return civitApi.GetBaseModelList(); diff --git a/StabilityMatrix.Core/Api/ICivitApi.cs b/StabilityMatrix.Core/Api/ICivitApi.cs index 55397d58f..a319b76b3 100644 --- a/StabilityMatrix.Core/Api/ICivitApi.cs +++ b/StabilityMatrix.Core/Api/ICivitApi.cs @@ -19,6 +19,9 @@ public interface ICivitApi [Get("/api/v1/model-versions/{id}")] Task GetModelVersionById(int id); + [Get("/api/v1/enums")] + Task GetEnums(); + [Get("/api/v1/models?baseModels=gimmethelist")] Task GetBaseModelList(); } diff --git a/StabilityMatrix.Core/Helper/CivitaiUrlHelper.cs b/StabilityMatrix.Core/Helper/CivitaiUrlHelper.cs new file mode 100644 index 000000000..c7ceb4282 --- /dev/null +++ b/StabilityMatrix.Core/Helper/CivitaiUrlHelper.cs @@ -0,0 +1,44 @@ +namespace StabilityMatrix.Core.Helper; + +public static class CivitaiUrlHelper +{ + public const string SafeHost = "civitai.com"; + public const string MatureHost = "civitai.red"; + + public static string GetModelUrl(int modelId, bool isNsfw, int? modelVersionId = null) + { + var baseUrl = $"https://{GetHost(isNsfw)}/models/{modelId}"; + return modelVersionId is > 0 ? $"{baseUrl}?modelVersionId={modelVersionId}" : baseUrl; + } + + public static bool TryParseModelId(string? url, out int modelId) + { + modelId = 0; + + if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + return false; + } + + if ( + !uri.Host.Equals(SafeHost, StringComparison.OrdinalIgnoreCase) + && !uri.Host.Equals(MatureHost, StringComparison.OrdinalIgnoreCase) + && !uri.Host.Equals($"www.{SafeHost}", StringComparison.OrdinalIgnoreCase) + && !uri.Host.Equals($"www.{MatureHost}", StringComparison.OrdinalIgnoreCase) + ) + { + return false; + } + + var segments = uri.AbsolutePath.Trim('/').Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries); + + return segments.Length >= 2 + && segments[0].Equals("models", StringComparison.OrdinalIgnoreCase) + && int.TryParse(segments[1], out modelId); + } + + private static string GetHost(bool isNsfw) + { + return isNsfw ? MatureHost : SafeHost; + } +} diff --git a/StabilityMatrix.Core/Models/Api/CivitEnumsResponse.cs b/StabilityMatrix.Core/Models/Api/CivitEnumsResponse.cs new file mode 100644 index 000000000..51633ea96 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/CivitEnumsResponse.cs @@ -0,0 +1,12 @@ +ο»Ώusing System.Text.Json.Serialization; + +namespace StabilityMatrix.Core.Models.Api; + +public class CivitEnumsResponse +{ + [JsonPropertyName("ActiveBaseModel")] + public List? ActiveBaseModel { get; init; } + + [JsonPropertyName("BaseModel")] + public List? BaseModel { get; init; } +} diff --git a/StabilityMatrix.Core/Models/Packages/ComfyZluda.cs b/StabilityMatrix.Core/Models/Packages/ComfyZluda.cs index 8cee6ce84..e3a8a3d98 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyZluda.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyZluda.cs @@ -102,7 +102,11 @@ public override List LaunchOptions }, }; - options.AddRange(base.LaunchOptions.Where(x => x.Name != "Cross Attention Method")); + options.AddRange( + base.LaunchOptions.Where(x => + x.Name != "Cross Attention Method" && !x.Options.Contains("--enable-manager") + ) + ); return options; } } diff --git a/StabilityMatrix.Core/Models/Packages/StableSwarm.cs b/StabilityMatrix.Core/Models/Packages/StableSwarm.cs index d71e52f5c..80af79da6 100644 --- a/StabilityMatrix.Core/Models/Packages/StableSwarm.cs +++ b/StabilityMatrix.Core/Models/Packages/StableSwarm.cs @@ -263,44 +263,54 @@ await prerequisiteHelper if (!options.IsUpdate) { - // set default settings - var settings = new StableSwarmSettings { IsInstalled = true }; + // Settings.fds - merge into the existing file instead of replacing it, so user edits + // (and any fields added by newer SwarmUI versions) survive a re-install. + var settingsPath = GetSettingsPath(installLocation); + Directory.CreateDirectory(Path.GetDirectoryName(settingsPath)!); + var settingsSection = File.Exists(settingsPath) + ? FDSUtility.ReadFile(settingsPath) + : new FDSSection(); + + settingsSection.Set("IsInstalled", true); if (options.SharedFolderMethod is SharedFolderMethod.Configuration) { - settings.Paths = new StableSwarmSettings.PathsData - { - ModelRoot = settingsManager.ModelsDirectory, - SDModelFolder = Path.Combine( - settingsManager.ModelsDirectory, - SharedFolderType.StableDiffusion.ToString() - ), - SDLoraFolder = Path.Combine( - settingsManager.ModelsDirectory, - SharedFolderType.Lora.ToString() - ), - SDVAEFolder = Path.Combine( - settingsManager.ModelsDirectory, - SharedFolderType.VAE.ToString() - ), - SDEmbeddingFolder = Path.Combine( - settingsManager.ModelsDirectory, - SharedFolderType.Embeddings.ToString() - ), - SDControlNetsFolder = Path.Combine( - settingsManager.ModelsDirectory, - SharedFolderType.ControlNet.ToString() - ), - SDClipVisionFolder = Path.Combine( - settingsManager.ModelsDirectory, - SharedFolderType.ClipVision.ToString() - ), - }; + var pathsSection = settingsSection.GetSection("Paths") ?? new FDSSection(); + pathsSection.Set("ModelRoot", settingsManager.ModelsDirectory); + pathsSection.Set( + "SDModelFolder", + Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.StableDiffusion.ToString()) + ); + pathsSection.Set( + "SDLoraFolder", + Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.Lora.ToString()) + ); + pathsSection.Set( + "SDVAEFolder", + Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.VAE.ToString()) + ); + pathsSection.Set( + "SDEmbeddingFolder", + Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.Embeddings.ToString()) + ); + pathsSection.Set( + "SDControlNetsFolder", + Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.ControlNet.ToString()) + ); + pathsSection.Set( + "SDClipVisionFolder", + Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.ClipVision.ToString()) + ); + settingsSection.Set("Paths", pathsSection); } - settings.Save(true).SaveToFile(GetSettingsPath(installLocation)); + settingsSection.SaveToFile(settingsPath); - var backendsFile = new FDSSection(); + // Backends.fds - same deal: preserve any user-added backend entries and only replace key "0" + var backendsPath = GetBackendsPath(installLocation); + var backendsFile = File.Exists(backendsPath) + ? FDSUtility.ReadFile(backendsPath) + : new FDSSection(); var dataSection = new FDSSection(); dataSection.Set("type", "comfyui_selfstart"); dataSection.Set("title", "StabilityMatrix ComfyUI Self-Start"); @@ -371,7 +381,7 @@ await File.WriteAllTextAsync(wrapperScriptPath, scriptContent, cancellationToken } backendsFile.Set("0", dataSection); - backendsFile.SaveToFile(GetBackendsPath(installLocation)); + backendsFile.SaveToFile(backendsPath); } }