diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index caed33d30..e29a57c43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,7 +87,7 @@ jobs: - name: Install PupNet run: | sudo apt-get -y install libfuse2 - dotnet tool install -g KuiperZone.PupNet + dotnet tool install -g KuiperZone.PupNet --version 1.8.0 - name: PupNet Build env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 49a8a6fdb..201eb71f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ 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). +## v2.14.3 +### Added +- Added the ability to search by pasting an entire Civitai model URL into the search bar in the Civitai model browser +### Changed +- The main sidebar now remembers whether it was collapsed or expanded between restarts. +- Inference is now able to load image metadata from Civitai generated images via drag & drop +- Updated process tracking for ComfyUI to help mitigate restart issues when using Comfy Manager +- Updated pre-selected download locations for certain model types in the Civitai model browser +- Updated nodejs to v20.19.3 to support newer InvokeAI versions +### Fixed +- Fixed missing .NET 8 dependency for SwarmUI installs in certain cases +- Fixed ComfyUI-Zluda not being recognized as a valid Comfy install for the workflow browser +- Fixed [#1291](https://github.com/LykosAI/StabilityMatrix/issues/1291) - Certain GPUs not being detected on Linux +- Fixed [#1284](https://github.com/LykosAI/StabilityMatrix/issues/1284) - Output browser not ignoring InvokeAI thumbnails folders +- Fixed [#1301](https://github.com/LykosAI/StabilityMatrix/issues/1301) - Error when installing kohya_ss +- Fixed [#1305](https://github.com/LykosAI/StabilityMatrix/issues/1305) - FluxGym installing incorrect packages for Blackwell GPUs +- Fixed [#1316](https://github.com/LykosAI/StabilityMatrix/issues/1316) - Errors when installing Triton & SageAttention +- Fixed "directory is not empty" error when updating packages with symlinks +- Fixed missing base model types in the Checkpoint Manager & Civitai Model Browser +### Supporters +#### 🌟 Visionaries +Big heartfelt thanks to our stellar Visionary-tier Patrons: **Waterclouds**, **Corey T**, **bluepopsicle**, **Bob S**, **Ibixat**, and **whudunit**! 🌟 Your extraordinary generosity continues to fuel Stability Matrix’s journey toward innovation and excellence. We appreciate you immensely! +#### 🚀 Pioneers +Massive thanks to our fantastic Pioneer-tier Patrons: **tankfox**, **Mr. Unknown**, **Szir777**, **Tigon**, **Noah M**, **USATechDude**, **Thom**, and **SeraphOfSalem**! Your unwavering support keeps our community thriving and inspires us to push even further. You’re all awesome! + ## v2.14.2 ### Changed - Changed Nvidia GPU detection to use compute capability level instead of the GPU name for certain feature gates / torch indexes diff --git a/Directory.Build.props b/Directory.Build.props index 2d8986377..d09384c92 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,7 @@  net9.0 + preview enable enable true diff --git a/Directory.Packages.props b/Directory.Packages.props index ea2eebf10..9f12d0225 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,7 +10,7 @@ - + diff --git a/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs b/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs index a80851652..2bbbf102d 100644 --- a/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs +++ b/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs @@ -42,7 +42,7 @@ InferenceProjectDocument projectDocument while (position < inputImage.Length) { var chunkLength = BitConverter.ToInt32( - inputImage[position..(position + 4)].Reverse().ToArray(), + inputImage[position..(position + 4)].AsEnumerable().Reverse().ToArray(), 0 ); var chunkType = Encoding.ASCII.GetString(inputImage[(position + 4)..(position + 8)]); @@ -53,8 +53,10 @@ InferenceProjectDocument projectDocument { var imageWidthBytes = inputImage[(position + 8)..(position + 12)]; var imageHeightBytes = inputImage[(position + 12)..(position + 16)]; - var imageWidth = BitConverter.ToInt32(imageWidthBytes.Reverse().ToArray()); - var imageHeight = BitConverter.ToInt32(imageHeightBytes.Reverse().ToArray()); + var imageWidth = BitConverter.ToInt32(imageWidthBytes.AsEnumerable().Reverse().ToArray()); + var imageHeight = BitConverter.ToInt32( + imageHeightBytes.AsEnumerable().Reverse().ToArray() + ); generationParameters.Width = imageWidth; generationParameters.Height = imageHeight; @@ -102,7 +104,7 @@ public static byte[] RemoveMetadata(byte[] inputImage) while (position < inputImage.Length) { var chunkLength = BitConverter.ToInt32( - inputImage[position..(position + 4)].Reverse().ToArray(), + inputImage[position..(position + 4)].AsEnumerable().Reverse().ToArray(), 0 ); var chunkType = Encoding.ASCII.GetString(inputImage[(position + 4)..(position + 8)]); @@ -124,9 +126,13 @@ private static byte[] BuildTextChunk(string key, string value) { var textData = $"{key}\0{value}"; var dataBytes = Encoding.UTF8.GetBytes(textData); - var textDataLength = BitConverter.GetBytes(dataBytes.Length).Reverse(); + var textDataLength = BitConverter.GetBytes(dataBytes.Length).AsEnumerable().Reverse().ToArray(); var textDataBytes = Text.Concat(dataBytes).ToArray(); - var crc = BitConverter.GetBytes(Crc32Algorithm.Compute(textDataBytes)).Reverse(); + var crc = BitConverter + .GetBytes(Crc32Algorithm.Compute(textDataBytes)) + .AsEnumerable() + .Reverse() + .ToArray(); return textDataLength.Concat(textDataBytes).Concat(crc).ToArray(); } diff --git a/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs b/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs index f88c122b8..c50ae9605 100644 --- a/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs +++ b/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs @@ -46,7 +46,8 @@ IPyRunner pyRunner private DirectoryPath DotnetDir => AssetsDir.JoinDir("dotnet"); private string DotnetPath => Path.Combine(DotnetDir, "dotnet"); - private bool IsDotnetInstalled => File.Exists(DotnetPath); + private string Dotnet7SdkExistsPath => Path.Combine(DotnetDir, "sdk", "7.0.405"); + private string Dotnet8SdkExistsPath => Path.Combine(DotnetDir, "sdk", "8.0.101"); private string Dotnet7DownloadUrlMacOs => "https://download.visualstudio.microsoft.com/download/pr/5bb0e0e4-2a8d-4aba-88ad-232e1f65c281/ee6d35f762d81965b4cf336edde1b318/dotnet-sdk-7.0.405-osx-arm64.tar.gz"; private string Dotnet8DownloadUrlMacOs => @@ -103,19 +104,17 @@ public async Task InstallPackageRequirements( public async Task InstallDotnetIfNecessary(IProgress? progress = null) { - if (IsDotnetInstalled) - return; + var downloadUrl = Compat.IsMacOS ? Dotnet8DownloadUrlMacOs : Dotnet8DownloadUrlLinux; - if (Compat.IsMacOS) - { - await DownloadAndExtractPrerequisite(progress, Dotnet7DownloadUrlMacOs, DotnetDir); - await DownloadAndExtractPrerequisite(progress, Dotnet8DownloadUrlMacOs, DotnetDir); - } - else + var dotnet8SdkExists = Directory.Exists(Dotnet8SdkExistsPath); + + if (dotnet8SdkExists && Directory.Exists(DotnetDir)) { - await DownloadAndExtractPrerequisite(progress, Dotnet7DownloadUrlLinux, DotnetDir); - await DownloadAndExtractPrerequisite(progress, Dotnet8DownloadUrlLinux, DotnetDir); + Logger.Info("Dotnet 8 SDK already installed at {DotnetDir}", DotnetDir); + return; } + + await DownloadAndExtractPrerequisite(progress, downloadUrl, DotnetDir); } private async Task InstallVirtualenvIfNecessary(IProgress? progress = null) @@ -149,7 +148,7 @@ public async Task InstallAllIfNecessary(IProgress? progress = nu public async Task UnpackResourcesIfNecessary(IProgress? progress = null) { // Array of (asset_uri, extract_to) - var assets = new[] { (Assets.SevenZipExecutable, AssetsDir), (Assets.SevenZipLicense, AssetsDir), }; + var assets = new[] { (Assets.SevenZipExecutable, AssetsDir), (Assets.SevenZipLicense, AssetsDir) }; progress?.Report(new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true)); @@ -177,10 +176,10 @@ public async Task InstallGitIfNecessary(IProgress? progress = nu { new TextBlock { - Text = "The current operation requires Git. Please install it to continue." + Text = "The current operation requires Git. Please install it to continue.", }, new SelectableTextBlock { Text = "$ sudo apt install git" }, - } + }, }, PrimaryButtonText = Resources.Action_Retry, CloseButtonText = Resources.Action_Close, @@ -352,6 +351,22 @@ public async Task RunNpm( ); } + // NOTE TO FUTURE DEVS: if this is causing merge conflicts with dev, just nuke it we don't need anymore + private async Task RunNode( + ProcessArgs args, + string? workingDirectory = null, + IReadOnlyDictionary? envVars = null + ) + { + var nodePath = Path.Combine(NodeDir, "bin", "node"); + var result = await ProcessRunner + .GetProcessResultAsync(nodePath, args, workingDirectory, envVars) + .ConfigureAwait(false); + + result.EnsureSuccessExitCode(); + return result.StandardOutput ?? result.StandardError ?? string.Empty; + } + [SupportedOSPlatform("Linux")] [SupportedOSPlatform("macOS")] public async Task RunDotnet( @@ -396,17 +411,32 @@ public async Task RunDotnet( [SupportedOSPlatform("macOS")] public async Task InstallNodeIfNecessary(IProgress? progress = null) { - if (IsNodeInstalled) + // NOTE TO FUTURE DEVS: if this is causing merge conflicts with dev, just nuke it we don't need anymore + if (NodeDir.Exists) { - Logger.Info("node already installed"); - return; + try + { + var result = await RunNode("-v"); + if (result.Contains("20.19.3")) + { + Logger.Debug("Node.js already installed at {NodeExistsPath}", NodeDir); + return; + } + } + catch (Exception) + { + // ignored + } + + Logger.Warn("Node.js version mismatch, reinstalling..."); + await NodeDir.DeleteAsync(true); } Logger.Info("Downloading node"); var downloadUrl = Compat.IsMacOS - ? "https://nodejs.org/dist/v20.11.0/node-v20.11.0-darwin-arm64.tar.gz" - : "https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.gz"; + ? "https://nodejs.org/dist/v20.19.3/node-v20.19.3-darwin-arm64.tar.gz" + : "https://nodejs.org/dist/v20.19.3/node-v20.19.3-linux-x64.tar.gz"; var nodeDownloadPath = AssetsDir.JoinFile(Path.GetFileName(downloadUrl)); @@ -426,8 +456,8 @@ public async Task InstallNodeIfNecessary(IProgress? progress = n await ArchiveHelper.Extract7ZAuto(nodeDownloadPath, AssetsDir); var nodeDir = Compat.IsMacOS - ? AssetsDir.JoinDir("node-v20.11.0-darwin-arm64") - : AssetsDir.JoinDir("node-v20.11.0-linux-x64"); + ? AssetsDir.JoinDir("node-v20.19.3-darwin-arm64") + : AssetsDir.JoinDir("node-v20.19.3-linux-x64"); Directory.Move(nodeDir, NodeDir); progress?.Report( diff --git a/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs b/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs index ddf62fd46..eb2afb259 100644 --- a/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs +++ b/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs @@ -32,7 +32,7 @@ IPyRunner pyRunner private const string TkinterDownloadUrl = "https://cdn.lykos.ai/tkinter-cpython-embedded-3.10.11-win-x64.zip"; - private const string NodeDownloadUrl = "https://nodejs.org/dist/v20.11.0/node-v20.11.0-win-x64.zip"; + private const string NodeDownloadUrl = "https://nodejs.org/dist/v20.19.3/node-v20.19.3-win-x64.zip"; private const string Dotnet7DownloadUrl = "https://download.visualstudio.microsoft.com/download/pr/2133b143-9c4f-4daa-99b0-34fa6035d67b/193ede446d922eb833f1bfe0239be3fc/dotnet-sdk-7.0.405-win-x64.zip"; @@ -69,11 +69,14 @@ IPyRunner pyRunner private string TkinterExtractPath => PythonDir; private string TkinterExistsPath => Path.Combine(PythonDir, "tkinter"); private string NodeExistsPath => Path.Combine(AssetsDir, "nodejs", "npm.cmd"); + private string NodeExePath => Path.Combine(AssetsDir, "nodejs", "node.exe"); private string NodeDownloadPath => Path.Combine(AssetsDir, "nodejs.zip"); private string Dotnet7DownloadPath => Path.Combine(AssetsDir, "dotnet-sdk-7.0.405-win-x64.zip"); private string Dotnet8DownloadPath => Path.Combine(AssetsDir, "dotnet-sdk-8.0.101-win-x64.zip"); private string DotnetExtractPath => Path.Combine(AssetsDir, "dotnet"); private string DotnetExistsPath => Path.Combine(DotnetExtractPath, "dotnet.exe"); + private string Dotnet7SdkExistsPath => Path.Combine(DotnetExtractPath, "sdk", "7.0.405"); + private string Dotnet8SdkExistsPath => Path.Combine(DotnetExtractPath, "sdk", "8.0.101"); private string VcBuildToolsDownloadPath => Path.Combine(AssetsDir, "vs_BuildTools.exe"); private string VcBuildToolsExistsPath => @@ -107,7 +110,7 @@ public async Task RunGit( onProcessOutput, environmentVariables: new Dictionary { - { "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) } + { "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) }, } ); await process.WaitForExitAsync().ConfigureAwait(false); @@ -125,7 +128,7 @@ public Task GetGitOutput(ProcessArgs args, string? workingDirecto workingDirectory: workingDirectory, environmentVariables: new Dictionary { - { "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) } + { "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) }, } ); } @@ -146,6 +149,21 @@ public async Task RunNpm( onProcessOutput?.Invoke(ProcessOutput.FromStdErrLine(result.StandardError)); } + // NOTE TO FUTURE DEVS: if this is causing merge conflicts with dev, just nuke it we don't need anymore + private async Task RunNode( + ProcessArgs args, + string? workingDirectory = null, + IReadOnlyDictionary? envVars = null + ) + { + var result = await ProcessRunner + .GetProcessResultAsync(NodeExePath, args, workingDirectory, envVars) + .ConfigureAwait(false); + + result.EnsureSuccessExitCode(); + return result.StandardOutput ?? result.StandardError ?? string.Empty; + } + public Task InstallPackageRequirements(BasePackage package, IProgress? progress = null) => InstallPackageRequirements(package.Prerequisites.ToList(), progress); @@ -212,7 +230,7 @@ public async Task InstallAllIfNecessary(IProgress? progress = nu public async Task UnpackResourcesIfNecessary(IProgress? progress = null) { // Array of (asset_uri, extract_to) - var assets = new[] { (Assets.SevenZipExecutable, AssetsDir), (Assets.SevenZipLicense, AssetsDir), }; + var assets = new[] { (Assets.SevenZipExecutable, AssetsDir), (Assets.SevenZipLicense, AssetsDir) }; progress?.Report(new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true)); @@ -485,12 +503,31 @@ await downloadService.DownloadToFileAsync( [SupportedOSPlatform("windows")] public async Task InstallNodeIfNecessary(IProgress? progress = null) { - if (File.Exists(NodeExistsPath)) - return; + // NOTE TO FUTURE DEVS: if this is causing merge conflicts with dev, just nuke it we don't need anymore + var nodeFolder = new DirectoryPath(AssetsDir, "nodejs"); + if (nodeFolder.Exists) + { + try + { + var result = await RunNode("-v"); + if (result.Contains("20.19.3")) + { + Logger.Debug("Node.js already installed at {NodeExistsPath}", NodeExistsPath); + return; + } + } + catch (Exception) + { + // ignored + } + + Logger.Warn("Node.js version mismatch, reinstalling..."); + await nodeFolder.DeleteAsync(true); + } await DownloadAndExtractPrerequisite(progress, NodeDownloadUrl, NodeDownloadPath, AssetsDir); - var extractedNodeDir = Path.Combine(AssetsDir, "node-v20.11.0-win-x64"); + var extractedNodeDir = Path.Combine(AssetsDir, "node-v20.19.3-win-x64"); if (Directory.Exists(extractedNodeDir)) { Directory.Move(extractedNodeDir, Path.Combine(AssetsDir, "nodejs")); @@ -500,21 +537,25 @@ public async Task InstallNodeIfNecessary(IProgress? progress = n [SupportedOSPlatform("windows")] public async Task InstallDotnetIfNecessary(IProgress? progress = null) { - if (File.Exists(DotnetExistsPath)) - return; + if (!Directory.Exists(Dotnet7SdkExistsPath)) + { + await DownloadAndExtractPrerequisite( + progress, + Dotnet7DownloadUrl, + Dotnet7DownloadPath, + DotnetExtractPath + ); + } - await DownloadAndExtractPrerequisite( - progress, - Dotnet7DownloadUrl, - Dotnet7DownloadPath, - DotnetExtractPath - ); - await DownloadAndExtractPrerequisite( - progress, - Dotnet8DownloadUrl, - Dotnet8DownloadPath, - DotnetExtractPath - ); + if (!Directory.Exists(Dotnet8SdkExistsPath)) + { + await DownloadAndExtractPrerequisite( + progress, + Dotnet8DownloadUrl, + Dotnet8DownloadPath, + DotnetExtractPath + ); + } } [SupportedOSPlatform("windows")] @@ -585,7 +626,7 @@ public async Task InstallHipSdkIfNecessary(IProgress? progress = Arguments = "-install -log hip_install.log", UseShellExecute = true, CreateNoWindow = true, - Verb = "runas" + Verb = "runas", }; if (Process.Start(info) is { } process) @@ -749,14 +790,14 @@ private async Task PatchHipSdkIfNecessary(IProgress? progress) { _ when downloadUrl.Contains("gfx1201") => null, _ when downloadUrl.Contains("gfx1150") => "rocm gfx1150 for hip skd 6.2.4", - _ when downloadUrl.Contains("gfx1103.AMD") - => "rocm gfx1103 AMD 780M phoenix V5.0 for hip skd 6.2.4", + _ when downloadUrl.Contains("gfx1103.AMD") => + "rocm gfx1103 AMD 780M phoenix V5.0 for hip skd 6.2.4", _ when downloadUrl.Contains("gfx1034") => "rocm gfx1034-gfx1035-gfx1036 for hip sdk 6.2.4", _ when downloadUrl.Contains("gfx1032") => "rocm gfx1032 for hip skd 6.2.4(navi21 logic)", _ when downloadUrl.Contains("gfx1031") => "rocm gfx1031 for hip skd 6.2.4 (littlewu's logic)", - _ when downloadUrl.Contains("gfx1010") - => "rocm gfx1010-xnack-gfx1011-xnack-gfx1012-xnack- for hip sdk 6.2.4", - _ => null + _ when downloadUrl.Contains("gfx1010") => + "rocm gfx1010-xnack-gfx1011-xnack-gfx1012-xnack- for hip sdk 6.2.4", + _ => null, }; var librarySourceDir = rocmLibsExtractPath; diff --git a/StabilityMatrix.Avalonia/Services/CivitBaseModelTypeService.cs b/StabilityMatrix.Avalonia/Services/CivitBaseModelTypeService.cs index 9f91ff253..bf9ef25e3 100644 --- a/StabilityMatrix.Avalonia/Services/CivitBaseModelTypeService.cs +++ b/StabilityMatrix.Avalonia/Services/CivitBaseModelTypeService.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Database; +using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Database; @@ -42,18 +43,18 @@ public async Task> GetBaseModelTypes(bool forceRefresh = false, boo var jsonContent = await baseModelsResponse.Content.ReadAsStringAsync(); var baseModels = JsonNode.Parse(jsonContent); - var jArray = - baseModels?["error"]?["issues"]?[0]?["unionErrors"]?[0]?["issues"]?[0]?["options"] - as JsonArray; + var innerJson = baseModels?["error"]?["message"]?.GetValue(); + var jArray = JsonNode.Parse(innerJson).AsArray(); + var baseModelValues = jArray[0]?["errors"]?[0]?[0]?["values"]?.AsArray(); - civitBaseModels = jArray?.GetValues().ToList() ?? []; + civitBaseModels = baseModelValues?.GetValues().ToList() ?? []; // Cache the results var cacheEntry = new CivitBaseModelTypeCacheEntry { Id = CacheId, ModelTypes = civitBaseModels, - CreatedAt = DateTimeOffset.UtcNow + CreatedAt = DateTimeOffset.UtcNow, }; await dbContext.UpsertCivitBaseModelTypeCacheEntry(cacheEntry); @@ -78,7 +79,8 @@ 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 ?? []; + return expiredCache?.ModelTypes + ?? Enum.GetValues().Select(b => b.GetStringValue()).ToList(); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs index 2ae913689..e4e8a933a 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs @@ -204,7 +204,13 @@ public void LoadImageMetadata(LocalImageFile localImageFile) /// private void LoadImageMetadata( string imageFilePath, - (string? Parameters, string? ParametersJson, string? SMProject, string? ComfyNodes) metadata + ( + string? Parameters, + string? ParametersJson, + string? SMProject, + string? ComfyNodes, + string? CivitParameters + ) metadata ) { // Has SMProject metadata @@ -236,8 +242,8 @@ private void LoadImageMetadata( // Load image if (this is IImageGalleryComponent imageGalleryComponent) { - Dispatcher.UIThread.Invoke( - () => imageGalleryComponent.LoadImagesToGallery(new ImageSource(imageFilePath)) + Dispatcher.UIThread.Invoke(() => + imageGalleryComponent.LoadImagesToGallery(new ImageSource(imageFilePath)) ); } @@ -270,8 +276,41 @@ private void LoadImageMetadata( // Load image if (this is IImageGalleryComponent imageGalleryComponent) { - Dispatcher.UIThread.Invoke( - () => imageGalleryComponent.LoadImagesToGallery(new ImageSource(imageFilePath)) + Dispatcher.UIThread.Invoke(() => + imageGalleryComponent.LoadImagesToGallery(new ImageSource(imageFilePath)) + ); + } + + return; + } + + // Civit generator metadata + if (metadata.CivitParameters is not null) + { + Logger.Info("Loading Parameters from metadata"); + + if (!GenerationParameters.TryParse(metadata.CivitParameters, out var parameters)) + { + throw new ApplicationException("Failed to parse parameters"); + } + + if (this is IParametersLoadableState paramsLoadableVm) + { + Dispatcher.UIThread.Invoke(() => paramsLoadableVm.LoadStateFromParameters(parameters)); + } + else + { + Logger.Warn( + "Load parameters target {Type} does not implement IParametersLoadableState, skipping", + GetType().Name + ); + } + + // Load image + if (this is IImageGalleryComponent imageGalleryComponent) + { + Dispatcher.UIThread.Invoke(() => + imageGalleryComponent.LoadImagesToGallery(new ImageSource(imageFilePath)) ); } diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs index 2d9557137..25bb76525 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs @@ -147,30 +147,26 @@ private void CheckIfInstalled() var latestVersionInstalled = latestVersion.Files != null - && latestVersion.Files.Any( - file => - file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null } - && installedModels.Contains(file.Hashes.BLAKE3) + && latestVersion.Files.Any(file => + file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null } + && installedModels.Contains(file.Hashes.BLAKE3) ); // check if any of the ModelVersion.Files.Hashes.BLAKE3 hashes are in the installedModels list var anyVersionInstalled = latestVersionInstalled - || CivitModel.ModelVersions.Any( - version => - version.Files != null - && version.Files.Any( - file => - file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null } - && installedModels.Contains(file.Hashes.BLAKE3) - ) + || CivitModel.ModelVersions.Any(version => + version.Files != null + && version.Files.Any(file => + file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null } + && installedModels.Contains(file.Hashes.BLAKE3) + ) ); - UpdateCardText = latestVersionInstalled - ? "Installed" - : anyVersionInstalled - ? "Update Available" - : string.Empty; + UpdateCardText = + latestVersionInstalled ? "Installed" + : anyVersionInstalled ? "Update Available" + : string.Empty; ShowUpdateCard = anyVersionInstalled; } @@ -179,15 +175,15 @@ private void UpdateImage() { var nsfwEnabled = settingsManager.Settings.ModelBrowserNsfwEnabled; var hideEarlyAccessModels = settingsManager.Settings.HideEarlyAccessModels; - var version = CivitModel.ModelVersions?.FirstOrDefault( - v => !hideEarlyAccessModels || !v.IsEarlyAccess + var version = CivitModel.ModelVersions?.FirstOrDefault(v => + !hideEarlyAccessModels || !v.IsEarlyAccess ); var images = version?.Images; // Try to find a valid image var image = images - ?.Where( - img => LocalModelFile.SupportedImageExtensions.Any(img.Url.Contains) && img.Type == "image" + ?.Where(img => + LocalModelFile.SupportedImageExtensions.Any(img.Url.Contains) && img.Type == "image" ) .FirstOrDefault(image => nsfwEnabled || image.NsfwLevel <= 1); if (image != null) @@ -351,6 +347,9 @@ private async Task ShowVersionDialog(CivitModel model) if ( model.BaseModelType == CivitBaseModelType.Flux1D.GetStringValue() || model.BaseModelType == CivitBaseModelType.Flux1S.GetStringValue() + || model.BaseModelType == CivitBaseModelType.WanVideo.GetStringValue() + || model.BaseModelType == CivitBaseModelType.HunyuanVideo.GetStringValue() + || selectedFile?.Metadata.Format is CivitModelFormat.GGUF ) { sharedFolder = SharedFolderType.DiffusionModels.GetStringValue(); diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs index 6485de10c..6334c875e 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs @@ -640,6 +640,29 @@ private async Task SearchModels(bool isInfiniteScroll = false) modelRequest.Sort = CivitSortMode.HighestRated; } } + else if (SearchQuery.StartsWith("https://civitai.com/models/")) + { + /* extract model ID from URL, could be one of: + https://civitai.com/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; + + if (modelRequest.Sort is CivitSortMode.Favorites or CivitSortMode.Installed) + { + SortMode = CivitSortMode.HighestRated; + modelRequest.Sort = CivitSortMode.HighestRated; + } + } else { modelRequest.Query = SearchQuery; diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs index 0cc851fdf..41b2d6a0b 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OpenArtWorkflowViewModel.cs @@ -47,7 +47,7 @@ IPackageFactory packageFactory public List AvailablePackages => settingsManager - .Settings.InstalledPackages.Where(package => package.PackageName == "ComfyUI") + .Settings.InstalledPackages.Where(package => package.PackageName is "ComfyUI" or "ComfyUI-Zluda") .ToList(); public List MissingNodes { get; } = []; @@ -157,7 +157,7 @@ out var manifestExtension currentSection = new OpenArtCustomNode { Title = node, - IsInstalled = installedNodesNames.Contains(node) + IsInstalled = installedNodesNames.Contains(node), }; // Add missing nodes to the list diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs index 034ef1258..254c0f6aa 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs @@ -314,7 +314,7 @@ private void LoadInstallLocations() var downloadDirectory = GetSharedFolderPath( rootModelsDirectory, - SelectedFile?.CivitFile.Type, + SelectedFile?.CivitFile, CivitModel.Type, CivitModel.BaseModelType ); @@ -356,12 +356,12 @@ var directory in downloadDirectory.EnumerateDirectories( private static DirectoryPath GetSharedFolderPath( DirectoryPath rootModelsDirectory, - CivitFileType? fileType, + CivitFile? civitFile, CivitModelType modelType, string? baseModelType ) { - if (fileType is CivitFileType.VAE) + if (civitFile?.Type is CivitFileType.VAE) { return rootModelsDirectory.JoinDir(SharedFolderType.VAE.GetStringValue()); } @@ -371,17 +371,15 @@ modelType is CivitModelType.Checkpoint && ( baseModelType == CivitBaseModelType.Flux1D.GetStringValue() || baseModelType == CivitBaseModelType.Flux1S.GetStringValue() + || baseModelType == CivitBaseModelType.WanVideo.GetStringValue() + || baseModelType == CivitBaseModelType.HunyuanVideo.GetStringValue() + || civitFile?.Metadata.Format == CivitModelFormat.GGUF ) ) { return rootModelsDirectory.JoinDir(SharedFolderType.DiffusionModels.GetStringValue()); } - if (modelType is CivitModelType.Checkpoint && baseModelType == "Wan Video") - { - return rootModelsDirectory.JoinDir(SharedFolderType.DiffusionModels.GetStringValue()); - } - return rootModelsDirectory.JoinDir(modelType.ConvertTo().GetStringValue()); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs index e4ba639c8..e1fb83a9a 100644 --- a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs @@ -60,6 +60,9 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] private object? selectedCategory; + [ObservableProperty] + public partial bool IsPaneOpen { get; set; } + [ObservableProperty] private List pages = new(); @@ -164,6 +167,13 @@ protected override async Task OnInitialLoadedAsync() return; } + settingsManager.RelayPropertyFor( + this, + vm => vm.IsPaneOpen, + settings => settings.IsMainWindowSidebarOpen, + true + ); + // Initialize Discord Rich Presence (this needs LibraryDir so is set here) discordRichPresenceService.UpdateState(); diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs index c9da0fbd1..7bf87daca 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs @@ -460,7 +460,7 @@ public async Task ConsolidateImages() { Text = Resources.Label_ConsolidateExplanation, TextWrapping = TextWrapping.Wrap, - Margin = new Thickness(0, 8, 0, 16) + Margin = new Thickness(0, 8, 0, 16), } ); foreach (var category in Categories) @@ -476,7 +476,7 @@ public async Task ConsolidateImages() Content = $"{category.Name} ({category.Path})", IsChecked = true, Margin = new Thickness(0, 8, 0, 0), - Tag = category.Path + Tag = category.Path, } ); } @@ -602,7 +602,14 @@ private void GetOutputs(string directory) var files = Directory .EnumerateFiles(directory, "*", EnumerationOptionConstants.AllDirectories) - .Where(file => allowedExtensions.Contains(new FilePath(file).Extension)) + .Where(file => + allowedExtensions.Contains(new FilePath(file).Extension) + && new FilePath(file).Info.DirectoryName?.EndsWith( + "thumbnails", + StringComparison.OrdinalIgnoreCase + ) + is false + ) .Select(file => LocalImageFile.FromPath(file)) .ToList(); @@ -647,24 +654,17 @@ private void RefreshCategories() .Settings.InstalledPackages.Where(x => !x.UseSharedOutputFolder) .Select(packageFactory.GetPackagePair) .WhereNotNull() - .Where( - p => - p.BasePackage.SharedOutputFolders is { Count: > 0 } && p.InstalledPackage.FullPath != null - ) - .Select( - pair => - new TreeViewDirectory - { - Path = Path.Combine( - pair.InstalledPackage.FullPath!, - pair.BasePackage.OutputFolderName - ), - Name = pair.InstalledPackage.DisplayName ?? "", - SubDirectories = GetSubfolders( - Path.Combine(pair.InstalledPackage.FullPath!, pair.BasePackage.OutputFolderName) - ) - } + .Where(p => + p.BasePackage.SharedOutputFolders is { Count: > 0 } && p.InstalledPackage.FullPath != null ) + .Select(pair => new TreeViewDirectory + { + Path = Path.Combine(pair.InstalledPackage.FullPath!, pair.BasePackage.OutputFolderName), + Name = pair.InstalledPackage.DisplayName ?? "", + SubDirectories = GetSubfolders( + Path.Combine(pair.InstalledPackage.FullPath!, pair.BasePackage.OutputFolderName) + ), + }) .ToList(); packageCategories.Insert( @@ -673,7 +673,7 @@ private void RefreshCategories() { Path = settingsManager.ImagesDirectory, Name = "Shared Output Folder", - SubDirectories = GetSubfolders(settingsManager.ImagesDirectory) + SubDirectories = GetSubfolders(settingsManager.ImagesDirectory), } ); diff --git a/StabilityMatrix.Avalonia/Views/MainWindow.axaml b/StabilityMatrix.Avalonia/Views/MainWindow.axaml index a50dbadaa..0761b30cc 100644 --- a/StabilityMatrix.Avalonia/Views/MainWindow.axaml +++ b/StabilityMatrix.Avalonia/Views/MainWindow.axaml @@ -1,27 +1,30 @@ - - + + @@ -30,35 +33,37 @@ - + - - + + - + + Text="{Binding Title, RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}"> - + @@ -66,110 +71,114 @@ + Tag="{Binding}" /> - - + + - + Symbol="ArrowDownload" /> + - - - + + + - + - - - + + + - - + + - - - + + + - + - + - + - + - - + + + Grid.Row="1" + Grid.RowSpan="2" + PreferredPlacement="Right" + Target="{Binding #FooterDownloadItem}" /> diff --git a/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs b/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs index 7e2f988aa..4c58618f3 100644 --- a/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs +++ b/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs @@ -70,7 +70,7 @@ private static IEnumerable IterGpuInfoWindows() [SupportedOSPlatform("linux")] private static IEnumerable IterGpuInfoLinux() { - var output = RunBashCommand("lspci | grep VGA"); + var output = RunBashCommand("lspci | grep -E \"(VGA|3D)\""); var gpuLines = output.Split("\n"); var gpuIndex = 0; @@ -153,15 +153,26 @@ public static IEnumerable IterGpuInfo(bool forceRefresh = false) return cachedGpuInfos; } - if (Compat.IsWindows) + if (Compat.IsMacOS) + { + return cachedGpuInfos = IterGpuInfoMacos().ToList(); + } + + if (Compat.IsLinux || Compat.IsWindows) { try { var smi = IterGpuInfoNvidiaSmi()?.ToList(); + var fallback = Compat.IsLinux + ? IterGpuInfoLinux().ToList() + : IterGpuInfoWindows().ToList(); + if (smi is null) - return cachedGpuInfos = IterGpuInfoWindows().ToList(); + { + return cachedGpuInfos = fallback; + } - var newList = smi.Concat(IterGpuInfoWindows().Where(gpu => !gpu.IsNvidia)) + var newList = smi.Concat(fallback.Where(gpu => !gpu.IsNvidia)) .Select( (gpu, index) => new GpuInfo @@ -177,22 +188,15 @@ public static IEnumerable IterGpuInfo(bool forceRefresh = false) catch (Exception e) { Logger.Error(e, "Failed to get GPU info using nvidia-smi, falling back to registry"); - return cachedGpuInfos = IterGpuInfoWindows().ToList(); - } - } - if (Compat.IsLinux) - { - return cachedGpuInfos = IterGpuInfoLinux().ToList(); - } - - if (Compat.IsMacOS) - { - return cachedGpuInfos = IterGpuInfoMacos().ToList(); + var fallback = Compat.IsLinux + ? IterGpuInfoLinux().ToList() + : IterGpuInfoWindows().ToList(); + return cachedGpuInfos = fallback; + } } Logger.Error("Unknown OS, returning empty GPU info list"); - return cachedGpuInfos = []; } } diff --git a/StabilityMatrix.Core/Helper/ImageMetadata.cs b/StabilityMatrix.Core/Helper/ImageMetadata.cs index 2d3b9f392..797d82a30 100644 --- a/StabilityMatrix.Core/Helper/ImageMetadata.cs +++ b/StabilityMatrix.Core/Helper/ImageMetadata.cs @@ -2,6 +2,7 @@ using System.Text; using System.Text.Json; using ExifLibrary; +using KGySoft.CoreLibraries; using MetadataExtractor; using MetadataExtractor.Formats.Exif; using MetadataExtractor.Formats.Png; @@ -52,8 +53,8 @@ public static System.Drawing.Size GetImageSize(byte[] inputImage) { var imageWidthBytes = inputImage[0x10..0x14]; var imageHeightBytes = inputImage[0x14..0x18]; - var imageWidth = BitConverter.ToInt32(imageWidthBytes.Reverse().ToArray()); - var imageHeight = BitConverter.ToInt32(imageHeightBytes.Reverse().ToArray()); + var imageWidth = BitConverter.ToInt32(imageWidthBytes.AsEnumerable().Reverse().ToArray()); + var imageHeight = BitConverter.ToInt32(imageHeightBytes.AsEnumerable().Reverse().ToArray()); return new System.Drawing.Size(imageWidth, imageHeight); } @@ -66,8 +67,8 @@ public static System.Drawing.Size GetImageSize(BinaryReader reader) var imageWidthBytes = reader.ReadBytes(4); var imageHeightBytes = reader.ReadBytes(4); - var imageWidth = BitConverter.ToInt32(imageWidthBytes.Reverse().ToArray()); - var imageHeight = BitConverter.ToInt32(imageHeightBytes.Reverse().ToArray()); + var imageWidth = BitConverter.ToInt32(imageWidthBytes.AsEnumerable().Reverse().ToArray()); + var imageHeight = BitConverter.ToInt32(imageHeightBytes.AsEnumerable().Reverse().ToArray()); reader.BaseStream.Position = oldPosition; @@ -78,7 +79,8 @@ public static ( string? Parameters, string? ParametersJson, string? SMProject, - string? ComfyNodes + string? ComfyNodes, + string? CivitParameters ) GetAllFileMetadata(FilePath filePath) { if (filePath.Extension.Equals(".webp", StringComparison.OrdinalIgnoreCase)) @@ -86,7 +88,7 @@ public static ( var paramsJson = ReadTextChunkFromWebp(filePath, ExifDirectoryBase.TagImageDescription); var smProj = ReadTextChunkFromWebp(filePath, ExifDirectoryBase.TagSoftware); - return (null, paramsJson, smProj, null); + return (null, paramsJson, smProj, null, null); } using var stream = filePath.Info.OpenRead(); @@ -96,12 +98,14 @@ public static ( var parametersJson = ReadTextChunk(reader, "parameters-json"); var smProject = ReadTextChunk(reader, "smproj"); var comfyNodes = ReadTextChunk(reader, "prompt"); + var civitParameters = ReadTextChunk(reader, "user_comment"); return ( string.IsNullOrEmpty(parameters) ? null : parameters, string.IsNullOrEmpty(parametersJson) ? null : parametersJson, string.IsNullOrEmpty(smProject) ? null : smProject, - string.IsNullOrEmpty(comfyNodes) ? null : comfyNodes + string.IsNullOrEmpty(comfyNodes) ? null : comfyNodes, + string.IsNullOrEmpty(civitParameters) ? null : civitParameters ); } @@ -124,8 +128,8 @@ public static ( // Use "parameters-json" tag if exists if ( - textualData.FirstOrDefault( - tag => tag.Description is { } desc && desc.StartsWith("parameters-json: ") + textualData.FirstOrDefault(tag => + tag.Description is { } desc && desc.StartsWith("parameters-json: ") ) is { Description: { } description } ) @@ -137,8 +141,8 @@ public static ( // Otherwise parse "parameters" tag if ( - textualData.FirstOrDefault( - tag => tag.Description is { } desc && desc.StartsWith("parameters: ") + textualData.FirstOrDefault(tag => + tag.Description is { } desc && desc.StartsWith("parameters: ") ) is { Description: { } parameters } ) @@ -166,7 +170,7 @@ public static string ReadTextChunk(BinaryReader byteStream, string key) while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4) { - var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).Reverse().ToArray()); + var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).AsEnumerable().Reverse().ToArray()); var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4)); if (chunkType == Encoding.UTF8.GetString(Idat)) @@ -217,7 +221,7 @@ public static string ReadTextChunk(BinaryReader byteStream, string key) while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4) { var chunkSizeBytes = byteStream.ReadBytes(4); - var chunkSize = BitConverter.ToInt32(chunkSizeBytes.Reverse().ToArray()); + var chunkSize = BitConverter.ToInt32(chunkSizeBytes.AsEnumerable().Reverse().ToArray()); var chunkTypeBytes = byteStream.ReadBytes(4); var chunkType = Encoding.UTF8.GetString(chunkTypeBytes); diff --git a/StabilityMatrix.Core/Models/Api/CivitModelFpType.cs b/StabilityMatrix.Core/Models/Api/CivitModelFpType.cs index 7f87dfbef..9fb14946d 100644 --- a/StabilityMatrix.Core/Models/Api/CivitModelFpType.cs +++ b/StabilityMatrix.Core/Models/Api/CivitModelFpType.cs @@ -10,5 +10,7 @@ public enum CivitModelFpType bf16, fp16, fp32, - tf32 + tf32, + fp8, + nf4, } diff --git a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs index eaba34335..935ad012e 100644 --- a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs +++ b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs @@ -51,7 +51,13 @@ public record LocalImageFile /// public string FileNameWithoutExtension => Path.GetFileNameWithoutExtension(AbsolutePath); - public (string? Parameters, string? ParametersJson, string? SMProject, string? ComfyNodes) ReadMetadata() + public ( + string? Parameters, + string? ParametersJson, + string? SMProject, + string? ComfyNodes, + string? CivitParameters + ) ReadMetadata() { if (AbsolutePath.EndsWith("webp")) { @@ -61,7 +67,7 @@ public record LocalImageFile ); var smProj = ImageMetadata.ReadTextChunkFromWebp(AbsolutePath, ExifDirectoryBase.TagSoftware); - return (null, paramsJson, smProj, null); + return (null, paramsJson, smProj, null, null); } using var stream = new FileStream(AbsolutePath, FileMode.Open, FileAccess.Read, FileShare.Read); @@ -71,12 +77,14 @@ public record LocalImageFile var parametersJson = ImageMetadata.ReadTextChunk(reader, "parameters-json"); var smProject = ImageMetadata.ReadTextChunk(reader, "smproj"); var comfyNodes = ImageMetadata.ReadTextChunk(reader, "prompt"); + var civitParameters = ImageMetadata.ReadTextChunk(reader, "user_comment"); return ( string.IsNullOrEmpty(parameters) ? null : parameters, string.IsNullOrEmpty(parametersJson) ? null : parametersJson, string.IsNullOrEmpty(smProject) ? null : smProject, - string.IsNullOrEmpty(comfyNodes) ? null : comfyNodes + string.IsNullOrEmpty(comfyNodes) ? null : comfyNodes, + string.IsNullOrEmpty(civitParameters) ? null : civitParameters ); } @@ -113,7 +121,7 @@ public static LocalImageFile FromPath(FilePath filePath) CreatedAt = filePath.Info.CreationTimeUtc, LastModifiedAt = filePath.Info.LastWriteTimeUtc, GenerationParameters = parameters, - ImageSize = new Size(parameters?.Width ?? 0, parameters?.Height ?? 0) + ImageSize = new Size(parameters?.Width ?? 0, parameters?.Height ?? 0), }; } @@ -136,6 +144,10 @@ public static LocalImageFile FromPath(FilePath filePath) else { metadata = ImageMetadata.ReadTextChunk(reader, "parameters"); + if (string.IsNullOrWhiteSpace(metadata)) // if still empty, try civitai metadata (user_comment) + { + metadata = ImageMetadata.ReadTextChunk(reader, "user_comment"); + } GenerationParameters.TryParse(metadata, out genParams); } @@ -148,7 +160,7 @@ public static LocalImageFile FromPath(FilePath filePath) CreatedAt = filePath.Info.CreationTimeUtc, LastModifiedAt = filePath.Info.LastWriteTimeUtc, GenerationParameters = genParams, - ImageSize = imageSize + ImageSize = imageSize, }; } @@ -162,7 +174,7 @@ public static LocalImageFile FromPath(FilePath filePath) ImageType = imageType, CreatedAt = filePath.Info.CreationTimeUtc, LastModifiedAt = filePath.Info.LastWriteTimeUtc, - ImageSize = new Size { Height = codec.Info.Height, Width = codec.Info.Width } + ImageSize = new Size { Height = codec.Info.Height, Width = codec.Info.Width }, }; } @@ -172,6 +184,6 @@ public static LocalImageFile FromPath(FilePath filePath) ".jpg", ".jpeg", ".gif", - ".webp" + ".webp", ]; } diff --git a/StabilityMatrix.Core/Models/PackageModification/InstallSageAttentionStep.cs b/StabilityMatrix.Core/Models/PackageModification/InstallSageAttentionStep.cs index b3fda0642..58cb8c1c7 100644 --- a/StabilityMatrix.Core/Models/PackageModification/InstallSageAttentionStep.cs +++ b/StabilityMatrix.Core/Models/PackageModification/InstallSageAttentionStep.cs @@ -60,6 +60,16 @@ public async Task ExecuteAsync(IProgress? progress = null) sageWheelUrl = "https://github.com/woct0rdho/SageAttention/releases/download/v2.1.1-windows/sageattention-2.1.1+cu128torch2.7.0-cp310-cp310-win_amd64.whl"; } + else if (torchInfo.Version.Contains("2.7.1") && torchInfo.Version.Contains("cu128")) + { + sageWheelUrl = + $"https://github.com/woct0rdho/SageAttention/releases/download/v2.2.0-windows/sageattention-2.2.0+cu128torch2.7.1-cp310-cp310-win_amd64.whl"; + } + else if (torchInfo.Version.Contains("2.8.0") && torchInfo.Version.Contains("cu128")) + { + sageWheelUrl = + $"https://github.com/woct0rdho/SageAttention/releases/download/v2.2.0-windows/sageattention-2.2.0+cu128torch2.8.0-cp310-cp310-win_amd64.whl"; + } var pipArgs = new PipInstallArgs(); if (IsBlackwellGpu) diff --git a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs index 7b8011fbb..622df87e4 100644 --- a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs @@ -446,18 +446,40 @@ await PrerequisiteHelper { if (SharedFolders is not null) { - Helper.SharedFolders.RemoveLinksForPackage( - SharedFolders, - new DirectoryPath(installedPackage.FullPath!) - ); + try + { + Helper.SharedFolders.RemoveLinksForPackage( + SharedFolders, + new DirectoryPath(installedPackage.FullPath!) + ); + } + catch (Exception e) + { + Logger.Warn( + e, + "Failed to remove symlinks for package {Package}", + installedPackage.PackageName + ); + } } if (SharedOutputFolders is not null && installedPackage.UseSharedOutputFolder) { - Helper.SharedFolders.RemoveLinksForPackage( - SharedOutputFolders, - new DirectoryPath(installedPackage.FullPath!) - ); + try + { + Helper.SharedFolders.RemoveLinksForPackage( + SharedOutputFolders, + new DirectoryPath(installedPackage.FullPath!) + ); + } + catch (Exception e) + { + Logger.Warn( + e, + "Failed to remove output symlinks for package {Package}", + installedPackage.PackageName + ); + } } } diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index 5674567a6..51aed8602 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -414,6 +414,11 @@ public override async Task RunPackage( OnExit ); + if (Compat.IsWindows) + { + ProcessTracker.AttachExitHandlerJobToProcess(VenvRunner.Process); + } + return; void HandleConsoleOutput(ProcessOutput s) diff --git a/StabilityMatrix.Core/Models/Packages/FluxGym.cs b/StabilityMatrix.Core/Models/Packages/FluxGym.cs index c4af5cd81..dc63a4513 100644 --- a/StabilityMatrix.Core/Models/Packages/FluxGym.cs +++ b/StabilityMatrix.Core/Models/Packages/FluxGym.cs @@ -47,19 +47,19 @@ IPrerequisiteHelper prerequisiteHelper new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.TextEncoders], - TargetRelativePaths = ["models/clip"] + TargetRelativePaths = ["models/clip"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.DiffusionModels], - TargetRelativePaths = ["models/unet"] + TargetRelativePaths = ["models/unet"], }, new SharedFolderLayoutRule { SourceTypes = [SharedFolderType.VAE], TargetRelativePaths = ["models/vae"], - } - ] + }, + ], }; public override Dictionary>? SharedOutputFolders => null; @@ -115,16 +115,20 @@ await sdsRequirements.ReadAllTextAsync(cancellationToken).ConfigureAwait(false), await venvRunner.PipInstall(sdsPipArgs, onConsoleOutput).ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Installing requirements", isIndeterminate: true)); + + var isLegacyNvidiaGpu = + SettingsManager.Settings.PreferredGpu?.IsLegacyNvidiaGpu() ?? HardwareHelper.HasLegacyNvidiaGpu(); + var requirements = new FilePath(installLocation, "requirements.txt"); var pipArgs = new PipInstallArgs() - .AddArg("--pre") .WithTorch() .WithTorchVision() .WithTorchAudio() - .WithTorchExtraIndex("cu121") + .WithTorchExtraIndex(isLegacyNvidiaGpu ? "cu126" : "cu128") + .AddArg("bitsandbytes>=0.46.0") .WithParsedFromRequirementsTxt( await requirements.ReadAllTextAsync(cancellationToken).ConfigureAwait(false), - "torch" + "torch$|bitsandbytes" ); if (installedPackage.PipOverrides != null) @@ -163,7 +167,7 @@ void HandleConsoleOutput(ProcessOutput s) } VenvRunner.RunDetached( - [Path.Combine(installLocation, options.Command ?? LaunchCommand), ..options.Arguments], + [Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments], HandleConsoleOutput, OnExit ); diff --git a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs index 366122678..ac7748506 100644 --- a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs +++ b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs @@ -214,20 +214,21 @@ private async Task SetupAndBuildInvokeFrontend( string installLocation, IProgress? progress, Action? onConsoleOutput, - IReadOnlyDictionary? envVars = null + IReadOnlyDictionary? envVars = null, + InstallPackageOptions? installOptions = null ) { await PrerequisiteHelper.InstallNodeIfNecessary(progress).ConfigureAwait(false); + + var pnpmVersion = installOptions?.VersionOptions.VersionTag?.Contains("v5") == true ? "8" : "10"; + await PrerequisiteHelper - .RunNpm(["i", "pnpm@8"], installLocation, envVars: envVars) + .RunNpm(["i", $"pnpm@{pnpmVersion}"], installLocation, envVars: envVars) .ConfigureAwait(false); - if (Compat.IsMacOS || Compat.IsLinux) - { - await PrerequisiteHelper - .RunNpm(["i", "vite", "--ignore-scripts=true"], installLocation, envVars: envVars) - .ConfigureAwait(false); - } + await PrerequisiteHelper + .RunNpm(["i", "vite", "--ignore-scripts=true"], installLocation, envVars: envVars) + .ConfigureAwait(false); var pnpmPath = Path.Combine( installLocation, @@ -257,7 +258,7 @@ await PrerequisiteHelper process = ProcessRunner.StartAnsiProcess( Compat.IsWindows ? pnpmPath : vitePath, - "build", + Compat.IsWindows ? "vite build" : "build", invokeFrontendPath, onConsoleOutput, envVars diff --git a/StabilityMatrix.Core/Models/Packages/KohyaSs.cs b/StabilityMatrix.Core/Models/Packages/KohyaSs.cs index 84ad1312e..3379b4070 100644 --- a/StabilityMatrix.Core/Models/Packages/KohyaSs.cs +++ b/StabilityMatrix.Core/Models/Packages/KohyaSs.cs @@ -49,51 +49,51 @@ IPyRunner runner Name = "Listen Address", Type = LaunchOptionType.String, DefaultValue = "127.0.0.1", - Options = ["--listen"] + Options = ["--listen"], }, new LaunchOptionDefinition { Name = "Port", Type = LaunchOptionType.String, - Options = ["--port"] + Options = ["--port"], }, new LaunchOptionDefinition { Name = "Username", Type = LaunchOptionType.String, - Options = ["--username"] + Options = ["--username"], }, new LaunchOptionDefinition { Name = "Password", Type = LaunchOptionType.String, - Options = ["--password"] + Options = ["--password"], }, new LaunchOptionDefinition { Name = "Auto-Launch Browser", Type = LaunchOptionType.Bool, - Options = ["--inbrowser"] + Options = ["--inbrowser"], }, new LaunchOptionDefinition { Name = "Share", Type = LaunchOptionType.Bool, - Options = ["--share"] + Options = ["--share"], }, new LaunchOptionDefinition { Name = "Headless", Type = LaunchOptionType.Bool, - Options = ["--headless"] + Options = ["--headless"], }, new LaunchOptionDefinition { Name = "Language", Type = LaunchOptionType.String, - Options = ["--language"] + Options = ["--language"], }, - LaunchOptionDefinition.Extras + LaunchOptionDefinition.Extras, ]; public override async Task InstallPackage( @@ -143,7 +143,7 @@ await PrerequisiteHelper .WithTorch() .WithTorchVision() .WithTorchAudio() - .WithXFormers() + .WithXFormers(">=0.0.30") .WithTorchExtraIndex(torchExtraIndex) .AddArg("--force-reinstall"); @@ -215,7 +215,7 @@ void HandleConsoleOutput(ProcessOutput s) } VenvRunner.RunDetached( - [Path.Combine(installLocation, options.Command ?? LaunchCommand), ..options.Arguments], + [Path.Combine(installLocation, options.Command ?? LaunchCommand), .. options.Arguments], HandleConsoleOutput, OnExit ); diff --git a/StabilityMatrix.Core/Models/Settings/Settings.cs b/StabilityMatrix.Core/Models/Settings/Settings.cs index 162f090db..0c40ae656 100644 --- a/StabilityMatrix.Core/Models/Settings/Settings.cs +++ b/StabilityMatrix.Core/Models/Settings/Settings.cs @@ -217,6 +217,8 @@ public IReadOnlyDictionary EnvironmentVariables public bool FilterExtraNetworksByBaseModel { get; set; } = true; + public bool IsMainWindowSidebarOpen { get; set; } + [JsonIgnore] public bool IsHolidayModeActive => HolidayModeSetting == HolidayMode.Automatic