From 070f80aafcf1930523ff9af65cad3ebb345bc3fc Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 7 Oct 2023 02:02:28 -0700 Subject: [PATCH 01/43] Added toggle for using Shared Output folder on packages tab --- .../Languages/Resources.Designer.cs | 9 ++++++ .../Languages/Resources.resx | 3 ++ .../PackageManager/PackageCardViewModel.cs | 31 ++++++++++++++++++ .../Views/PackageManagerPage.axaml | 10 ++++++ StabilityMatrix.Core/Helper/SharedFolders.cs | 28 +++++++++++----- .../Models/InstalledPackage.cs | 1 + .../Models/Packages/A3WebUI.cs | 11 +++++++ .../Models/Packages/BaseGitPackage.cs | 32 ++++++++++++++++++- .../Models/Packages/BasePackage.cs | 9 +++++- .../Models/Packages/ComfyUI.cs | 3 ++ .../Models/Packages/DankDiffusion.cs | 6 ++++ .../Models/Packages/Fooocus.cs | 3 ++ .../Models/Packages/FooocusMre.cs | 3 ++ .../Models/Packages/InvokeAI.cs | 3 ++ .../Models/Packages/UnknownPackage.cs | 16 ++++++++++ .../Models/Packages/VladAutomatic.cs | 11 +++++++ .../Models/Packages/VoltaML.cs | 8 +++++ .../Models/SharedOutputType.cs | 11 +++++++ .../Services/ISettingsManager.cs | 1 + .../Services/SettingsManager.cs | 1 + 20 files changed, 190 insertions(+), 10 deletions(-) create mode 100644 StabilityMatrix.Core/Models/SharedOutputType.cs diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index 439a5da2..4bc22b99 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -1517,6 +1517,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Output Sharing. + /// + public static string Label_UseSharedOutputFolder { + get { + return ResourceManager.GetString("Label_UseSharedOutputFolder", resourceCulture); + } + } + /// /// Looks up a localized string similar to VAE. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index a1697964..06611182 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -678,4 +678,7 @@ Restore Default Layout + + Output Sharing + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs index a733bbdd..cca0b036 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs @@ -62,6 +62,9 @@ public partial class PackageCardViewModel : ProgressViewModel [ObservableProperty] private bool canUseConfigMethod; + [ObservableProperty] + private bool useSharedOutput; + public PackageCardViewModel( ILogger logger, IPackageFactory packageFactory, @@ -100,6 +103,7 @@ public partial class PackageCardViewModel : ProgressViewModel CanUseConfigMethod = basePackage?.AvailableSharedFolderMethods.Contains(SharedFolderMethod.Configuration) ?? false; + UseSharedOutput = Package?.UseSharedOutputFolder ?? false; } } @@ -367,6 +371,33 @@ public partial class PackageCardViewModel : ProgressViewModel public void ToggleSharedModelNone() => IsSharedModelDisabled = !IsSharedModelDisabled; + public void ToggleSharedOutput() => UseSharedOutput = !UseSharedOutput; + + partial void OnUseSharedOutputChanged(bool value) + { + if (Package == null) + return; + + if (value == Package.UseSharedOutputFolder) + return; + + using var st = settingsManager.BeginTransaction(); + Package.UseSharedOutputFolder = value; + + var basePackage = packageFactory[Package.PackageName!]; + if (basePackage == null) + return; + + if (value) + { + basePackage.SetupOutputFolderLinks(Package.FullPath!); + } + else + { + basePackage.RemoveOutputFolderLinks(Package.FullPath!); + } + } + // fake radio button stuff partial void OnIsSharedModelSymlinkChanged(bool oldValue, bool newValue) { diff --git a/StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml b/StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml index d2d60e39..7ab65538 100644 --- a/StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml +++ b/StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml @@ -97,6 +97,16 @@ + + + + + diff --git a/StabilityMatrix.Core/Helper/SharedFolders.cs b/StabilityMatrix.Core/Helper/SharedFolders.cs index aeb67c29..588fe3dc 100644 --- a/StabilityMatrix.Core/Helper/SharedFolders.cs +++ b/StabilityMatrix.Core/Helper/SharedFolders.cs @@ -45,10 +45,12 @@ public class SharedFolders : ISharedFolders /// Shared source (i.e. "Models/") /// Destination (i.e. "webui/models/lora") /// Whether to overwrite the destination if it exists + /// Whether to recursively delete the directory after moving data out of it public static async Task CreateOrUpdateLink( DirectoryPath sourceDir, DirectoryPath destinationDir, - bool overwrite = false + bool overwrite = false, + bool recursiveDelete = false ) { // Create source folder if it doesn't exist @@ -93,7 +95,7 @@ public class SharedFolders : ISharedFolders } Logger.Info($"Deleting existing empty folder at target {destinationDir}"); - await destinationDir.DeleteAsync(false).ConfigureAwait(false); + await destinationDir.DeleteAsync(recursiveDelete).ConfigureAwait(false); } } @@ -117,11 +119,13 @@ public class SharedFolders : ISharedFolders /// Updates or creates shared links for a package. /// Will attempt to move files from the destination to the source if the destination is not empty. /// - public static async Task UpdateLinksForPackage( - Dictionary> sharedFolders, + public static async Task UpdateLinksForPackage( + Dictionary> sharedFolders, DirectoryPath modelsDirectory, - DirectoryPath installDirectory + DirectoryPath installDirectory, + bool recursiveDelete = false ) + where T : Enum { foreach (var (folderType, relativePaths) in sharedFolders) { @@ -130,14 +134,22 @@ public class SharedFolders : ISharedFolders var sourceDir = new DirectoryPath(modelsDirectory, folderType.GetStringValue()); var destinationDir = installDirectory.JoinDir(relativePath); - await CreateOrUpdateLink(sourceDir, destinationDir).ConfigureAwait(false); + await CreateOrUpdateLink( + sourceDir, + destinationDir, + recursiveDelete: recursiveDelete + ) + .ConfigureAwait(false); } } } - public static void RemoveLinksForPackage(BasePackage package, DirectoryPath installPath) + public static void RemoveLinksForPackage( + Dictionary>? sharedFolders, + DirectoryPath installPath + ) + where T : Enum { - var sharedFolders = package.SharedFolders; if (sharedFolders == null) { return; diff --git a/StabilityMatrix.Core/Models/InstalledPackage.cs b/StabilityMatrix.Core/Models/InstalledPackage.cs index 4695bb12..557d07ab 100644 --- a/StabilityMatrix.Core/Models/InstalledPackage.cs +++ b/StabilityMatrix.Core/Models/InstalledPackage.cs @@ -50,6 +50,7 @@ public class InstalledPackage : IJsonOnDeserialized public bool UpdateAvailable { get; set; } public TorchVersion? PreferredTorchVersion { get; set; } public SharedFolderMethod? PreferredSharedFolderMethod { get; set; } + public bool UseSharedOutputFolder { get; set; } /// /// Get the launch args host option value. diff --git a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs index 270d8cc8..51db6842 100644 --- a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs +++ b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs @@ -61,6 +61,17 @@ public class A3WebUI : BaseGitPackage [SharedFolderType.AfterDetailer] = new[] { "models/adetailer" } }; + public override Dictionary>? SharedOutputFolders => + new() + { + [SharedOutputType.Extras] = new[] { "outputs/extras-images" }, + [SharedOutputType.Saved] = new[] { "log/images" }, + [SharedOutputType.Img2Img] = new[] { "outputs/img2img-images" }, + [SharedOutputType.Text2Img] = new[] { "outputs/text2img-images" }, + [SharedOutputType.Img2ImgGrids] = new[] { "outputs/img2img-grids" }, + [SharedOutputType.Text2ImgGrids] = new[] { "outputs/text2img-grids" } + }; + [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")] public override List LaunchOptions => new() diff --git a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs index dab562cc..98588563 100644 --- a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs @@ -372,7 +372,37 @@ public abstract class BaseGitPackage : BasePackage { if (SharedFolders is not null && sharedFolderMethod == SharedFolderMethod.Symlink) { - StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(this, installDirectory); + StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage( + SharedFolders, + installDirectory + ); + } + return Task.CompletedTask; + } + + public override Task SetupOutputFolderLinks(DirectoryPath installDirectory) + { + if (SharedOutputFolders is { } sharedOutputFolders) + { + return StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage( + sharedOutputFolders, + SettingsManager.OutputDirectory, + installDirectory, + recursiveDelete: true + ); + } + + return Task.CompletedTask; + } + + public override Task RemoveOutputFolderLinks(DirectoryPath installDirectory) + { + if (SharedOutputFolders is { } sharedOutputFolders) + { + StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage( + sharedOutputFolders, + installDirectory + ); } return Task.CompletedTask; } diff --git a/StabilityMatrix.Core/Models/Packages/BasePackage.cs b/StabilityMatrix.Core/Models/Packages/BasePackage.cs index 2c224ad3..9b85f729 100644 --- a/StabilityMatrix.Core/Models/Packages/BasePackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BasePackage.cs @@ -93,6 +93,9 @@ public abstract class BasePackage SharedFolderMethod sharedFolderMethod ); + public abstract Task SetupOutputFolderLinks(DirectoryPath installDirectory); + public abstract Task RemoveOutputFolderLinks(DirectoryPath installDirectory); + public abstract IEnumerable AvailableTorchVersions { get; } public virtual TorchVersion GetRecommendedTorchVersion() @@ -142,7 +145,11 @@ public abstract class BasePackage /// The shared folders that this package supports. /// Mapping of to the relative paths from the package root. /// - public virtual Dictionary>? SharedFolders { get; } + public abstract Dictionary>? SharedFolders { get; } + public abstract Dictionary< + SharedOutputType, + IReadOnlyList + >? SharedOutputFolders { get; } public abstract Task GetLatestVersion(); public abstract Task GetAllVersionOptions(); diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index af3881fb..6de64dc9 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -58,6 +58,9 @@ public class ComfyUI : BaseGitPackage [SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }, }; + public override Dictionary>? SharedOutputFolders => + new() { [SharedOutputType.Text2Img] = new[] { "output" } }; + public override List LaunchOptions => new List { diff --git a/StabilityMatrix.Core/Models/Packages/DankDiffusion.cs b/StabilityMatrix.Core/Models/Packages/DankDiffusion.cs index ea1e34b5..8149eed9 100644 --- a/StabilityMatrix.Core/Models/Packages/DankDiffusion.cs +++ b/StabilityMatrix.Core/Models/Packages/DankDiffusion.cs @@ -68,6 +68,12 @@ public class DankDiffusion : BaseGitPackage public override List LaunchOptions { get; } + public override Dictionary>? SharedFolders { get; } + public override Dictionary< + SharedOutputType, + IReadOnlyList + >? SharedOutputFolders { get; } + public override Task GetLatestVersion() { throw new NotImplementedException(); diff --git a/StabilityMatrix.Core/Models/Packages/Fooocus.cs b/StabilityMatrix.Core/Models/Packages/Fooocus.cs index 424f3553..85c50d97 100644 --- a/StabilityMatrix.Core/Models/Packages/Fooocus.cs +++ b/StabilityMatrix.Core/Models/Packages/Fooocus.cs @@ -83,6 +83,9 @@ public class Fooocus : BaseGitPackage [SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" } }; + public override Dictionary>? SharedOutputFolders => + new() { [SharedOutputType.Text2Img] = new[] { "outputs" } }; + public override IEnumerable AvailableTorchVersions => new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm }; diff --git a/StabilityMatrix.Core/Models/Packages/FooocusMre.cs b/StabilityMatrix.Core/Models/Packages/FooocusMre.cs index fb65ce28..273dab5d 100644 --- a/StabilityMatrix.Core/Models/Packages/FooocusMre.cs +++ b/StabilityMatrix.Core/Models/Packages/FooocusMre.cs @@ -85,6 +85,9 @@ public class FooocusMre : BaseGitPackage [SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" } }; + public override Dictionary>? SharedOutputFolders => + new() { [SharedOutputType.Text2Img] = new[] { "outputs" } }; + public override IEnumerable AvailableTorchVersions => new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm }; diff --git a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs index 0284143c..9a3a4607 100644 --- a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs +++ b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs @@ -69,6 +69,9 @@ public class InvokeAI : BaseGitPackage [SharedFolderType.ControlNet] = new[] { RelativeRootPath + "/autoimport/controlnet" }, }; + public override Dictionary>? SharedOutputFolders => + new() { [SharedOutputType.Text2Img] = new[] { "invokeai-root/outputs/images" } }; + // https://github.com/invoke-ai/InvokeAI/blob/main/docs/features/CONFIGURATION.md public override List LaunchOptions => new List diff --git a/StabilityMatrix.Core/Models/Packages/UnknownPackage.cs b/StabilityMatrix.Core/Models/Packages/UnknownPackage.cs index 5be7baf0..2061b6fe 100644 --- a/StabilityMatrix.Core/Models/Packages/UnknownPackage.cs +++ b/StabilityMatrix.Core/Models/Packages/UnknownPackage.cs @@ -83,6 +83,16 @@ public class UnknownPackage : BasePackage throw new NotImplementedException(); } + public override Task SetupOutputFolderLinks(DirectoryPath installDirectory) + { + throw new NotImplementedException(); + } + + public override Task RemoveOutputFolderLinks(DirectoryPath installDirectory) + { + throw new NotImplementedException(); + } + public override IEnumerable AvailableTorchVersions => new[] { TorchVersion.Cuda, TorchVersion.Cpu, TorchVersion.Rocm, TorchVersion.DirectMl }; @@ -122,6 +132,12 @@ public class UnknownPackage : BasePackage public override List LaunchOptions => new(); + public override Dictionary>? SharedFolders { get; } + public override Dictionary< + SharedOutputType, + IReadOnlyList + >? SharedOutputFolders { get; } + public override Task GetLatestVersion() => Task.FromResult(string.Empty); public override Task GetAllVersionOptions() => diff --git a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs index 08fd6fe9..cdfb06bb 100644 --- a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs +++ b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs @@ -67,6 +67,17 @@ public class VladAutomatic : BaseGitPackage [SharedFolderType.ControlNet] = new[] { "models/ControlNet" } }; + public override Dictionary>? SharedOutputFolders => + new() + { + [SharedOutputType.Text2Img] = new[] { "outputs/text" }, + [SharedOutputType.Img2Img] = new[] { "outputs/image" }, + [SharedOutputType.Extras] = new[] { "outputs/extras" }, + [SharedOutputType.Img2ImgGrids] = new[] { "outputs/grids" }, + [SharedOutputType.Text2ImgGrids] = new[] { "outputs/grids" }, + [SharedOutputType.Saved] = new[] { "outputs/save" }, + }; + [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")] public override List LaunchOptions => new() diff --git a/StabilityMatrix.Core/Models/Packages/VoltaML.cs b/StabilityMatrix.Core/Models/Packages/VoltaML.cs index b8ff9733..a6dd02e5 100644 --- a/StabilityMatrix.Core/Models/Packages/VoltaML.cs +++ b/StabilityMatrix.Core/Models/Packages/VoltaML.cs @@ -45,6 +45,14 @@ public class VoltaML : BaseGitPackage [SharedFolderType.TextualInversion] = new[] { "data/textual-inversion" }, }; + public override Dictionary>? SharedOutputFolders => + new() + { + [SharedOutputType.Text2Img] = new[] { "outputs/txt2img" }, + [SharedOutputType.Extras] = new[] { "outputs/extra" }, + [SharedOutputType.Img2Img] = new[] { "outputs/img2img" }, + }; + public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override IEnumerable AvailableTorchVersions => new[] { TorchVersion.None }; diff --git a/StabilityMatrix.Core/Models/SharedOutputType.cs b/StabilityMatrix.Core/Models/SharedOutputType.cs new file mode 100644 index 00000000..0075ebe0 --- /dev/null +++ b/StabilityMatrix.Core/Models/SharedOutputType.cs @@ -0,0 +1,11 @@ +namespace StabilityMatrix.Core.Models; + +public enum SharedOutputType +{ + Text2Img, + Img2Img, + Extras, + Text2ImgGrids, + Img2ImgGrids, + Saved +} diff --git a/StabilityMatrix.Core/Services/ISettingsManager.cs b/StabilityMatrix.Core/Services/ISettingsManager.cs index 462469ae..879b5126 100644 --- a/StabilityMatrix.Core/Services/ISettingsManager.cs +++ b/StabilityMatrix.Core/Services/ISettingsManager.cs @@ -21,6 +21,7 @@ public interface ISettingsManager List PackageInstallsInProgress { get; set; } Settings Settings { get; } + string OutputDirectory { get; } /// /// Event fired when the library directory is changed diff --git a/StabilityMatrix.Core/Services/SettingsManager.cs b/StabilityMatrix.Core/Services/SettingsManager.cs index 30695720..e005f32f 100644 --- a/StabilityMatrix.Core/Services/SettingsManager.cs +++ b/StabilityMatrix.Core/Services/SettingsManager.cs @@ -63,6 +63,7 @@ public class SettingsManager : ISettingsManager private string SettingsPath => Path.Combine(LibraryDir, "settings.json"); public string ModelsDirectory => Path.Combine(LibraryDir, "Models"); public string DownloadsDirectory => Path.Combine(LibraryDir, ".downloads"); + public string OutputDirectory => Path.Combine(LibraryDir, "Output"); public List PackageInstallsInProgress { get; set; } = new(); public DirectoryPath TagsDirectory => new(LibraryDir, "Tags"); From dca0df35c2e7306efe326546f4d0167e01e6eeb1 Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 7 Oct 2023 11:49:37 -0700 Subject: [PATCH 02/43] preliminary/poc output viewer --- StabilityMatrix.Avalonia/App.axaml.cs | 5 +- .../DesignData/DesignData.cs | 3 + .../ViewModels/OutputsPageViewModel.cs | 68 +++++++++++++++++++ .../Views/OutputsPage.axaml | 49 +++++++++++++ .../Views/OutputsPage.axaml.cs | 14 ++++ 5 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs create mode 100644 StabilityMatrix.Avalonia/Views/OutputsPage.axaml create mode 100644 StabilityMatrix.Avalonia/Views/OutputsPage.axaml.cs diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 2633c3f0..b28534c5 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -259,7 +259,8 @@ public sealed class App : Application .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); services.AddSingleton( provider => @@ -278,6 +279,7 @@ public sealed class App : Application provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), + provider.GetRequiredService() }, FooterPages = { provider.GetRequiredService() } } @@ -388,6 +390,7 @@ public sealed class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Inference tabs services.AddTransient(); diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 7a5e9b0d..24c50441 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -336,6 +336,9 @@ public static class DesignData public static LaunchPageViewModel LaunchPageViewModel => Services.GetRequiredService(); + public static OutputsPageViewModel OutputsPageViewModel => + Services.GetRequiredService(); + public static PackageManagerViewModel PackageManagerViewModel { get diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs new file mode 100644 index 00000000..c0ba14b8 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using CommunityToolkit.Mvvm.ComponentModel; +using DynamicData; +using DynamicData.Alias; +using DynamicData.Binding; +using FluentAvalonia.UI.Controls; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Avalonia.Views; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Services; +using Symbol = FluentIcons.Common.Symbol; +using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; + +namespace StabilityMatrix.Avalonia.ViewModels; + +[View(typeof(OutputsPage))] +public partial class OutputsPageViewModel : PageViewModelBase +{ + private readonly ISettingsManager settingsManager; + public override string Title => "Outputs"; + public override IconSource IconSource => + new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true }; + + public SourceCache OutputsCache { get; } = new(p => p.FullName); + public IObservableCollection Outputs { get; } = + new ObservableCollectionExtended(); + + public OutputsPageViewModel(ISettingsManager settingsManager) + { + this.settingsManager = settingsManager; + + OutputsCache + .Connect() + .DeferUntilLoaded() + .SortBy(x => x.CreationTime, SortDirection.Descending) + .Select(x => x.FullName) + .Bind(Outputs) + .Subscribe(); + } + + public override void OnLoaded() + { + GetOutputs(); + } + + private void GetOutputs() + { + if (!settingsManager.IsLibraryDirSet) + return; + + foreach ( + var file in Directory.EnumerateFiles( + settingsManager.OutputDirectory, + "*.*", + SearchOption.AllDirectories + ) + ) + { + var fileInfo = new FileInfo(file); + if (!fileInfo.Extension.Contains("png")) + continue; + + OutputsCache.AddOrUpdate(fileInfo); + } + } +} diff --git a/StabilityMatrix.Avalonia/Views/OutputsPage.axaml b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml new file mode 100644 index 00000000..647f69c5 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml @@ -0,0 +1,49 @@ + + + + + Test + Test 2 + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/OutputsPage.axaml.cs b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml.cs new file mode 100644 index 00000000..e14c5818 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using StabilityMatrix.Avalonia.Controls; + +namespace StabilityMatrix.Avalonia.Views; + +public partial class OutputsPage : UserControlBase +{ + public OutputsPage() + { + InitializeComponent(); + } +} From f95ee83e079428ba8405fcd39764e39f47302c8d Mon Sep 17 00:00:00 2001 From: JT Date: Mon, 9 Oct 2023 18:57:02 -0700 Subject: [PATCH 03/43] update chagenlog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a829382..805e347b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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.6.0 +### Added +- Added "Output Sharing" option for all packages in the three-dots menu on the Packages page + - This will link the package's output folders to the relevant subfolders in the "Outputs" directory + - When a package only has a generic "outputs" folder, all generated images from that package will be linked to the "Outputs\Text2Img" folder when this option is enabled +- Added "Outputs" page for viewing generated images from any package, or the shared output folder + ## v2.5.2 ### Added - Right click Inference Batch options to enable selecting a "Batch Index". This can be used to reproduce a specific image from a batch generation. The field will be automatically populated in metadata of individual images from a batch generation. From eb2e2135cf28f28d8d7f93afba6008a059adaa08 Mon Sep 17 00:00:00 2001 From: JT Date: Mon, 9 Oct 2023 22:24:03 -0700 Subject: [PATCH 04/43] use Images folder instead of Outputs & "handle" file conflicts --- .../ViewModels/OutputsPageViewModel.cs | 13 ++++++------- StabilityMatrix.Core/Helper/SharedFolders.cs | 1 + .../Models/FileInterfaces/FilePath.cs | 16 +++++++++++++++- .../Models/Packages/BaseGitPackage.cs | 2 +- .../Services/ISettingsManager.cs | 1 - StabilityMatrix.Core/Services/SettingsManager.cs | 1 - 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs index 3a82ad20..9bd7adee 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs @@ -39,7 +39,6 @@ namespace StabilityMatrix.Avalonia.ViewModels; public partial class OutputsPageViewModel : PageViewModelBase { private readonly ISettingsManager settingsManager; - private readonly IPackageFactory packageFactory; private readonly INotificationService notificationService; private readonly INavigationService navigationService; public override string Title => "Outputs"; @@ -74,7 +73,6 @@ public partial class OutputsPageViewModel : PageViewModelBase ) { this.settingsManager = settingsManager; - this.packageFactory = packageFactory; this.notificationService = notificationService; this.navigationService = navigationService; @@ -108,7 +106,7 @@ public partial class OutputsPageViewModel : PageViewModelBase 0, new PackageOutputCategory { - Path = settingsManager.OutputDirectory, + Path = settingsManager.ImagesDirectory, Name = "Shared Output Folder" } ); @@ -134,9 +132,10 @@ public partial class OutputsPageViewModel : PageViewModelBase if (oldValue == newValue || newValue == null) return; - var path = CanShowOutputTypes - ? Path.Combine(newValue.Path, SelectedOutputType.ToString()) - : SelectedCategory.Path; + var path = + CanShowOutputTypes && SelectedOutputType != SharedOutputType.All + ? Path.Combine(newValue.Path, SelectedOutputType.ToString()) + : SelectedCategory.Path; GetOutputs(path); } @@ -249,7 +248,7 @@ public partial class OutputsPageViewModel : PageViewModelBase if (!settingsManager.IsLibraryDirSet) return; - if (!Directory.Exists(directory)) + if (!Directory.Exists(directory) && SelectedOutputType != SharedOutputType.All) { Directory.CreateDirectory(directory); return; diff --git a/StabilityMatrix.Core/Helper/SharedFolders.cs b/StabilityMatrix.Core/Helper/SharedFolders.cs index 588fe3dc..e9406527 100644 --- a/StabilityMatrix.Core/Helper/SharedFolders.cs +++ b/StabilityMatrix.Core/Helper/SharedFolders.cs @@ -76,6 +76,7 @@ public class SharedFolders : ISharedFolders // Otherwise delete the link Logger.Info($"Deleting existing junction at target {destinationDir}"); + destinationDir.Info.Attributes = FileAttributes.Normal; await destinationDir.DeleteAsync(false).ConfigureAwait(false); } else diff --git a/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs b/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs index 53873549..c73b2d8f 100644 --- a/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs +++ b/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs @@ -161,7 +161,21 @@ public class FilePath : FileSystemPath, IPathObject /// public async Task MoveToAsync(FilePath destinationFile) { - await Task.Run(() => Info.MoveTo(destinationFile.FullPath)).ConfigureAwait(false); + await Task.Run(() => + { + var path = destinationFile.FullPath; + if (destinationFile.Exists) + { + var num = Random.Shared.NextInt64(0, 10000); + path = path.Replace( + destinationFile.NameWithoutExtension, + $"{destinationFile.NameWithoutExtension}_{num}" + ); + } + + Info.MoveTo(path); + }) + .ConfigureAwait(false); // Return the new path return destinationFile; } diff --git a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs index 98588563..9af5e597 100644 --- a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs @@ -386,7 +386,7 @@ public abstract class BaseGitPackage : BasePackage { return StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage( sharedOutputFolders, - SettingsManager.OutputDirectory, + SettingsManager.ImagesDirectory, installDirectory, recursiveDelete: true ); diff --git a/StabilityMatrix.Core/Services/ISettingsManager.cs b/StabilityMatrix.Core/Services/ISettingsManager.cs index 879b5126..462469ae 100644 --- a/StabilityMatrix.Core/Services/ISettingsManager.cs +++ b/StabilityMatrix.Core/Services/ISettingsManager.cs @@ -21,7 +21,6 @@ public interface ISettingsManager List PackageInstallsInProgress { get; set; } Settings Settings { get; } - string OutputDirectory { get; } /// /// Event fired when the library directory is changed diff --git a/StabilityMatrix.Core/Services/SettingsManager.cs b/StabilityMatrix.Core/Services/SettingsManager.cs index e005f32f..30695720 100644 --- a/StabilityMatrix.Core/Services/SettingsManager.cs +++ b/StabilityMatrix.Core/Services/SettingsManager.cs @@ -63,7 +63,6 @@ public class SettingsManager : ISettingsManager private string SettingsPath => Path.Combine(LibraryDir, "settings.json"); public string ModelsDirectory => Path.Combine(LibraryDir, "Models"); public string DownloadsDirectory => Path.Combine(LibraryDir, ".downloads"); - public string OutputDirectory => Path.Combine(LibraryDir, "Output"); public List PackageInstallsInProgress { get; set; } = new(); public DirectoryPath TagsDirectory => new(LibraryDir, "Tags"); From bedb67433b1545d7d4bdf26a41a9e1393dbcfb37 Mon Sep 17 00:00:00 2001 From: JT Date: Mon, 9 Oct 2023 23:10:45 -0700 Subject: [PATCH 05/43] don't change output type selection on page load --- .../ViewModels/OutputsPageViewModel.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs index 9bd7adee..209fd099 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs @@ -8,15 +8,12 @@ using System.Threading.Tasks; using AsyncAwaitBestPractices; using AsyncImageLoader; using Avalonia.Controls; -using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; -using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Extensions; -using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; @@ -121,7 +118,11 @@ public partial class OutputsPageViewModel : PageViewModelBase if (Design.IsDesignMode) return; - GetOutputs(SelectedCategory.Path); + var path = + CanShowOutputTypes && SelectedOutputType != SharedOutputType.All + ? Path.Combine(SelectedCategory.Path, SelectedOutputType.ToString()) + : SelectedCategory.Path; + GetOutputs(path); } partial void OnSelectedCategoryChanged( From ed882f83e50ef421109239e8f09e2710ef648875 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 10 Oct 2023 03:20:48 -0400 Subject: [PATCH 06/43] Fix crashes during gallery operations if file is externally deleted --- .../Inference/ImageFolderCardViewModel.cs | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs index e8097834..f88ebd6d 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs @@ -4,7 +4,6 @@ using System.Reactive.Linq; using System.Threading.Tasks; using AsyncAwaitBestPractices; using AsyncImageLoader; -using Avalonia; using Avalonia.Controls.Notifications; using Avalonia.Platform.Storage; using Avalonia.Threading; @@ -120,20 +119,50 @@ public partial class ImageFolderCardViewModel : ViewModelBase imageIndexService.RefreshIndexForAllCollections().SafeFireAndForget(); } + /// + /// Gets the image path if it exists, returns null. + /// If the image path is resolved but the file doesn't exist, it will be removed from the index. + /// + private FilePath? GetImagePathIfExists(LocalImageFile item) + { + if (item.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) + { + return null; + } + + var imageFile = new FilePath(imagePath); + + if (!imageFile.Exists) + { + // Remove from index + imageIndexService.InferenceImages.Remove(item); + + // Invalidate cache + if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) + { + loader.RemoveAllNamesFromCache(imageFile.Name); + } + + return null; + } + + return imageFile; + } + /// /// Handles image clicks to show preview /// [RelayCommand] private async Task OnImageClick(LocalImageFile item) { - if (item.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) + if (GetImagePathIfExists(item) is not { } imageFile) { return; } var currentIndex = LocalImages.IndexOf(item); - var image = new ImageSource(new FilePath(imagePath)); + var image = new ImageSource(imageFile); // Preload await image.GetBitmapAsync(); @@ -163,7 +192,7 @@ public partial class ImageFolderCardViewModel : ViewModelBase // Preload await newImageSource.GetBitmapAsync(); - var oldImageSource = sender.ImageSource; + // var oldImageSource = sender.ImageSource; sender.ImageSource = newImageSource; sender.LocalImageFile = newImage; @@ -185,13 +214,12 @@ public partial class ImageFolderCardViewModel : ViewModelBase [RelayCommand] private async Task OnImageDelete(LocalImageFile? item) { - if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) + if (item is null || GetImagePathIfExists(item) is not { } imageFile) { return; } // Delete the file - var imageFile = new FilePath(imagePath); var result = await notificationService.TryAsync(imageFile.DeleteAsync()); if (!result.IsSuccessful) @@ -215,14 +243,14 @@ public partial class ImageFolderCardViewModel : ViewModelBase [RelayCommand] private async Task OnImageCopy(LocalImageFile? item) { - if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) + if (item is null || GetImagePathIfExists(item) is not { } imageFile) { return; } var clipboard = App.Clipboard; - await clipboard.SetFileDataObjectAsync(imagePath); + await clipboard.SetFileDataObjectAsync(imageFile.FullPath); } /// @@ -231,12 +259,12 @@ public partial class ImageFolderCardViewModel : ViewModelBase [RelayCommand] private async Task OnImageOpen(LocalImageFile? item) { - if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) + if (item is null || GetImagePathIfExists(item) is not { } imageFile) { return; } - await ProcessRunner.OpenFileBrowser(imagePath); + await ProcessRunner.OpenFileBrowser(imageFile); } /// @@ -248,13 +276,11 @@ public partial class ImageFolderCardViewModel : ViewModelBase bool includeMetadata = false ) { - if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } sourcePath) + if (item is null || GetImagePathIfExists(item) is not { } sourceFile) { return; } - var sourceFile = new FilePath(sourcePath); - var formatName = format.ToString(); var storageFile = await App.StorageProvider.SaveFilePickerAsync( From c8331a85bde9a4d34914de0a385cfde6467cb333 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 10 Oct 2023 03:23:32 -0400 Subject: [PATCH 07/43] Add fix to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f70dba0..c8ac01e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Added French UI language option, thanks to eephyne for the translation ### Fixed - Fixed Automatic 1111 missing dependencies on startup by no longer enabling `--skip-install` by default. +- Fixed crash when clicking Inference gallery image after the image is deleted externally in file explorer ## v2.5.2 ### Added From 8c8ec8f2be18cba613481274d6db03118b2e2637 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 10 Oct 2023 18:13:14 -0400 Subject: [PATCH 08/43] Fix Sentry unhandled exception tagging --- StabilityMatrix.Avalonia/Program.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Program.cs b/StabilityMatrix.Avalonia/Program.cs index 16c0c3fd..98c126b9 100644 --- a/StabilityMatrix.Avalonia/Program.cs +++ b/StabilityMatrix.Avalonia/Program.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using AsyncAwaitBestPractices; using AsyncImageLoader; using Avalonia; using Avalonia.Controls; @@ -211,10 +212,16 @@ public class Program if (e.ExceptionObject is not Exception ex) return; - Logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message); + // Exception automatically logged by Sentry if enabled if (SentrySdk.IsEnabled) { + ex.SetSentryMechanism("AppDomain.UnhandledException", handled: false); SentrySdk.CaptureException(ex); + SentrySdk.FlushAsync().SafeFireAndForget(); + } + else + { + Logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message); } if ( @@ -269,6 +276,10 @@ public class Program [DoesNotReturn] private static void ExitWithException(Exception exception) { + if (SentrySdk.IsEnabled) + { + SentrySdk.Flush(); + } App.Shutdown(1); Dispatcher.UIThread.InvokeShutdown(); Environment.Exit(Marshal.GetHRForException(exception)); From e84e9ba35e0b9d4be51f4f3df160bec847fbdedd Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 10 Oct 2023 18:14:12 -0400 Subject: [PATCH 09/43] Move gallery fix to v2.5.4 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ac01e1..f1f31ecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,15 @@ 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.5.4 +### Fixed +- Fixed crash when clicking Inference gallery image after the image is deleted externally in file explorer + ## v2.5.3 ### Added - Added French UI language option, thanks to eephyne for the translation ### Fixed - Fixed Automatic 1111 missing dependencies on startup by no longer enabling `--skip-install` by default. -- Fixed crash when clicking Inference gallery image after the image is deleted externally in file explorer ## v2.5.2 ### Added From a40e4e5a989399ba43ab106b10814a7d7b9361fa Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 11 Oct 2023 23:57:22 -0400 Subject: [PATCH 10/43] Add file name formatting frameworks --- .../Models/Inference/FileNameFormat.cs | 68 +++++++++++ .../Models/Inference/FileNameFormatPart.cs | 5 + .../Inference/FileNameFormatProvider.cs | 111 ++++++++++++++++++ .../Avalonia/FileNameFormatProviderTests.cs | 25 ++++ .../Avalonia/FileNameFormatTests.cs | 24 ++++ 5 files changed, 233 insertions(+) create mode 100644 StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs create mode 100644 StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs create mode 100644 StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs create mode 100644 StabilityMatrix.Tests/Avalonia/FileNameFormatProviderTests.cs create mode 100644 StabilityMatrix.Tests/Avalonia/FileNameFormatTests.cs diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs new file mode 100644 index 00000000..28e9a35a --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace StabilityMatrix.Avalonia.Models.Inference; + +public record FileNameFormat +{ + public string Template { get; } + + public string Prefix { get; set; } = ""; + + public string Postfix { get; set; } = ""; + + public IReadOnlyList Parts { get; } + + private FileNameFormat(string template, IReadOnlyList parts) + { + Template = template; + Parts = parts; + } + + public FileNameFormat WithBatchPostFix(int current, int total) + { + return this with { Postfix = Postfix + $" ({current}-{total})" }; + } + + public FileNameFormat WithGridPrefix() + { + return this with { Prefix = Prefix + "Grid_" }; + } + + public string GetFileName() + { + return Prefix + + string.Join("", Parts.Select(p => p.Constant ?? p.Substitution?.Invoke() ?? "")) + + Postfix; + } + + public static FileNameFormat Parse(string template, FileNameFormatProvider provider) + { + provider.Validate(template); + var parts = provider.GetParts(template).ToImmutableArray(); + return new FileNameFormat(template, parts); + } + + public static bool TryParse( + string template, + FileNameFormatProvider provider, + [NotNullWhen(true)] out FileNameFormat? format + ) + { + try + { + format = Parse(template, provider); + return true; + } + catch (ArgumentException) + { + format = null; + return false; + } + } + + public const string DefaultTemplate = "{date}_{time}-{model_name}-{seed}"; +} diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs new file mode 100644 index 00000000..bfbcc8d9 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs @@ -0,0 +1,5 @@ +using System; + +namespace StabilityMatrix.Avalonia.Models.Inference; + +public record FileNameFormatPart(string? Constant, Func? Substitution); diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs new file mode 100644 index 00000000..e6ecc563 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using StabilityMatrix.Core.Extensions; +using StabilityMatrix.Core.Models; + +namespace StabilityMatrix.Avalonia.Models.Inference; + +public partial class FileNameFormatProvider +{ + public GenerationParameters? GenerationParameters { get; init; } + + public InferenceProjectType? ProjectType { get; init; } + + public string? ProjectName { get; init; } + + private Dictionary>? _substitutions; + + private Dictionary> Substitutions => + _substitutions ??= new Dictionary> + { + { "seed", () => GenerationParameters?.Seed.ToString() }, + { "model_name", () => GenerationParameters?.ModelName }, + { "model_hash", () => GenerationParameters?.ModelHash }, + { "width", () => GenerationParameters?.Width.ToString() }, + { "height", () => GenerationParameters?.Height.ToString() }, + { "project_type", () => ProjectType?.GetStringValue() }, + { "project_name", () => ProjectName }, + { "date", () => DateTime.Now.ToString("yyyy-MM-dd") }, + { "time", () => DateTime.Now.ToString("HH-mm-ss") } + }; + + public (int Current, int Total)? BatchInfo { get; init; } + + /// + /// Validate a format string + /// + public void Validate(string format) + { + var regex = BracketRegex(); + var matches = regex.Matches(format); + var variables = matches.Select(m => m.Value[1..^1]).ToList(); + + foreach (var variable in variables) + { + if (!Substitutions.ContainsKey(variable)) + { + throw new ArgumentException($"Unknown variable '{variable}'"); + } + } + } + + public IEnumerable GetParts(string template) + { + var regex = BracketRegex(); + var matches = regex.Matches(template); + + var parts = new List(); + + // Loop through all parts of the string, including matches and non-matches + var currentIndex = 0; + + foreach (var result in matches.Cast()) + { + // If the match is not at the start of the string, add a constant part + if (result.Index != currentIndex) + { + var constant = template[currentIndex..result.Index]; + parts.Add(new FileNameFormatPart(constant, null)); + + currentIndex += constant.Length; + } + + var variable = result.Value[1..^1]; + parts.Add(new FileNameFormatPart(null, Substitutions[variable])); + + currentIndex += result.Length; + } + + // Add remaining as constant + if (currentIndex != template.Length) + { + var constant = template[currentIndex..]; + parts.Add(new FileNameFormatPart(constant, null)); + } + + return parts; + } + + /// + /// Return a string substituting the variables in the format string + /// + private string? GetSubstitution(string variable) + { + return variable switch + { + "seed" => GenerationParameters.Seed.ToString(), + "model_name" => GenerationParameters.ModelName, + "model_hash" => GenerationParameters.ModelHash, + "width" => GenerationParameters.Width.ToString(), + "height" => GenerationParameters.Height.ToString(), + "date" => DateTime.Now.ToString("yyyy-MM-dd"), + "time" => DateTime.Now.ToString("HH-mm-ss"), + _ => throw new ArgumentOutOfRangeException(nameof(variable), variable, null) + }; + } + + [GeneratedRegex(@"\{[a-z_]+\}")] + private static partial Regex BracketRegex(); +} diff --git a/StabilityMatrix.Tests/Avalonia/FileNameFormatProviderTests.cs b/StabilityMatrix.Tests/Avalonia/FileNameFormatProviderTests.cs new file mode 100644 index 00000000..cdf7fdfa --- /dev/null +++ b/StabilityMatrix.Tests/Avalonia/FileNameFormatProviderTests.cs @@ -0,0 +1,25 @@ +using StabilityMatrix.Avalonia.Models.Inference; + +namespace StabilityMatrix.Tests.Avalonia; + +[TestClass] +public class FileNameFormatProviderTests +{ + [TestMethod] + public void TestFileNameFormatProviderValidate_Valid_ShouldNotThrow() + { + var provider = new FileNameFormatProvider(); + + provider.Validate("{date}_{time}-{model_name}-{seed}"); + } + + [TestMethod] + public void TestFileNameFormatProviderValidate_Invalid_ShouldThrow() + { + var provider = new FileNameFormatProvider(); + + Assert.ThrowsException( + () => provider.Validate("{date}_{time}-{model_name}-{seed}-{invalid}") + ); + } +} diff --git a/StabilityMatrix.Tests/Avalonia/FileNameFormatTests.cs b/StabilityMatrix.Tests/Avalonia/FileNameFormatTests.cs new file mode 100644 index 00000000..0da1eb84 --- /dev/null +++ b/StabilityMatrix.Tests/Avalonia/FileNameFormatTests.cs @@ -0,0 +1,24 @@ +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Models.Inference; +using StabilityMatrix.Core.Models; + +namespace StabilityMatrix.Tests.Avalonia; + +[TestClass] +public class FileNameFormatTests +{ + [TestMethod] + public void TestFileNameFormatParse() + { + var provider = new FileNameFormatProvider + { + GenerationParameters = new GenerationParameters { Seed = 123 }, + ProjectName = "uwu", + ProjectType = InferenceProjectType.TextToImage, + }; + + var format = FileNameFormat.Parse("{project_type} - {project_name} ({seed})", provider); + + Assert.AreEqual("TextToImage - uwu (123)", format.GetFileName()); + } +} From 10141c00a12e08fdb9d536619471186fc8ece58c Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 11 Oct 2023 23:57:54 -0400 Subject: [PATCH 11/43] Add InferenceOutputImageFileNameFormat Setting --- .../ViewModels/SettingsViewModel.cs | 10 +++++++ .../Views/SettingsPage.axaml | 26 +++++++++++++++++-- .../Models/Settings/Settings.cs | 5 ++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index eee148fa..97772909 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -107,6 +107,9 @@ public partial class SettingsViewModel : PageViewModelBase [ObservableProperty] private bool isCompletionRemoveUnderscoresEnabled = true; + [ObservableProperty] + private string? outputImageFileNameFormat; + [ObservableProperty] private bool isImageViewerPixelGridEnabled = true; @@ -201,6 +204,13 @@ public partial class SettingsViewModel : PageViewModelBase true ); + settingsManager.RelayPropertyFor( + this, + vm => vm.OutputImageFileNameFormat, + settings => settings.InferenceOutputImageFileNameFormat, + true + ); + settingsManager.RelayPropertyFor( this, vm => vm.IsImageViewerPixelGridEnabled, diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml index 21ab36ac..d51bce28 100644 --- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml @@ -6,10 +6,12 @@ xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" + xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit" d:DataContext="{x:Static mocks:DesignData.SettingsViewModel}" d:DesignHeight="700" d:DesignWidth="800" @@ -83,10 +85,10 @@ - + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Core/Models/Settings/Settings.cs b/StabilityMatrix.Core/Models/Settings/Settings.cs index cb01f14e..73d02cb6 100644 --- a/StabilityMatrix.Core/Models/Settings/Settings.cs +++ b/StabilityMatrix.Core/Models/Settings/Settings.cs @@ -70,6 +70,11 @@ public class Settings /// public bool IsCompletionRemoveUnderscoresEnabled { get; set; } = true; + /// + /// Format for Inference output image file names + /// + public string? InferenceOutputImageFileNameFormat { get; set; } + /// /// Whether the Inference Image Viewer shows pixel grids at high zoom levels /// From 9cacf9283bd32d87c76e6a7d2fd9840fc42bdbb3 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 11 Oct 2023 23:58:12 -0400 Subject: [PATCH 12/43] Add Stream overload for Png AddMetadata --- StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs b/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs index 086d0785..aa31add4 100644 --- a/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs +++ b/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs @@ -16,6 +16,17 @@ public static class PngDataHelper private static readonly byte[] Text = { 0x74, 0x45, 0x58, 0x74 }; private static readonly byte[] Iend = { 0x49, 0x45, 0x4E, 0x44 }; + public static byte[] AddMetadata( + Stream inputStream, + GenerationParameters generationParameters, + InferenceProjectDocument projectDocument + ) + { + using var ms = new MemoryStream(); + inputStream.CopyTo(ms); + return AddMetadata(ms.ToArray(), generationParameters, projectDocument); + } + public static byte[] AddMetadata( byte[] inputImage, GenerationParameters generationParameters, From 2f033893140d1a05c3d087c3498379417470b751 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 12 Oct 2023 00:13:41 -0400 Subject: [PATCH 13/43] Refresh Info before checking access time --- StabilityMatrix.Core/Models/Database/LocalImageFile.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs index 184b9c63..bf728a1b 100644 --- a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs +++ b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs @@ -126,6 +126,8 @@ public class LocalImageFile GenerationParameters.TryParse(metadata, out genParams); } + filePath.Info.Refresh(); + return new LocalImageFile { RelativePath = relativePath, From a2c3acb95256651a41f29d242f35d6ef2b16e07b Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 12 Oct 2023 01:02:29 -0400 Subject: [PATCH 14/43] Change Inference to use downloaded images and custom file name formatting --- .../Extensions/ComfyNodeBuilderExtensions.cs | 14 +- .../Helpers/ImageProcessor.cs | 47 +++-- .../Services/InferenceClientManager.cs | 62 +++++- .../Base/InferenceGenerationViewModelBase.cs | 194 +++++++++++++----- .../InferenceImageUpscaleViewModel.cs | 4 +- .../InferenceTextToImageViewModel.cs | 3 +- 6 files changed, 239 insertions(+), 85 deletions(-) diff --git a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs index a6303f21..e04f4f46 100644 --- a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs +++ b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs @@ -282,20 +282,16 @@ public static class ComfyNodeBuilderExtensions builder.Connections.ImageSize = builder.Connections.LatentSize; } - var saveImage = builder.Nodes.AddNamedNode( + var previewImage = builder.Nodes.AddNamedNode( new NamedComfyNode("SaveImage") { - ClassType = "SaveImage", - Inputs = new Dictionary - { - ["filename_prefix"] = "Inference/TextToImage", - ["images"] = builder.Connections.Image - } + ClassType = "PreviewImage", + Inputs = new Dictionary { ["images"] = builder.Connections.Image } } ); - builder.Connections.OutputNodes.Add(saveImage); + builder.Connections.OutputNodes.Add(previewImage); - return saveImage.Name; + return previewImage.Name; } } diff --git a/StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs b/StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs index 28c215d6..a090c6a2 100644 --- a/StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs +++ b/StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs @@ -13,50 +13,57 @@ public static class ImageProcessor /// public static (int rows, int columns) GetGridDimensionsFromImageCount(int count) { - if (count <= 1) return (1, 1); - if (count == 2) return (1, 2); - + if (count <= 1) + return (1, 1); + if (count == 2) + return (1, 2); + // Prefer one extra row over one extra column, // the row count will be the floor of the square root // and the column count will be floor of count / rows - var rows = (int) Math.Floor(Math.Sqrt(count)); - var columns = (int) Math.Floor((double) count / rows); + var rows = (int)Math.Floor(Math.Sqrt(count)); + var columns = (int)Math.Floor((double)count / rows); return (rows, columns); } - - public static SKImage CreateImageGrid( - IReadOnlyList images, - int spacing = 0) + + public static SKImage CreateImageGrid(IReadOnlyList images, int spacing = 0) { + if (images.Count == 0) + throw new ArgumentException("Must have at least one image"); + var (rows, columns) = GetGridDimensionsFromImageCount(images.Count); var singleWidth = images[0].Width; var singleHeight = images[0].Height; - + // Make output image using var output = new SKBitmap( - singleWidth * columns + spacing * (columns - 1), - singleHeight * rows + spacing * (rows - 1)); - + singleWidth * columns + spacing * (columns - 1), + singleHeight * rows + spacing * (rows - 1) + ); + // Draw images using var canvas = new SKCanvas(output); - - foreach (var (row, column) in - Enumerable.Range(0, rows).Product(Enumerable.Range(0, columns))) + + foreach ( + var (row, column) in Enumerable.Range(0, rows).Product(Enumerable.Range(0, columns)) + ) { // Stop if we have drawn all images var index = row * columns + column; - if (index >= images.Count) break; - + if (index >= images.Count) + break; + // Get image var image = images[index]; - + // Draw image var destination = new SKRect( singleWidth * column + spacing * column, singleHeight * row + spacing * row, singleWidth * column + spacing * column + image.Width, - singleHeight * row + spacing * row + image.Height); + singleHeight * row + spacing * row + image.Height + ); canvas.DrawImage(image, destination); } diff --git a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs index 3e71a877..aec47d07 100644 --- a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -345,6 +346,61 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient return ConnectAsyncImpl(new Uri("http://127.0.0.1:8188"), cancellationToken); } + private async Task MigrateLinksIfNeeded(PackagePair packagePair) + { + if (packagePair.InstalledPackage.FullPath is not { } packagePath) + { + throw new ArgumentException("Package path is null", nameof(packagePair)); + } + + var newDestination = settingsManager.ImagesInferenceDirectory; + + // If new destination is a reparse point (like before, delete it first) + if (newDestination is { IsSymbolicLink: true, Info.LinkTarget: null }) + { + logger.LogInformation("Deleting existing link target at {NewDir}", newDestination); + newDestination.Info.Attributes = FileAttributes.Normal; + await newDestination.DeleteAsync(false).ConfigureAwait(false); + } + + newDestination.Create(); + + // For locally installed packages only + // Move all files in ./output/Inference to /Images/Inference and delete ./output/Inference + + var legacyLinkSource = new DirectoryPath(packagePair.InstalledPackage.FullPath).JoinDir( + "output", + "Inference" + ); + if (!legacyLinkSource.Exists) + { + return; + } + + // Move files if not empty + if (legacyLinkSource.Info.EnumerateFiles().Any()) + { + logger.LogInformation( + "Moving files from {LegacyDir} to {NewDir}", + legacyLinkSource, + newDestination + ); + await FileTransfers + .MoveAllFilesAndDirectories( + legacyLinkSource, + newDestination, + overwriteIfHashMatches: true, + overwrite: false + ) + .ConfigureAwait(false); + } + + // Delete legacy link + logger.LogInformation("Deleting legacy link at {LegacyDir}", legacyLinkSource); + legacyLinkSource.Info.Attributes = FileAttributes.Normal; + await legacyLinkSource.DeleteAsync(false).ConfigureAwait(false); + } + /// public async Task ConnectAsync( PackagePair packagePair, @@ -367,11 +423,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient logger.LogError(ex, "Error setting up completion provider"); }); - // Setup image folder links - await comfyPackage.SetupInferenceOutputFolderLinks( - packagePair.InstalledPackage.FullPath - ?? throw new InvalidOperationException("Package does not have a Path") - ); + await MigrateLinksIfNeeded(packagePair); // Get user defined host and port var host = packagePair.InstalledPackage.GetLaunchArgsHost(); diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs index 3bd7e614..c7fe35e0 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -3,11 +3,14 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; +using Avalonia.Controls.Notifications; +using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.Input; using NLog; @@ -27,6 +30,8 @@ using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Base; @@ -41,6 +46,7 @@ public abstract partial class InferenceGenerationViewModelBase { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private readonly ISettingsManager settingsManager; private readonly INotificationService notificationService; private readonly ServiceManager vmFactory; @@ -60,11 +66,13 @@ public abstract partial class InferenceGenerationViewModelBase protected InferenceGenerationViewModelBase( ServiceManager vmFactory, IInferenceClientManager inferenceClientManager, - INotificationService notificationService + INotificationService notificationService, + ISettingsManager settingsManager ) : base(notificationService) { this.notificationService = notificationService; + this.settingsManager = settingsManager; this.vmFactory = vmFactory; ClientManager = inferenceClientManager; @@ -75,6 +83,100 @@ public abstract partial class InferenceGenerationViewModelBase GenerateImageCommand.WithConditionalNotificationErrorHandler(notificationService); } + /// + /// Write an image to the default output folder + /// + protected Task WriteOutputImageAsync( + Stream imageStream, + ImageGenerationEventArgs args, + int batchNum = 0, + int batchTotal = 0, + bool isGrid = false + ) + { + var defaultOutputDir = settingsManager.ImagesInferenceDirectory; + defaultOutputDir.Create(); + + return WriteOutputImageAsync( + imageStream, + defaultOutputDir, + args, + batchNum, + batchTotal, + isGrid + ); + } + + /// + /// Write an image to an output folder + /// + protected async Task WriteOutputImageAsync( + Stream imageStream, + DirectoryPath outputDir, + ImageGenerationEventArgs args, + int batchNum = 0, + int batchTotal = 0, + bool isGrid = false + ) + { + var formatTemplateStr = settingsManager.Settings.InferenceOutputImageFileNameFormat; + + var formatProvider = new FileNameFormatProvider + { + GenerationParameters = args.Parameters, + ProjectType = args.Project?.ProjectType, + ProjectName = ProjectFile?.NameWithoutExtension + }; + + // Parse to format + if ( + string.IsNullOrEmpty(formatTemplateStr) + || !FileNameFormat.TryParse(formatTemplateStr, formatProvider, out var format) + ) + { + // Fallback to default + Logger.Warn( + "Failed to parse format template: {FormatTemplate}, using default", + formatTemplateStr + ); + + format = FileNameFormat.Parse(FileNameFormat.DefaultTemplate, formatProvider); + } + + if (isGrid) + { + format = format.WithGridPrefix(); + } + + if (batchNum >= 1 && batchTotal > 1) + { + format = format.WithBatchPostFix(batchNum, batchTotal); + } + + var fileName = format.GetFileName() + ".png"; + var file = outputDir.JoinFile(fileName); + + // Until the file is free, keep adding _{i} to the end + for (var i = 0; i < 100; i++) + { + if (!file.Exists) + break; + + file = outputDir.JoinFile($"{fileName}_{i + 1}"); + } + + // If that fails, append an 7-char uuid + if (file.Exists) + { + file = outputDir.JoinFile($"{fileName}_{Guid.NewGuid():N}"[..7]); + } + + await using var fileStream = file.Info.OpenWrite(); + await imageStream.CopyToAsync(fileStream); + + return file; + } + /// /// Builds the image generation prompt /// @@ -156,7 +258,7 @@ public abstract partial class InferenceGenerationViewModelBase // Wait for prompt to finish await promptTask.Task.WaitAsync(cancellationToken); - Logger.Trace($"Prompt task {promptTask.Id} finished"); + Logger.Debug($"Prompt task {promptTask.Id} finished"); // Get output images var imageOutputs = await client.GetImagesForExecutedPromptAsync( @@ -164,6 +266,20 @@ public abstract partial class InferenceGenerationViewModelBase cancellationToken ); + if ( + !imageOutputs.TryGetValue(args.OutputNodeNames[0], out var images) + || images is not { Count: > 0 } + ) + { + // No images match + notificationService.Show( + "No output", + "Did not receive any output images", + NotificationType.Warning + ); + return; + } + // Disable cancellation await promptInterrupt.DisposeAsync(); @@ -172,15 +288,6 @@ public abstract partial class InferenceGenerationViewModelBase ImageGalleryCardViewModel.ImageSources.Clear(); } - if ( - !imageOutputs.TryGetValue(args.OutputNodeNames[0], out var images) || images is null - ) - { - // No images match - notificationService.Show("No output", "Did not receive any output images"); - return; - } - await ProcessOutputImages(images, args); } finally @@ -207,19 +314,22 @@ public abstract partial class InferenceGenerationViewModelBase ImageGenerationEventArgs args ) { + var client = args.Client; + // Write metadata to images + var outputImagesBytes = new List(); var outputImages = new List(); - foreach ( - var (i, filePath) in images - .Select(image => image.ToFilePath(args.Client.OutputImagesDir!)) - .Enumerate() - ) + + foreach (var (i, comfyImage) in images.Enumerate()) { - if (!filePath.Exists) - { - Logger.Warn($"Image file {filePath} does not exist"); - continue; - } + Logger.Debug("Downloading image: {FileName}", comfyImage.FileName); + var imageStream = await client.GetImageStreamAsync(comfyImage); + + using var ms = new MemoryStream(); + await imageStream.CopyToAsync(ms); + + var imageArray = ms.ToArray(); + outputImagesBytes.Add(imageArray); var parameters = args.Parameters!; var project = args.Project!; @@ -248,17 +358,15 @@ public abstract partial class InferenceGenerationViewModelBase ); } - var bytesWithMetadata = PngDataHelper.AddMetadata( - await filePath.ReadAllBytesAsync(), - parameters, - project - ); + var bytesWithMetadata = PngDataHelper.AddMetadata(imageArray, parameters, project); - await using (var outputStream = filePath.Info.OpenWrite()) - { - await outputStream.WriteAsync(bytesWithMetadata); - await outputStream.FlushAsync(); - } + // Write using generated name + var filePath = await WriteOutputImageAsync( + new MemoryStream(bytesWithMetadata), + args, + i + 1, + images.Count + ); outputImages.Add(new ImageSource(filePath)); @@ -268,17 +376,7 @@ public abstract partial class InferenceGenerationViewModelBase // Download all images to make grid, if multiple if (outputImages.Count > 1) { - var outputDir = outputImages[0].LocalFile!.Directory; - - var loadedImages = outputImages - .Select(i => i.LocalFile) - .Where(f => f is { Exists: true }) - .Select(f => - { - using var stream = f!.Info.OpenRead(); - return SKImage.FromEncodedData(stream); - }) - .ToImmutableArray(); + var loadedImages = outputImagesBytes.Select(SKImage.FromEncodedData).ToImmutableArray(); var project = args.Project!; @@ -297,13 +395,11 @@ public abstract partial class InferenceGenerationViewModelBase ); // Save to disk - var lastName = outputImages.Last().LocalFile?.Info.Name; - var gridPath = outputDir!.JoinFile($"grid-{lastName}"); - - await using (var fileStream = gridPath.Info.OpenWrite()) - { - await fileStream.WriteAsync(gridBytesWithMetadata); - } + var gridPath = await WriteOutputImageAsync( + new MemoryStream(gridBytesWithMetadata), + args, + isGrid: true + ); // Insert to start of images var gridImage = new ImageSource(gridPath); diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs index 9e56a16d..d97cf780 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs @@ -19,6 +19,7 @@ using StabilityMatrix.Avalonia.Views.Inference; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; +using StabilityMatrix.Core.Services; using Path = System.IO.Path; #pragma warning disable CS0657 // Not a valid attribute location for this declaration @@ -60,9 +61,10 @@ public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase public InferenceImageUpscaleViewModel( INotificationService notificationService, IInferenceClientManager inferenceClientManager, + ISettingsManager settingsManager, ServiceManager vmFactory ) - : base(vmFactory, inferenceClientManager, notificationService) + : base(vmFactory, inferenceClientManager, notificationService, settingsManager) { this.notificationService = notificationService; diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 07124ac0..929aa771 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -86,10 +86,11 @@ public class InferenceTextToImageViewModel public InferenceTextToImageViewModel( INotificationService notificationService, IInferenceClientManager inferenceClientManager, + ISettingsManager settingsManager, ServiceManager vmFactory, IModelIndexService modelIndexService ) - : base(vmFactory, inferenceClientManager, notificationService) + : base(vmFactory, inferenceClientManager, notificationService, settingsManager) { this.notificationService = notificationService; this.modelIndexService = modelIndexService; From d3d10be753c986f86ef74b03c29ef071e7cdc1b9 Mon Sep 17 00:00:00 2001 From: JT Date: Wed, 11 Oct 2023 23:44:31 -0700 Subject: [PATCH 15/43] Added selectable image button control for Outputs page & beyond --- StabilityMatrix.Avalonia/App.axaml | 1 + .../SelectableImageButton.axaml | 51 ++++++++++++ .../SelectableImageButton.cs | 25 ++++++ .../DesignData/DesignData.cs | 26 ++++++- .../Languages/Resources.Designer.cs | 18 +++++ .../Languages/Resources.resx | 8 +- .../OutputsPage/OutputImageViewModel.cs | 19 +++++ .../ViewModels/OutputsPageViewModel.cs | 74 +++++++++++++----- .../Views/OutputsPage.axaml | 78 ++++++++++++------- 9 files changed, 249 insertions(+), 51 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml create mode 100644 StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs create mode 100644 StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml index 0258b36b..fb90f374 100644 --- a/StabilityMatrix.Avalonia/App.axaml +++ b/StabilityMatrix.Avalonia/App.axaml @@ -60,6 +60,7 @@ + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs b/StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs new file mode 100644 index 00000000..3a1ca335 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs @@ -0,0 +1,25 @@ +using Avalonia; +using Avalonia.Controls; + +namespace StabilityMatrix.Avalonia.Controls.SelectableImageCard; + +public class SelectableImageButton : Button +{ + public static readonly StyledProperty IsSelectedProperty = + CheckBox.IsCheckedProperty.AddOwner(); + + public static readonly StyledProperty SourceProperty = + BetterAdvancedImage.SourceProperty.AddOwner(); + + public bool? IsSelected + { + get => GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); + } + + public string? Source + { + get => GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } +} diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 4b4d4599..068466b3 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -7,6 +7,7 @@ using System.IO; using System.Net.Http; using System.Text; using AvaloniaEdit.Utils; +using DynamicData.Binding; using Microsoft.Extensions.DependencyInjection; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls.CodeCompletion; @@ -20,6 +21,7 @@ using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Progress; using StabilityMatrix.Avalonia.ViewModels.Inference; +using StabilityMatrix.Avalonia.ViewModels.OutputsPage; using StabilityMatrix.Avalonia.ViewModels.Settings; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Database; @@ -29,6 +31,7 @@ using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api.Comfy; +using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Python; @@ -336,8 +339,27 @@ public static class DesignData public static LaunchPageViewModel LaunchPageViewModel => Services.GetRequiredService(); - public static OutputsPageViewModel OutputsPageViewModel => - Services.GetRequiredService(); + public static OutputsPageViewModel OutputsPageViewModel + { + get + { + var vm = Services.GetRequiredService(); + vm.Outputs = new ObservableCollectionExtended + { + new( + new LocalImageFile + { + AbsolutePath = + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg", + RelativePath = + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg", + ImageType = LocalImageFileType.TextToImage + } + ) + }; + return vm; + } + } public static PackageManagerViewModel PackageManagerViewModel { diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index ab1c7e63..336977f1 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -257,6 +257,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Open in Image Viewer. + /// + public static string Action_OpenInViewer { + get { + return ResourceManager.GetString("Action_OpenInViewer", resourceCulture); + } + } + /// /// Looks up a localized string similar to Open on CivitAI. /// @@ -1157,6 +1166,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to {0} images selected. + /// + public static string Label_NumImagesSelected { + get { + return ResourceManager.GetString("Label_NumImagesSelected", resourceCulture); + } + } + /// /// Looks up a localized string similar to Only available on Windows. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index a0fd01c3..05d6781e 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -687,4 +687,10 @@ Copy - \ No newline at end of file + + Open in Image Viewer + + + {0} images selected + + diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs new file mode 100644 index 00000000..1102d0ab --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs @@ -0,0 +1,19 @@ +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Core.Models.Database; + +namespace StabilityMatrix.Avalonia.ViewModels.OutputsPage; + +public partial class OutputImageViewModel : ViewModelBase +{ + public LocalImageFile ImageFile { get; } + + [ObservableProperty] + private bool isSelected; + + public OutputImageViewModel(LocalImageFile imageFile) + { + ImageFile = imageFile; + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs index 209fd099..4e07140d 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs @@ -4,21 +4,24 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Reactive.Linq; +using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using AsyncImageLoader; +using Avalonia; using Avalonia.Controls; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; +using Microsoft.Extensions.Logging; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; -using StabilityMatrix.Avalonia.Views; +using StabilityMatrix.Avalonia.ViewModels.OutputsPage; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Factory; @@ -32,21 +35,23 @@ using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; namespace StabilityMatrix.Avalonia.ViewModels; -[View(typeof(OutputsPage))] +[View(typeof(Views.OutputsPage))] public partial class OutputsPageViewModel : PageViewModelBase { private readonly ISettingsManager settingsManager; private readonly INotificationService notificationService; private readonly INavigationService navigationService; + private readonly ILogger logger; public override string Title => "Outputs"; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true }; - public SourceCache OutputsCache { get; } = new(p => p.AbsolutePath); + public SourceCache OutputsCache { get; } = + new(p => p.ImageFile.AbsolutePath); - public IObservableCollection Outputs { get; } = - new ObservableCollectionExtended(); + public IObservableCollection Outputs { get; set; } = + new ObservableCollectionExtended(); public IEnumerable OutputTypes { get; } = Enum.GetValues(); @@ -60,25 +65,35 @@ public partial class OutputsPageViewModel : PageViewModelBase [ObservableProperty] private SharedOutputType selectedOutputType; + [ObservableProperty] + private int numItemsSelected; + public bool CanShowOutputTypes => SelectedCategory.Name.Equals("Shared Output Folder"); public OutputsPageViewModel( ISettingsManager settingsManager, IPackageFactory packageFactory, INotificationService notificationService, - INavigationService navigationService + INavigationService navigationService, + ILogger logger ) { this.settingsManager = settingsManager; this.notificationService = notificationService; this.navigationService = navigationService; + this.logger = logger; OutputsCache .Connect() .DeferUntilLoaded() - .SortBy(x => x.CreatedAt, SortDirection.Descending) + .SortBy(x => x.ImageFile.CreatedAt, SortDirection.Descending) + .ObserveOn(SynchronizationContext.Current) .Bind(Outputs) - .Subscribe(); + .WhenPropertyChanged(p => p.IsSelected) + .Subscribe(_ => + { + NumItemsSelected = Outputs.Count(o => o.IsSelected); + }); if (!settingsManager.IsLibraryDirSet || Design.IsDesignMode) return; @@ -152,16 +167,29 @@ public partial class OutputsPageViewModel : PageViewModelBase GetOutputs(path); } - public async Task OnImageClick(LocalImageFile item) + public async Task OnImageClick(OutputImageViewModel item) + { + // Select image if we're in "select mode" + if (NumItemsSelected > 0) + { + item.IsSelected = !item.IsSelected; + } + else + { + await ShowImageDialog(item); + } + } + + public async Task ShowImageDialog(OutputImageViewModel item) { var currentIndex = Outputs.IndexOf(item); - var image = new ImageSource(new FilePath(item.AbsolutePath)); + var image = new ImageSource(new FilePath(item.ImageFile.AbsolutePath)); // Preload await image.GetBitmapAsync(); - var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item }; + var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item.ImageFile }; using var onNext = Observable .FromEventPattern( @@ -180,14 +208,14 @@ public partial class OutputsPageViewModel : PageViewModelBase { var newImage = Outputs[newIndex]; var newImageSource = new ImageSource( - new FilePath(newImage.AbsolutePath) + new FilePath(newImage.ImageFile.AbsolutePath) ); // Preload await newImageSource.GetBitmapAsync(); sender.ImageSource = newImageSource; - sender.LocalImageFile = newImage; + sender.LocalImageFile = newImage.ImageFile; currentIndex = newIndex; } @@ -207,9 +235,9 @@ public partial class OutputsPageViewModel : PageViewModelBase public async Task OpenImage(string imagePath) => await ProcessRunner.OpenFileBrowser(imagePath); - public async Task DeleteImage(LocalImageFile? item) + public async Task DeleteImage(OutputImageViewModel? item) { - if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) + if (item?.ImageFile.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) { return; } @@ -223,7 +251,7 @@ public partial class OutputsPageViewModel : PageViewModelBase return; } - Outputs.Remove(item); + OutputsCache.Remove(item); // Invalidate cache if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) @@ -244,6 +272,14 @@ public partial class OutputsPageViewModel : PageViewModelBase EventManager.Instance.OnInferenceUpscaleRequested(image); } + public void ClearSelection() + { + foreach (var output in Outputs) + { + output.IsSelected = false; + } + } + private void GetOutputs(string directory) { if (!settingsManager.IsLibraryDirSet) @@ -257,9 +293,9 @@ public partial class OutputsPageViewModel : PageViewModelBase var list = Directory .EnumerateFiles(directory, "*.png", SearchOption.AllDirectories) - .Select(file => LocalImageFile.FromPath(file)) - .OrderByDescending(f => f.CreatedAt); + .Select(file => new OutputImageViewModel(LocalImageFile.FromPath(file))) + .OrderByDescending(f => f.ImageFile.CreatedAt); - OutputsCache.EditDiff(list, (x, y) => x.AbsolutePath == y.AbsolutePath); + OutputsCache.EditDiff(list, (x, y) => x.ImageFile.AbsolutePath == y.ImageFile.AbsolutePath); } } diff --git a/StabilityMatrix.Avalonia/Views/OutputsPage.axaml b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml index 1c200b5d..32a86148 100644 --- a/StabilityMatrix.Avalonia/Views/OutputsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml @@ -13,7 +13,8 @@ xmlns:models1="clr-namespace:StabilityMatrix.Avalonia.Models" xmlns:database="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core" xmlns:fluentAvalonia="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia" - xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" + xmlns:outputsPage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.OutputsPage" + xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard" d:DataContext="{x:Static mocks:DesignData.OutputsPageViewModel}" d:DesignHeight="450" d:DesignWidth="700" @@ -24,8 +25,9 @@ - + @@ -48,13 +50,31 @@ - + + VerticalContentAlignment="Center" /> + + + + + + + + + From 62c083bba3b33393ad9c93f93d5c98ddf6f893ae Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 12 Oct 2023 15:23:08 -0400 Subject: [PATCH 16/43] Cleanup unused --- .../Inference/FileNameFormatProvider.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs index e6ecc563..7b4f3508 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs @@ -31,8 +31,6 @@ public partial class FileNameFormatProvider { "time", () => DateTime.Now.ToString("HH-mm-ss") } }; - public (int Current, int Total)? BatchInfo { get; init; } - /// /// Validate a format string /// @@ -89,23 +87,8 @@ public partial class FileNameFormatProvider } /// - /// Return a string substituting the variables in the format string + /// Regex for matching contents within a curly brace. /// - private string? GetSubstitution(string variable) - { - return variable switch - { - "seed" => GenerationParameters.Seed.ToString(), - "model_name" => GenerationParameters.ModelName, - "model_hash" => GenerationParameters.ModelHash, - "width" => GenerationParameters.Width.ToString(), - "height" => GenerationParameters.Height.ToString(), - "date" => DateTime.Now.ToString("yyyy-MM-dd"), - "time" => DateTime.Now.ToString("HH-mm-ss"), - _ => throw new ArgumentOutOfRangeException(nameof(variable), variable, null) - }; - } - [GeneratedRegex(@"\{[a-z_]+\}")] private static partial Regex BracketRegex(); } From c422ae7b4bed588397d0e961707c96de989c4255 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 12 Oct 2023 15:23:32 -0400 Subject: [PATCH 17/43] Fix inference link migration --- .../Services/InferenceClientManager.cs | 59 +++++++------------ 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs index aec47d07..17023dfa 100644 --- a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs @@ -353,52 +353,35 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient throw new ArgumentException("Package path is null", nameof(packagePair)); } - var newDestination = settingsManager.ImagesInferenceDirectory; - - // If new destination is a reparse point (like before, delete it first) - if (newDestination is { IsSymbolicLink: true, Info.LinkTarget: null }) - { - logger.LogInformation("Deleting existing link target at {NewDir}", newDestination); - newDestination.Info.Attributes = FileAttributes.Normal; - await newDestination.DeleteAsync(false).ConfigureAwait(false); - } - - newDestination.Create(); + var inferenceDir = settingsManager.ImagesInferenceDirectory; + inferenceDir.Create(); // For locally installed packages only - // Move all files in ./output/Inference to /Images/Inference and delete ./output/Inference + // Delete ./output/Inference - var legacyLinkSource = new DirectoryPath(packagePair.InstalledPackage.FullPath).JoinDir( - "output", - "Inference" - ); - if (!legacyLinkSource.Exists) - { - return; - } + var legacyInferenceLinkDir = new DirectoryPath( + packagePair.InstalledPackage.FullPath + ).JoinDir("output", "Inference"); - // Move files if not empty - if (legacyLinkSource.Info.EnumerateFiles().Any()) + if (legacyInferenceLinkDir.Exists) { logger.LogInformation( - "Moving files from {LegacyDir} to {NewDir}", - legacyLinkSource, - newDestination + "Deleting legacy inference link at {LegacyDir}", + legacyInferenceLinkDir ); - await FileTransfers - .MoveAllFilesAndDirectories( - legacyLinkSource, - newDestination, - overwriteIfHashMatches: true, - overwrite: false - ) - .ConfigureAwait(false); - } - // Delete legacy link - logger.LogInformation("Deleting legacy link at {LegacyDir}", legacyLinkSource); - legacyLinkSource.Info.Attributes = FileAttributes.Normal; - await legacyLinkSource.DeleteAsync(false).ConfigureAwait(false); + if (legacyInferenceLinkDir.IsSymbolicLink) + { + await legacyInferenceLinkDir.DeleteAsync(false); + } + else + { + logger.LogWarning( + "Legacy inference link at {LegacyDir} is not a symbolic link, skipping", + legacyInferenceLinkDir + ); + } + } } /// From a13fcbc90397603b66d645e5f7ac209d42cb1dde Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 12 Oct 2023 18:13:36 -0400 Subject: [PATCH 18/43] Add sample parameters and validation --- .../Models/Inference/FileNameFormat.cs | 9 +++- .../Models/Inference/FileNameFormatPart.cs | 13 ++++- .../Inference/FileNameFormatProvider.cs | 48 +++++++++++++---- .../Models/Inference/FileNameFormatVar.cs | 8 +++ .../StabilityMatrix.Avalonia.csproj | 1 + .../Base/InferenceGenerationViewModelBase.cs | 1 - .../ViewModels/SettingsViewModel.cs | 54 +++++++++++++++++++ .../Views/SettingsPage.axaml | 26 +++++++-- .../Models/GenerationParameters.cs | 20 +++++++ 9 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Models/Inference/FileNameFormatVar.cs diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs index 28e9a35a..b20f6c48 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -35,13 +36,17 @@ public record FileNameFormat public string GetFileName() { return Prefix - + string.Join("", Parts.Select(p => p.Constant ?? p.Substitution?.Invoke() ?? "")) + + string.Join( + "", + Parts.Select( + part => part.Match(constant => constant, substitution => substitution.Invoke()) + ) + ) + Postfix; } public static FileNameFormat Parse(string template, FileNameFormatProvider provider) { - provider.Validate(template); var parts = provider.GetParts(template).ToImmutableArray(); return new FileNameFormat(template, parts); } diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs index bfbcc8d9..9210adc0 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs @@ -1,5 +1,16 @@ using System; +using System.Runtime.InteropServices; +using CSharpDiscriminatedUnion.Attributes; namespace StabilityMatrix.Avalonia.Models.Inference; -public record FileNameFormatPart(string? Constant, Func? Substitution); +[GenerateDiscriminatedUnion(CaseFactoryPrefix = "From")] +[StructLayout(LayoutKind.Auto)] +public readonly partial struct FileNameFormatPart +{ + [StructCase("Constant", isDefaultValue: true)] + private readonly string constant; + + [StructCase("Substitution")] + private readonly Func substitution; +} diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs index 7b4f3508..96cacf04 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs @@ -1,7 +1,11 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.Contracts; using System.Linq; using System.Text.RegularExpressions; +using Avalonia.Data; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; @@ -17,7 +21,7 @@ public partial class FileNameFormatProvider private Dictionary>? _substitutions; - private Dictionary> Substitutions => + public Dictionary> Substitutions => _substitutions ??= new Dictionary> { { "seed", () => GenerationParameters?.Seed.ToString() }, @@ -34,19 +38,24 @@ public partial class FileNameFormatProvider /// /// Validate a format string /// - public void Validate(string format) + /// Format string + /// Thrown if the format string contains an unknown variable + [Pure] + public ValidationResult Validate(string format) { var regex = BracketRegex(); var matches = regex.Matches(format); - var variables = matches.Select(m => m.Value[1..^1]).ToList(); + var variables = matches.Select(m => m.Groups[1].Value); foreach (var variable in variables) { if (!Substitutions.ContainsKey(variable)) { - throw new ArgumentException($"Unknown variable '{variable}'"); + return new ValidationResult($"Unknown variable '{variable}'"); } } + + return ValidationResult.Success!; } public IEnumerable GetParts(string template) @@ -65,13 +74,15 @@ public partial class FileNameFormatProvider if (result.Index != currentIndex) { var constant = template[currentIndex..result.Index]; - parts.Add(new FileNameFormatPart(constant, null)); + parts.Add(FileNameFormatPart.FromConstant(constant)); currentIndex += constant.Length; } - var variable = result.Value[1..^1]; - parts.Add(new FileNameFormatPart(null, Substitutions[variable])); + // Now we're at start of the current match, add the variable part + var variable = result.Groups[1].Value; + + parts.Add(FileNameFormatPart.FromSubstitution(Substitutions[variable])); currentIndex += result.Length; } @@ -80,15 +91,34 @@ public partial class FileNameFormatProvider if (currentIndex != template.Length) { var constant = template[currentIndex..]; - parts.Add(new FileNameFormatPart(constant, null)); + parts.Add(FileNameFormatPart.FromConstant(constant)); } return parts; } + /// + /// Return a sample provider for UI preview + /// + public static FileNameFormatProvider GetSample() + { + return new FileNameFormatProvider + { + GenerationParameters = GenerationParameters.GetSample(), + ProjectType = InferenceProjectType.TextToImage, + ProjectName = "Sample Project" + }; + } + /// /// Regex for matching contents within a curly brace. /// - [GeneratedRegex(@"\{[a-z_]+\}")] + [GeneratedRegex(@"\{([a-z_]+)\}")] private static partial Regex BracketRegex(); + + /// + /// Regex for matching a Python-like array index. + /// + [GeneratedRegex(@"\[(?:(?-?\d+)?)\:(?:(?-?\d+)?)?(?:\:(?-?\d+))?\]")] + private static partial Regex IndexRegex(); } diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatVar.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatVar.cs new file mode 100644 index 00000000..a453b3bc --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatVar.cs @@ -0,0 +1,8 @@ +namespace StabilityMatrix.Avalonia.Models.Inference; + +public record FileNameFormatVar +{ + public required string Variable { get; init; } + + public string? Example { get; init; } +} diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index ea70b345..1c95a536 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -32,6 +32,7 @@ + diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs index c7fe35e0..d3b746f6 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -10,7 +10,6 @@ using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls.Notifications; -using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.Input; using NLog; diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index 97772909..850a397c 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -3,10 +3,12 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; +using System.Reactive.Linq; using System.Reflection; using System.Text; using System.Text.Json; @@ -21,6 +23,7 @@ using Avalonia.Styling; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DynamicData.Binding; using FluentAvalonia.UI.Controls; using NLog; using SkiaSharp; @@ -29,6 +32,7 @@ using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; @@ -108,8 +112,24 @@ public partial class SettingsViewModel : PageViewModelBase private bool isCompletionRemoveUnderscoresEnabled = true; [ObservableProperty] + [CustomValidation(typeof(SettingsViewModel), nameof(ValidateOutputImageFileNameFormat))] private string? outputImageFileNameFormat; + [ObservableProperty] + private string? outputImageFileNameFormatSample; + + public IEnumerable OutputImageFileNameFormatVars => + FileNameFormatProvider + .GetSample() + .Substitutions.Select( + kv => + new FileNameFormatVar + { + Variable = $"{{{kv.Key}}}", + Example = kv.Value.Invoke() + } + ); + [ObservableProperty] private bool isImageViewerPixelGridEnabled = true; @@ -204,6 +224,32 @@ public partial class SettingsViewModel : PageViewModelBase true ); + this.WhenPropertyChanged(vm => vm.OutputImageFileNameFormat) + .Throttle(TimeSpan.FromMilliseconds(50)) + .Subscribe(formatProperty => + { + var provider = FileNameFormatProvider.GetSample(); + var template = formatProperty.Value; + + if ( + !string.IsNullOrEmpty(template) + && provider.Validate(template) == ValidationResult.Success + ) + { + var format = FileNameFormat.Parse(template, provider); + OutputImageFileNameFormatSample = format.GetFileName() + ".png"; + } + else + { + // Use default format if empty + var defaultFormat = FileNameFormat.Parse( + FileNameFormat.DefaultTemplate, + provider + ); + OutputImageFileNameFormatSample = defaultFormat.GetFileName() + ".png"; + } + }); + settingsManager.RelayPropertyFor( this, vm => vm.OutputImageFileNameFormat, @@ -235,6 +281,14 @@ public partial class SettingsViewModel : PageViewModelBase UpdateAvailableTagCompletionCsvs(); } + public static ValidationResult ValidateOutputImageFileNameFormat( + string format, + ValidationContext context + ) + { + return FileNameFormatProvider.GetSample().Validate(format); + } + partial void OnSelectedThemeChanged(string? value) { // In case design / tests diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml index d51bce28..18d65bb8 100644 --- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml @@ -12,6 +12,9 @@ xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit" + xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.Models.Inference" + xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight" + Focusable="True" d:DataContext="{x:Static mocks:DesignData.SettingsViewModel}" d:DesignHeight="700" d:DesignWidth="800" @@ -168,15 +171,32 @@ - + FontFamily="Cascadia Code,Consolas,Menlo,Monospace"/> + + + + + diff --git a/StabilityMatrix.Core/Models/GenerationParameters.cs b/StabilityMatrix.Core/Models/GenerationParameters.cs index fab9f793..9ab6c778 100644 --- a/StabilityMatrix.Core/Models/GenerationParameters.cs +++ b/StabilityMatrix.Core/Models/GenerationParameters.cs @@ -126,6 +126,26 @@ public partial record GenerationParameters return (sampler, scheduler); } + /// + /// Return a sample parameters for UI preview + /// + public static GenerationParameters GetSample() + { + return new GenerationParameters + { + PositivePrompt = "(cat:1.2), by artist, detailed, [shaded]", + NegativePrompt = "blurry, jpg artifacts", + Steps = 30, + CfgScale = 7, + Width = 640, + Height = 896, + Seed = 124825529, + ModelName = "ExampleMix7", + ModelHash = "b899d188a1ac7356bfb9399b2277d5b21712aa360f8f9514fba6fcce021baff7", + Sampler = "DPM++ 2M Karras" + }; + } + // Example: Steps: 30, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 2216407431, Size: 640x896, Model hash: eb2h052f91, Model: anime_v1 [GeneratedRegex( """^Steps: (?\d+), Sampler: (?.+?), CFG scale: (?\d+(\.\d+)?), Seed: (?\d+), Size: (?\d+)x(?\d+), Model hash: (?.+?), Model: (?.+)$""" From 7d980c08abeab8e38994312dbe50cadf4dcb0429 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 12 Oct 2023 18:16:54 -0400 Subject: [PATCH 19/43] Fix tests --- .../Avalonia/FileNameFormatProviderTests.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/StabilityMatrix.Tests/Avalonia/FileNameFormatProviderTests.cs b/StabilityMatrix.Tests/Avalonia/FileNameFormatProviderTests.cs index cdf7fdfa..5905aca0 100644 --- a/StabilityMatrix.Tests/Avalonia/FileNameFormatProviderTests.cs +++ b/StabilityMatrix.Tests/Avalonia/FileNameFormatProviderTests.cs @@ -1,4 +1,5 @@ -using StabilityMatrix.Avalonia.Models.Inference; +using System.ComponentModel.DataAnnotations; +using StabilityMatrix.Avalonia.Models.Inference; namespace StabilityMatrix.Tests.Avalonia; @@ -10,7 +11,8 @@ public class FileNameFormatProviderTests { var provider = new FileNameFormatProvider(); - provider.Validate("{date}_{time}-{model_name}-{seed}"); + var result = provider.Validate("{date}_{time}-{model_name}-{seed}"); + Assert.AreEqual(ValidationResult.Success, result); } [TestMethod] @@ -18,8 +20,9 @@ public class FileNameFormatProviderTests { var provider = new FileNameFormatProvider(); - Assert.ThrowsException( - () => provider.Validate("{date}_{time}-{model_name}-{seed}-{invalid}") - ); + var result = provider.Validate("{date}_{time}-{model_name}-{seed}-{invalid}"); + Assert.AreNotEqual(ValidationResult.Success, result); + + Assert.AreEqual("Unknown variable 'invalid'", result.ErrorMessage); } } From 3920ccb1e945aee3eaa1a84b15951305cdbd71e4 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 12 Oct 2023 18:32:02 -0400 Subject: [PATCH 20/43] Version bump --- StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index 1c95a536..a6ed93d0 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -8,7 +8,7 @@ app.manifest true ./Assets/Icon.ico - 2.5.3-dev.1 + 2.6.0-dev.1 $(Version) true true From 4d36d66f9687b56c527fe974cb949e845a2c6284 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 12 Oct 2023 22:16:07 -0400 Subject: [PATCH 21/43] Add variable slice support and prompts --- .../Inference/FileNameFormatProvider.cs | 80 +++++++++++++++++-- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs index 96cacf04..1422efa1 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs @@ -25,6 +25,8 @@ public partial class FileNameFormatProvider _substitutions ??= new Dictionary> { { "seed", () => GenerationParameters?.Seed.ToString() }, + { "prompt", () => GenerationParameters?.PositivePrompt }, + { "negative_prompt", () => GenerationParameters?.NegativePrompt }, { "model_name", () => GenerationParameters?.ModelName }, { "model_hash", () => GenerationParameters?.ModelHash }, { "width", () => GenerationParameters?.Width.ToString() }, @@ -47,11 +49,20 @@ public partial class FileNameFormatProvider var matches = regex.Matches(format); var variables = matches.Select(m => m.Groups[1].Value); - foreach (var variable in variables) + foreach (var variableText in variables) { - if (!Substitutions.ContainsKey(variable)) + try { - return new ValidationResult($"Unknown variable '{variable}'"); + var (variable, _) = ExtractVariableAndSlice(variableText); + + if (!Substitutions.ContainsKey(variable)) + { + return new ValidationResult($"Unknown variable '{variable}'"); + } + } + catch (Exception e) + { + return new ValidationResult($"Invalid variable '{variableText}': {e.Message}"); } } @@ -80,9 +91,38 @@ public partial class FileNameFormatProvider } // Now we're at start of the current match, add the variable part - var variable = result.Groups[1].Value; + var (variable, slice) = ExtractVariableAndSlice(result.Groups[1].Value); + var substitution = Substitutions[variable]; - parts.Add(FileNameFormatPart.FromSubstitution(Substitutions[variable])); + // Slice string if necessary + if (slice is not null) + { + parts.Add( + FileNameFormatPart.FromSubstitution(() => + { + var value = substitution(); + if (value is null) + return null; + + if (slice.End is null) + { + value = value[(slice.Start ?? 0)..]; + } + else + { + var length = + Math.Min(value.Length, slice.End.Value) - (slice.Start ?? 0); + value = value.Substring(slice.Start ?? 0, length); + } + + return value; + }) + ); + } + else + { + parts.Add(FileNameFormatPart.FromSubstitution(substitution)); + } currentIndex += result.Length; } @@ -110,10 +150,36 @@ public partial class FileNameFormatProvider }; } + /// + /// Extract variable and index from a combined string + /// + private static (string Variable, Slice? Slice) ExtractVariableAndSlice(string combined) + { + if (IndexRegex().Matches(combined).FirstOrDefault() is not { Success: true } match) + { + return (combined, null); + } + + // Variable is everything before the match + var variable = combined[..match.Groups[0].Index]; + + var start = match.Groups["start"].Value; + var end = match.Groups["end"].Value; + var step = match.Groups["step"].Value; + + var slice = new Slice( + string.IsNullOrEmpty(start) ? null : int.Parse(start), + string.IsNullOrEmpty(end) ? null : int.Parse(end), + string.IsNullOrEmpty(step) ? null : int.Parse(step) + ); + + return (variable, slice); + } + /// /// Regex for matching contents within a curly brace. /// - [GeneratedRegex(@"\{([a-z_]+)\}")] + [GeneratedRegex(@"\{([a-z_:\d\[\]]+)\}")] private static partial Regex BracketRegex(); /// @@ -121,4 +187,6 @@ public partial class FileNameFormatProvider /// [GeneratedRegex(@"\[(?:(?-?\d+)?)\:(?:(?-?\d+)?)?(?:\:(?-?\d+))?\]")] private static partial Regex IndexRegex(); + + private record Slice(int? Start, int? End, int? Step); } From 37147c220eacb1b95ea1d20dd6f470796e62cbec Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 13 Oct 2023 15:27:17 -0400 Subject: [PATCH 22/43] Fix existing file name id appending --- .../ViewModels/Base/InferenceGenerationViewModelBase.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs index d3b746f6..f1d0d11c 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -152,8 +152,8 @@ public abstract partial class InferenceGenerationViewModelBase format = format.WithBatchPostFix(batchNum, batchTotal); } - var fileName = format.GetFileName() + ".png"; - var file = outputDir.JoinFile(fileName); + var fileName = format.GetFileName(); + var file = outputDir.JoinFile($"{fileName}.png"); // Until the file is free, keep adding _{i} to the end for (var i = 0; i < 100; i++) @@ -161,13 +161,14 @@ public abstract partial class InferenceGenerationViewModelBase if (!file.Exists) break; - file = outputDir.JoinFile($"{fileName}_{i + 1}"); + file = outputDir.JoinFile($"{fileName}_{i + 1}.png"); } // If that fails, append an 7-char uuid if (file.Exists) { - file = outputDir.JoinFile($"{fileName}_{Guid.NewGuid():N}"[..7]); + var uuid = Guid.NewGuid().ToString("N")[..7]; + file = outputDir.JoinFile($"{fileName}_{uuid}.png"); } await using var fileStream = file.Info.OpenWrite(); From 205218b01ab0b6a16aa92944c80069d49b18cfdd Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 13 Oct 2023 15:28:56 -0400 Subject: [PATCH 23/43] Completion popup wrapping for arrow key navigation --- .../Controls/CodeCompletion/CompletionList.cs | 4 ++-- .../CodeCompletion/CompletionListBox.cs | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs index 9150209b..79f2b766 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs @@ -202,11 +202,11 @@ public class CompletionList : TemplatedControl { case Key.Down: e.Handled = true; - _listBox.SelectIndex(_listBox.SelectedIndex + 1); + _listBox.SelectNextIndexWithLoop(); break; case Key.Up: e.Handled = true; - _listBox.SelectIndex(_listBox.SelectedIndex - 1); + _listBox.SelectPreviousIndexWithLoop(); break; case Key.PageDown: e.Handled = true; diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs index 4313be17..90f40a28 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs @@ -91,6 +91,28 @@ public class CompletionListBox : ListBox SelectedIndex = -1; } + /// + /// Selects the next item. If the last item is already selected, selects the first item. + /// + public void SelectNextIndexWithLoop() + { + if (ItemCount <= 0) + return; + + SelectIndex((SelectedIndex + 1) % ItemCount); + } + + /// + /// Selects the previous item. If the first item is already selected, selects the last item. + /// + public void SelectPreviousIndexWithLoop() + { + if (ItemCount <= 0) + return; + + SelectIndex((SelectedIndex - 1 + ItemCount) % ItemCount); + } + /// /// Selects the item with the specified index and scrolls it into view. /// From 54c3c409457b613eab624e458309f48605279ade Mon Sep 17 00:00:00 2001 From: JT Date: Fri, 13 Oct 2023 23:19:30 -0700 Subject: [PATCH 24/43] Added select all / clear selection / delete buttons for output viewer & made checkboxes bigger --- .../Languages/Resources.Designer.cs | 99 +++++++++++++ .../Languages/Resources.resx | 33 +++++ .../ViewModels/OutputsPageViewModel.cs | 77 +++++++++- .../Views/OutputsPage.axaml | 134 +++++++++++------- .../Views/PackageManagerPage.axaml | 33 ++++- 5 files changed, 322 insertions(+), 54 deletions(-) diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index 336977f1..caaba671 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -113,6 +113,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Clear Selection. + /// + public static string Action_ClearSelection { + get { + return ResourceManager.GetString("Action_ClearSelection", resourceCulture); + } + } + /// /// Looks up a localized string similar to Close. /// @@ -401,6 +410,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Select All. + /// + public static string Action_SelectAll { + get { + return ResourceManager.GetString("Action_SelectAll", resourceCulture); + } + } + /// /// Looks up a localized string similar to Select Directory. /// @@ -428,6 +446,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Send to Inference. + /// + public static string Action_SendToInference { + get { + return ResourceManager.GetString("Action_SendToInference", resourceCulture); + } + } + /// /// Looks up a localized string similar to Show in Explorer. /// @@ -905,6 +932,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Image to Image. + /// + public static string Label_ImageToImage { + get { + return ResourceManager.GetString("Label_ImageToImage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Import as Connected. /// @@ -950,6 +986,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Inpainting. + /// + public static string Label_Inpainting { + get { + return ResourceManager.GetString("Label_Inpainting", resourceCulture); + } + } + /// /// Looks up a localized string similar to Input. /// @@ -1175,6 +1220,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to 1 image selected. + /// + public static string Label_OneImageSelected { + get { + return ResourceManager.GetString("Label_OneImageSelected", resourceCulture); + } + } + /// /// Looks up a localized string similar to Only available on Windows. /// @@ -1184,6 +1238,33 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Output Folder. + /// + public static string Label_OutputFolder { + get { + return ResourceManager.GetString("Label_OutputFolder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Output Browser. + /// + public static string Label_OutputsPageTitle { + get { + return ResourceManager.GetString("Label_OutputsPageTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Output Type. + /// + public static string Label_OutputType { + get { + return ResourceManager.GetString("Label_OutputType", resourceCulture); + } + } + /// /// Looks up a localized string similar to Package Environment. /// @@ -1508,6 +1589,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Text to Image. + /// + public static string Label_TextToImage { + get { + return ResourceManager.GetString("Label_TextToImage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Theme. /// @@ -1553,6 +1643,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Upscale. + /// + public static string Label_Upscale { + get { + return ResourceManager.GetString("Label_Upscale", resourceCulture); + } + } + /// /// Looks up a localized string similar to Output Sharing. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index 05d6781e..aa3f91db 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -693,4 +693,37 @@ {0} images selected + + Output Folder + + + Output Type + + + Clear Selection + + + Select All + + + Send to Inference + + + Text to Image + + + Image to Image + + + Inpainting + + + Upscale + + + Output Browser + + + 1 image selected + diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs index 4e07140d..cf98a914 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reactive.Linq; @@ -16,7 +17,9 @@ using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using Microsoft.Extensions.Logging; +using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Extensions; +using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; @@ -42,7 +45,7 @@ public partial class OutputsPageViewModel : PageViewModelBase private readonly INotificationService notificationService; private readonly INavigationService navigationService; private readonly ILogger logger; - public override string Title => "Outputs"; + public override string Title => Resources.Label_OutputsPageTitle; public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true }; @@ -66,10 +69,16 @@ public partial class OutputsPageViewModel : PageViewModelBase private SharedOutputType selectedOutputType; [ObservableProperty] + [NotifyPropertyChangedFor(nameof(NumImagesSelected))] private int numItemsSelected; public bool CanShowOutputTypes => SelectedCategory.Name.Equals("Shared Output Folder"); + public string NumImagesSelected => + NumItemsSelected == 1 + ? Resources.Label_OneImageSelected + : string.Format(Resources.Label_NumImagesSelected, NumItemsSelected); + public OutputsPageViewModel( ISettingsManager settingsManager, IPackageFactory packageFactory, @@ -237,6 +246,19 @@ public partial class OutputsPageViewModel : PageViewModelBase public async Task DeleteImage(OutputImageViewModel? item) { + var confirmationDialog = new BetterContentDialog + { + Title = "Are you sure you want to delete this image?", + Content = "This action cannot be undone.", + PrimaryButtonText = Resources.Action_Delete, + SecondaryButtonText = Resources.Action_Cancel, + DefaultButton = ContentDialogButton.Primary, + IsSecondaryButtonEnabled = true, + }; + var dialogResult = await confirmationDialog.ShowAsync(); + if (dialogResult != ContentDialogResult.Primary) + return; + if (item?.ImageFile.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) { return; @@ -280,6 +302,59 @@ public partial class OutputsPageViewModel : PageViewModelBase } } + public void SelectAll() + { + foreach (var output in Outputs) + { + output.IsSelected = true; + } + } + + public async Task DeleteAllSelected() + { + var confirmationDialog = new BetterContentDialog + { + Title = $"Are you sure you want to delete {NumItemsSelected} images?", + Content = "This action cannot be undone.", + PrimaryButtonText = Resources.Action_Delete, + SecondaryButtonText = Resources.Action_Cancel, + DefaultButton = ContentDialogButton.Primary, + IsSecondaryButtonEnabled = true, + }; + var dialogResult = await confirmationDialog.ShowAsync(); + if (dialogResult != ContentDialogResult.Primary) + return; + + var selected = Outputs.Where(o => o.IsSelected).ToList(); + Debug.Assert(selected.Count == NumItemsSelected); + foreach (var output in selected) + { + if (output?.ImageFile.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) + { + continue; + } + + // Delete the file + var imageFile = new FilePath(imagePath); + var result = await notificationService.TryAsync(imageFile.DeleteAsync()); + + if (!result.IsSuccessful) + { + continue; + } + OutputsCache.Remove(output); + + // Invalidate cache + if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) + { + loader.RemoveAllNamesFromCache(imageFile.Name); + } + } + + NumItemsSelected = 0; + ClearSelection(); + } + private void GetOutputs(string directory) { if (!settingsManager.IsLibraryDirSet) diff --git a/StabilityMatrix.Avalonia/Views/OutputsPage.axaml b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml index 32a86148..8fdcac4f 100644 --- a/StabilityMatrix.Avalonia/Views/OutputsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml @@ -11,10 +11,10 @@ xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:models1="clr-namespace:StabilityMatrix.Avalonia.Models" - xmlns:database="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core" xmlns:fluentAvalonia="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia" xmlns:outputsPage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.OutputsPage" xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard" + xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" d:DataContext="{x:Static mocks:DesignData.OutputsPageViewModel}" d:DesignHeight="450" d:DesignWidth="700" @@ -22,60 +22,95 @@ x:DataType="vm:OutputsPageViewModel" mc:Ignorable="d"> - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - + TextAlignment="Center" + HorizontalAlignment="Right" + Text="{Binding NumImagesSelected, FallbackValue=1234 images selected}" /> - + + private FilePath? GetImagePathIfExists(LocalImageFile item) { - if (item.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) - { - return null; - } - - var imageFile = new FilePath(imagePath); + var imageFile = new FilePath(item.AbsolutePath); if (!imageFile.Exists) { @@ -185,9 +180,7 @@ public partial class ImageFolderCardViewModel : ViewModelBase if (newIndex >= 0 && newIndex < LocalImages.Count) { var newImage = LocalImages[newIndex]; - var newImageSource = new ImageSource( - new FilePath(newImage.GetFullPath(settingsManager.ImagesDirectory)) - ); + var newImageSource = new ImageSource(newImage.AbsolutePath); // Preload await newImageSource.GetBitmapAsync(); diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs index ff175ccd..67d69673 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs @@ -88,7 +88,7 @@ public partial class SelectImageCardViewModel : ViewModelBase, IDropTarget { var current = ImageSource; - ImageSource = new ImageSource(imageFile.GlobalFullPath); + ImageSource = new ImageSource(imageFile.AbsolutePath); current?.Dispose(); }); diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs index cf98a914..9dac32ed 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs @@ -246,6 +246,9 @@ public partial class OutputsPageViewModel : PageViewModelBase public async Task DeleteImage(OutputImageViewModel? item) { + if (item is null) + return; + var confirmationDialog = new BetterContentDialog { Title = "Are you sure you want to delete this image?", @@ -259,13 +262,8 @@ public partial class OutputsPageViewModel : PageViewModelBase if (dialogResult != ContentDialogResult.Primary) return; - if (item?.ImageFile.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) - { - return; - } - // Delete the file - var imageFile = new FilePath(imagePath); + var imageFile = new FilePath(item.ImageFile.AbsolutePath); var result = await notificationService.TryAsync(imageFile.DeleteAsync()); if (!result.IsSuccessful) @@ -329,13 +327,8 @@ public partial class OutputsPageViewModel : PageViewModelBase Debug.Assert(selected.Count == NumItemsSelected); foreach (var output in selected) { - if (output?.ImageFile.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) - { - continue; - } - // Delete the file - var imageFile = new FilePath(imagePath); + var imageFile = new FilePath(output.ImageFile.AbsolutePath); var result = await notificationService.TryAsync(imageFile.DeleteAsync()); if (!result.IsSuccessful) diff --git a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs index bf728a1b..7e468c87 100644 --- a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs +++ b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs @@ -11,56 +11,42 @@ namespace StabilityMatrix.Core.Models.Database; /// public class LocalImageFile { - /// - /// Relative path of the file from the root images directory ("%LIBRARY%/Images"). - /// - [BsonId] - public required string RelativePath { get; set; } - - public required string AbsolutePath { get; set; } + public required string AbsolutePath { get; init; } /// /// Type of the model file. /// - public LocalImageFileType ImageType { get; set; } + public LocalImageFileType ImageType { get; init; } /// /// Creation time of the file. /// - public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset CreatedAt { get; init; } /// /// Last modified time of the file. /// - public DateTimeOffset LastModifiedAt { get; set; } + public DateTimeOffset LastModifiedAt { get; init; } /// /// Generation parameters metadata of the file. /// - public GenerationParameters? GenerationParameters { get; set; } + public GenerationParameters? GenerationParameters { get; init; } /// /// Dimensions of the image /// - public Size? ImageSize { get; set; } + public Size? ImageSize { get; init; } /// /// File name of the relative path. /// - public string FileName => Path.GetFileName(RelativePath); + public string FileName => Path.GetFileName(AbsolutePath); /// /// File name of the relative path without extension. /// - public string FileNameWithoutExtension => Path.GetFileNameWithoutExtension(RelativePath); - - public string GlobalFullPath => - GlobalConfig.LibraryDir.JoinDir("Images").JoinFile(RelativePath); - - public string GetFullPath(string rootImageDirectory) - { - return Path.Combine(rootImageDirectory, RelativePath); - } + public string FileNameWithoutExtension => Path.GetFileNameWithoutExtension(AbsolutePath); public ( string? Parameters, @@ -70,7 +56,7 @@ public class LocalImageFile ) ReadMetadata() { using var stream = new FileStream( - GlobalFullPath, + AbsolutePath, FileMode.Open, FileAccess.Read, FileShare.Read @@ -92,29 +78,19 @@ public class LocalImageFile public static LocalImageFile FromPath(FilePath filePath) { - var relativePath = Path.GetRelativePath( - GlobalConfig.LibraryDir.JoinDir("Images"), - filePath - ); - // TODO: Support other types const LocalImageFileType imageType = LocalImageFileType.Inference | LocalImageFileType.TextToImage; // Get metadata - using var stream = new FileStream( - filePath.FullPath, - FileMode.Open, - FileAccess.Read, - FileShare.Read - ); + using var stream = filePath.Info.OpenRead(); using var reader = new BinaryReader(stream); var imageSize = ImageMetadata.GetImageSize(reader); var metadata = ImageMetadata.ReadTextChunk(reader, "parameters-json"); - GenerationParameters? genParams = null; + GenerationParameters? genParams; if (!string.IsNullOrWhiteSpace(metadata)) { @@ -130,7 +106,6 @@ public class LocalImageFile return new LocalImageFile { - RelativePath = relativePath, AbsolutePath = filePath, ImageType = imageType, CreatedAt = filePath.Info.CreationTimeUtc, @@ -155,21 +130,23 @@ public class LocalImageFile return false; if (x.GetType() != y.GetType()) return false; - return x.RelativePath == y.RelativePath + return x.AbsolutePath == y.AbsolutePath && x.ImageType == y.ImageType && x.CreatedAt.Equals(y.CreatedAt) && x.LastModifiedAt.Equals(y.LastModifiedAt) - && Equals(x.GenerationParameters, y.GenerationParameters); + && Equals(x.GenerationParameters, y.GenerationParameters) + && Nullable.Equals(x.ImageSize, y.ImageSize); } public int GetHashCode(LocalImageFile obj) { return HashCode.Combine( - obj.RelativePath, + obj.AbsolutePath, obj.ImageType, obj.CreatedAt, obj.LastModifiedAt, - obj.GenerationParameters + obj.GenerationParameters, + obj.ImageSize ); } } diff --git a/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs b/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs index c73b2d8f..3e7e3e39 100644 --- a/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs +++ b/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs @@ -11,7 +11,6 @@ public class FilePath : FileSystemPath, IPathObject { private FileInfo? _info; - // ReSharper disable once MemberCanBePrivate.Global [JsonIgnore] public FileInfo Info => _info ??= new FileInfo(FullPath); diff --git a/StabilityMatrix.Core/Services/IImageIndexService.cs b/StabilityMatrix.Core/Services/IImageIndexService.cs index 5962df87..66430063 100644 --- a/StabilityMatrix.Core/Services/IImageIndexService.cs +++ b/StabilityMatrix.Core/Services/IImageIndexService.cs @@ -9,11 +9,6 @@ public interface IImageIndexService { IndexCollection InferenceImages { get; } - /// - /// Gets a list of local images that start with the given path prefix - /// - Task> GetLocalImagesByPrefix(string pathPrefix); - /// /// Refresh index for all collections /// @@ -25,9 +20,4 @@ public interface IImageIndexService /// Refreshes the index of local images in the background /// void BackgroundRefreshIndex(); - - /// - /// Removes a local image from the database - /// - Task RemoveImage(LocalImageFile imageFile); } diff --git a/StabilityMatrix.Core/Services/ImageIndexService.cs b/StabilityMatrix.Core/Services/ImageIndexService.cs index c7ee7739..a8d734a4 100644 --- a/StabilityMatrix.Core/Services/ImageIndexService.cs +++ b/StabilityMatrix.Core/Services/ImageIndexService.cs @@ -1,11 +1,8 @@ using System.Collections.Concurrent; using System.Diagnostics; -using System.Text.Json; using AsyncAwaitBestPractices; using DynamicData; -using DynamicData.Binding; using Microsoft.Extensions.Logging; -using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; @@ -16,46 +13,30 @@ namespace StabilityMatrix.Core.Services; public class ImageIndexService : IImageIndexService { private readonly ILogger logger; - private readonly ILiteDbContext liteDbContext; private readonly ISettingsManager settingsManager; /// public IndexCollection InferenceImages { get; } - public ImageIndexService( - ILogger logger, - ILiteDbContext liteDbContext, - ISettingsManager settingsManager - ) + public ImageIndexService(ILogger logger, ISettingsManager settingsManager) { this.logger = logger; - this.liteDbContext = liteDbContext; this.settingsManager = settingsManager; InferenceImages = new IndexCollection( this, - file => file.RelativePath + file => file.AbsolutePath ) { - RelativePath = "inference" + RelativePath = "Inference" }; EventManager.Instance.ImageFileAdded += OnImageFileAdded; } - /// - public async Task> GetLocalImagesByPrefix(string pathPrefix) - { - return await liteDbContext.LocalImageFiles - .Query() - .Where(imageFile => imageFile.RelativePath.StartsWith(pathPrefix)) - .ToArrayAsync() - .ConfigureAwait(false); - } - - public async Task RefreshIndexForAllCollections() + public Task RefreshIndexForAllCollections() { - await RefreshIndex(InferenceImages).ConfigureAwait(false); + return RefreshIndex(InferenceImages); } public async Task RefreshIndex(IndexCollection indexCollection) @@ -120,110 +101,9 @@ public class ImageIndexService : IImageIndexService } } - /*public async Task RefreshIndex(IndexCollection indexCollection) - { - var imagesDir = settingsManager.ImagesDirectory; - if (!imagesDir.Exists) - { - return; - } - - // Start - var stopwatch = Stopwatch.StartNew(); - logger.LogInformation("Refreshing images index..."); - - using var db = await liteDbContext.Database.BeginTransactionAsync().ConfigureAwait(false); - - var localImageFiles = db.GetCollection("LocalImageFiles")!; - - await localImageFiles.DeleteAllAsync().ConfigureAwait(false); - - // Record start of actual indexing - var indexStart = stopwatch.Elapsed; - - var added = 0; - - foreach ( - var file in imagesDir.Info - .EnumerateFiles("*.*", SearchOption.AllDirectories) - .Where(info => LocalImageFile.SupportedImageExtensions.Contains(info.Extension)) - .Select(info => new FilePath(info)) - ) - { - var relativePath = Path.GetRelativePath(imagesDir, file); - - // Skip if not in sub-path - if (!string.IsNullOrEmpty(subPath) && !relativePath.StartsWith(subPath)) - { - continue; - } - - // TODO: Support other types - const LocalImageFileType imageType = - LocalImageFileType.Inference | LocalImageFileType.TextToImage; - - // Get metadata - using var reader = new BinaryReader(new FileStream(file.FullPath, FileMode.Open)); - var metadata = ImageMetadata.ReadTextChunk(reader, "parameters-json"); - GenerationParameters? genParams = null; - - if (!string.IsNullOrWhiteSpace(metadata)) - { - genParams = JsonSerializer.Deserialize(metadata); - } - else - { - metadata = ImageMetadata.ReadTextChunk(reader, "parameters"); - if (!string.IsNullOrWhiteSpace(metadata)) - { - GenerationParameters.TryParse(metadata, out genParams); - } - } - - var localImage = new LocalImageFile - { - RelativePath = relativePath, - ImageType = imageType, - CreatedAt = file.Info.CreationTimeUtc, - LastModifiedAt = file.Info.LastWriteTimeUtc, - GenerationParameters = genParams - }; - - // Insert into database - await localImageFiles.InsertAsync(localImage).ConfigureAwait(false); - - added++; - } - // Record end of actual indexing - var indexEnd = stopwatch.Elapsed; - - await db.CommitAsync().ConfigureAwait(false); - - // End - stopwatch.Stop(); - var indexDuration = indexEnd - indexStart; - var dbDuration = stopwatch.Elapsed - indexDuration; - - logger.LogInformation( - "Image index updated for {Prefix} with {Entries} files, took {IndexDuration:F1}ms ({DbDuration:F1}ms db)", - subPath, - added, - indexDuration.TotalMilliseconds, - dbDuration.TotalMilliseconds - ); - }*/ - /// public void BackgroundRefreshIndex() { RefreshIndexForAllCollections().SafeFireAndForget(); } - - /// - public async Task RemoveImage(LocalImageFile imageFile) - { - await liteDbContext.LocalImageFiles - .DeleteAsync(imageFile.RelativePath) - .ConfigureAwait(false); - } } From cbdb5213e10e3d0f035affc06946c5f4f2007b48 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 15 Oct 2023 19:42:36 -0400 Subject: [PATCH 32/43] Fix one click install showing with packages --- StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs index 0bd3f1cf..1abe5f17 100644 --- a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs @@ -108,7 +108,7 @@ public partial class MainWindowViewModel : ViewModelBase var startupTime = CodeTimer.FormatTime(Program.StartupTimer.Elapsed); Logger.Info($"App started ({startupTime})"); - if (Program.Args.DebugOneClickInstall || settingsManager.Settings.InstalledPackages.Any()) + if (Program.Args.DebugOneClickInstall || !settingsManager.Settings.InstalledPackages.Any()) { var viewModel = dialogFactory.Get(); var dialog = new BetterContentDialog From 49b36fdc45de8f089d1148df9643e1c674680cc9 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 15 Oct 2023 19:53:47 -0400 Subject: [PATCH 33/43] Fix design vms using ImageIndexService --- .../DesignData/MockImageIndexService.cs | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs b/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs index 7cc01c83..50c52e81 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using DynamicData; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Services; @@ -8,18 +9,49 @@ namespace StabilityMatrix.Avalonia.DesignData; public class MockImageIndexService : IImageIndexService { /// - public IndexCollection InferenceImages { get; } = - new(null!, file => file.AbsolutePath) { RelativePath = "Inference" }; + public IndexCollection InferenceImages { get; } + + public MockImageIndexService() + { + InferenceImages = new IndexCollection( + this, + file => file.AbsolutePath + ) + { + RelativePath = "Inference" + }; + } /// public Task RefreshIndexForAllCollections() { - return Task.CompletedTask; + return RefreshIndex(InferenceImages); } /// public Task RefreshIndex(IndexCollection indexCollection) { + var toAdd = new LocalImageFile[] + { + new() + { + AbsolutePath = + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/4a7e00a7-6f18-42d4-87c0-10e792df2640/width=1152", + }, + new() + { + AbsolutePath = + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1024" + }, + new() + { + AbsolutePath = + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/16588c94-6595-4be9-8806-d7e6e22d198c/width=1152" + } + }; + + indexCollection.ItemsSource.EditDiff(toAdd, LocalImageFile.Comparer); + return Task.CompletedTask; } From 9747eb0d04a60224fe4e22d7dc26765ae489d41d Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 15 Oct 2023 20:06:21 -0400 Subject: [PATCH 34/43] Add more metadata for design view --- .../DesignData/MockImageIndexService.cs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs b/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs index 50c52e81..d6538b55 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs @@ -28,26 +28,27 @@ public class MockImageIndexService : IImageIndexService return RefreshIndex(InferenceImages); } + private static LocalImageFile GetSampleImage(string url) + { + return new LocalImageFile + { + AbsolutePath = url, + GenerationParameters = GenerationParameters.GetSample(), + ImageSize = new System.Drawing.Size(1024, 1024) + }; + } + /// public Task RefreshIndex(IndexCollection indexCollection) { - var toAdd = new LocalImageFile[] + var toAdd = new[] { - new() - { - AbsolutePath = - "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/4a7e00a7-6f18-42d4-87c0-10e792df2640/width=1152", - }, - new() - { - AbsolutePath = - "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1024" - }, - new() - { - AbsolutePath = - "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/16588c94-6595-4be9-8806-d7e6e22d198c/width=1152" - } + GetSampleImage( + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1024" + ), + GetSampleImage( + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/16588c94-6595-4be9-8806-d7e6e22d198c/width=1152" + ) }; indexCollection.ItemsSource.EditDiff(toAdd, LocalImageFile.Comparer); From a2bcd2268ae80c9996d2e439e3f530ced310eefe Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 15 Oct 2023 20:31:09 -0400 Subject: [PATCH 35/43] Use GenerationParameters interface to save correct sampler name --- .../InferenceTextToImageViewModel.cs | 23 +++---------------- .../Inference/ModelCardViewModel.cs | 6 ++++- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 929aa771..36ab0ac0 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -382,22 +382,7 @@ public class InferenceTextToImageViewModel Client = ClientManager.Client, Nodes = buildPromptArgs.Builder.ToNodeDictionary(), OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(), - Parameters = new GenerationParameters - { - Seed = (ulong)seed, - Steps = SamplerCardViewModel.Steps, - CfgScale = SamplerCardViewModel.CfgScale, - Sampler = SamplerCardViewModel.SelectedSampler?.Name, - ModelName = ModelCardViewModel.SelectedModelName, - ModelHash = ModelCardViewModel - .SelectedModel - ?.Local - ?.ConnectedModelInfo - ?.Hashes - .SHA256, - PositivePrompt = PromptCardViewModel.PromptDocument.Text, - NegativePrompt = PromptCardViewModel.NegativePromptDocument.Text - }, + Parameters = SaveStateToParameters(new GenerationParameters()), Project = InferenceProjectDocument.FromLoadable(this), // Only clear output images on the first batch ClearOutputImages = i == 0 @@ -418,10 +403,9 @@ public class InferenceTextToImageViewModel { PromptCardViewModel.LoadStateFromParameters(parameters); SamplerCardViewModel.LoadStateFromParameters(parameters); + ModelCardViewModel.LoadStateFromParameters(parameters); SeedCardViewModel.Seed = Convert.ToInt64(parameters.Seed); - - ModelCardViewModel.LoadStateFromParameters(parameters); } /// @@ -429,11 +413,10 @@ public class InferenceTextToImageViewModel { parameters = PromptCardViewModel.SaveStateToParameters(parameters); parameters = SamplerCardViewModel.SaveStateToParameters(parameters); + parameters = ModelCardViewModel.SaveStateToParameters(parameters); parameters.Seed = (ulong)SeedCardViewModel.Seed; - parameters = ModelCardViewModel.SaveStateToParameters(parameters); - return parameters; } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs index 2c28da8f..f4db9a4a 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs @@ -114,6 +114,10 @@ public partial class ModelCardViewModel : LoadableViewModelBase, IParametersLoad /// public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { - return parameters with { ModelName = SelectedModel?.FileName }; + return parameters with + { + ModelName = SelectedModel?.FileName, + ModelHash = SelectedModel?.Local?.ConnectedModelInfo?.Hashes.SHA256 + }; } } From f3290bc0f74f9869b8eaa02fe35dda73b4258e0c Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 15 Oct 2023 21:20:13 -0400 Subject: [PATCH 36/43] More generation params in gallery tooltip --- .../Controls/ImageFolderCard.axaml | 205 ++++++++++-------- 1 file changed, 111 insertions(+), 94 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml index 41790214..50691cf1 100644 --- a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml @@ -2,16 +2,12 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:StabilityMatrix.Avalonia.Controls" - xmlns:fluentAvalonia="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia" + xmlns:dbModels="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core" + xmlns:input="using:FluentAvalonia.UI.Input" + xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" - xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models" xmlns:ui="using:FluentAvalonia.UI.Controls" - xmlns:input="using:FluentAvalonia.UI.Input" xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" - xmlns:dbModels="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core" - xmlns:animations="clr-namespace:StabilityMatrix.Avalonia.Animations" - xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" - xmlns:behaviors="clr-namespace:StabilityMatrix.Avalonia.Behaviors" x:DataType="vmInference:ImageFolderCardViewModel"> @@ -22,15 +18,10 @@ - - - + + + + + + + + + + + + + + + + + + - + - + FontSize="12" + Text="{Binding FileNameWithoutExtension}" + TextTrimming="CharacterEllipsis" /> From 25f56186252f3d0e7a14a35b4bb5053f1b2725f2 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 15 Oct 2023 22:29:24 -0400 Subject: [PATCH 37/43] Add tooltips for model card --- .../Controls/ModelCard.axaml | 190 +++++++++++++----- .../DesignData/MockInferenceClientManager.cs | 12 ++ 2 files changed, 157 insertions(+), 45 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/ModelCard.axaml b/StabilityMatrix.Avalonia/Controls/ModelCard.axaml index a3d19de9..81119f2f 100644 --- a/StabilityMatrix.Avalonia/Controls/ModelCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/ModelCard.axaml @@ -1,41 +1,145 @@ - + - - + + + + + + + + + + + + + + + - + IsVisible="{Binding IsRefinerSelectionEnabled}" + Text="{x:Static lang:Resources.Label_Refiner}" + TextAlignment="Left" /> + - - + + - + IsVisible="{Binding IsVaeSelectionEnabled}" + Text="{x:Static lang:Resources.Label_VAE}" + TextAlignment="Left" /> + - + diff --git a/StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs b/StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs index dffaee55..70b928f4 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; +using DynamicData; using DynamicData.Binding; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; @@ -48,6 +49,17 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl /// public bool CanUserDisconnect => IsConnected && !IsConnecting; + public MockInferenceClientManager() + { + Models.AddRange( + new[] + { + HybridModelFile.FromRemote("v1-5-pruned-emaonly.safetensors"), + HybridModelFile.FromRemote("artshaper1.safetensors"), + } + ); + } + /// public Task CopyImageToInputAsync( FilePath imageFile, From ae394a1955da3883f0ea7838f17473f4788fffce Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 15 Oct 2023 21:22:12 -0700 Subject: [PATCH 38/43] Use git operations for package updates & installs. Also adds search box to outputs page --- CHANGELOG.md | 1 + .../Helpers/UnixPrerequisiteHelper.cs | 6 +- .../Helpers/WindowsPrerequisiteHelper.cs | 152 ++++++++----- .../ViewModels/Dialogs/InstallerViewModel.cs | 12 + .../Dialogs/OneClickInstallViewModel.cs | 17 +- .../ViewModels/OutputsPageViewModel.cs | 38 +++- .../PackageManager/PackageCardViewModel.cs | 24 +- .../Views/OutputsPage.axaml | 22 +- .../Views/PackageManagerPage.axaml | 3 +- .../Helper/IPrerequisiteHelper.cs | 13 +- .../Helper/PrerequisiteHelper.cs | 206 ++++++++++++------ .../Models/DownloadPackageVersionOptions.cs | 1 + .../PackageModification/InstallPackageStep.cs | 11 +- .../PackageModification/UpdatePackageStep.cs | 11 +- .../Models/Packages/A3WebUI.cs | 14 +- .../Models/Packages/BaseGitPackage.cs | 187 +++++++++++++--- .../Models/Packages/BasePackage.cs | 2 + .../Models/Packages/ComfyUI.cs | 10 +- .../Models/Packages/Fooocus.cs | 10 +- .../Models/Packages/FooocusMre.cs | 13 +- .../Models/Packages/InvokeAI.cs | 134 +++--------- .../Models/Packages/StableDiffusionUx.cs | 14 +- .../Models/Packages/UnknownPackage.cs | 2 + .../Models/Packages/VladAutomatic.cs | 20 +- .../Models/Packages/VoltaML.cs | 10 +- 25 files changed, 639 insertions(+), 294 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7216ffaf..f2f2c665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Added [Stable Diffusion WebUI/UX](https://github.com/anapnoe/stable-diffusion-webui-ux) package ### Changed - If ComfyUI for Inference is chosen during the One-Click Installer, the Inference page will be opened after installation instead of the Launch page +- Changed all package installs & updates to use git commands instead of downloading zip files ### Fixed - Fixed crash when clicking Inference gallery image after the image is deleted externally in file explorer - Fixed Inference popup Install button not working on One-Click Installer diff --git a/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs b/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs index 07a38fb9..eee43c2c 100644 --- a/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs +++ b/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs @@ -126,7 +126,11 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper } } - public async Task RunGit(string? workingDirectory = null, params string[] args) + public async Task RunGit( + string? workingDirectory = null, + Action? onProcessOutput = null, + params string[] args + ) { var command = args.Length == 0 ? "git" : "git " + string.Join(" ", args.Select(ProcessRunner.Quote)); diff --git a/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs b/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs index e5ee642f..ad77d03a 100644 --- a/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs +++ b/StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs @@ -17,70 +17,82 @@ namespace StabilityMatrix.Avalonia.Helpers; public class WindowsPrerequisiteHelper : IPrerequisiteHelper { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - + private readonly IGitHubClient gitHubClient; private readonly IDownloadService downloadService; private readonly ISettingsManager settingsManager; - + private const string VcRedistDownloadUrl = "https://aka.ms/vs/16/release/vc_redist.x64.exe"; - + private string HomeDir => settingsManager.LibraryDir; - + private string VcRedistDownloadPath => Path.Combine(HomeDir, "vcredist.x64.exe"); private string AssetsDir => Path.Combine(HomeDir, "Assets"); private string SevenZipPath => Path.Combine(AssetsDir, "7za.exe"); - + private string PythonDownloadPath => Path.Combine(AssetsDir, "python-3.10.11-embed-amd64.zip"); private string PythonDir => Path.Combine(AssetsDir, "Python310"); private string PythonDllPath => Path.Combine(PythonDir, "python310.dll"); private string PythonLibraryZipPath => Path.Combine(PythonDir, "python310.zip"); private string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc"); + // Temporary directory to extract venv to during python install private string VenvTempDir => Path.Combine(PythonDir, "venv"); - + private string PortableGitInstallDir => Path.Combine(HomeDir, "PortableGit"); private string PortableGitDownloadPath => Path.Combine(HomeDir, "PortableGit.7z.exe"); private string GitExePath => Path.Combine(PortableGitInstallDir, "bin", "git.exe"); public string GitBinPath => Path.Combine(PortableGitInstallDir, "bin"); - + public bool IsPythonInstalled => File.Exists(PythonDllPath); public WindowsPrerequisiteHelper( IGitHubClient gitHubClient, - IDownloadService downloadService, - ISettingsManager settingsManager) + IDownloadService downloadService, + ISettingsManager settingsManager + ) { this.gitHubClient = gitHubClient; this.downloadService = downloadService; this.settingsManager = settingsManager; } - public async Task RunGit(string? workingDirectory = null, params string[] args) + public async Task RunGit( + string? workingDirectory = null, + Action? onProcessOutput = null, + params string[] args + ) { - var process = ProcessRunner.StartAnsiProcess(GitExePath, args, + var process = ProcessRunner.StartAnsiProcess( + GitExePath, + args, workingDirectory: workingDirectory, environmentVariables: new Dictionary { - {"PATH", Compat.GetEnvPathWithExtensions(GitBinPath)} - }); - + { "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) } + }, + outputDataReceived: onProcessOutput + ); + await ProcessRunner.WaitForExitConditionAsync(process); } public async Task GetGitOutput(string? workingDirectory = null, params string[] args) { var process = await ProcessRunner.GetProcessOutputAsync( - GitExePath, string.Join(" ", args), + GitExePath, + string.Join(" ", args), workingDirectory: workingDirectory, environmentVariables: new Dictionary { - {"PATH", Compat.GetEnvPathWithExtensions(GitBinPath)} - }); - + { "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) } + } + ); + return process; } - + public async Task InstallAllIfNecessary(IProgress? progress = null) { await InstallVcRedistIfNecessary(progress); @@ -97,16 +109,20 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper (Assets.SevenZipExecutable, AssetsDir), (Assets.SevenZipLicense, AssetsDir), }; - - progress?.Report(new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true)); - + + progress?.Report( + new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true) + ); + Directory.CreateDirectory(AssetsDir); foreach (var (asset, extractDir) in assets) { await asset.ExtractToDir(extractDir); } - - progress?.Report(new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false)); + + progress?.Report( + new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false) + ); } public async Task InstallPythonIfNecessary(IProgress? progress = null) @@ -120,7 +136,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper Logger.Info("Python not found at {PythonDllPath}, downloading...", PythonDllPath); Directory.CreateDirectory(AssetsDir); - + // Delete existing python zip if it exists if (File.Exists(PythonLibraryZipPath)) { @@ -130,44 +146,45 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper var remote = Assets.PythonDownloadUrl; var url = remote.Url.ToString(); Logger.Info($"Downloading Python from {url} to {PythonLibraryZipPath}"); - + // Cleanup to remove zip if download fails try { // Download python zip await downloadService.DownloadToFileAsync(url, PythonDownloadPath, progress: progress); - + // Verify python hash var downloadHash = await FileHash.GetSha256Async(PythonDownloadPath, progress); if (downloadHash != remote.HashSha256) { var fileExists = File.Exists(PythonDownloadPath); var fileSize = new FileInfo(PythonDownloadPath).Length; - var msg = $"Python download hash mismatch: {downloadHash} != {remote.HashSha256} " + - $"(file exists: {fileExists}, size: {fileSize})"; + var msg = + $"Python download hash mismatch: {downloadHash} != {remote.HashSha256} " + + $"(file exists: {fileExists}, size: {fileSize})"; throw new Exception(msg); } - + progress?.Report(new ProgressReport(progress: 1f, message: "Python download complete")); - + progress?.Report(new ProgressReport(-1, "Installing Python...", isIndeterminate: true)); - + // We also need 7z if it's not already unpacked if (!File.Exists(SevenZipPath)) { await Assets.SevenZipExecutable.ExtractToDir(AssetsDir); await Assets.SevenZipLicense.ExtractToDir(AssetsDir); } - + // Delete existing python dir if (Directory.Exists(PythonDir)) { Directory.Delete(PythonDir, true); } - + // Unzip python await ArchiveHelper.Extract7Z(PythonDownloadPath, PythonDir); - + try { // Extract embedded venv folder @@ -185,7 +202,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper await resource.ExtractTo(path); } // Add venv to python's library zip - + await ArchiveHelper.AddToArchive7Z(PythonLibraryZipPath, VenvTempDir); } finally @@ -196,16 +213,16 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper Directory.Delete(VenvTempDir, true); } } - + // Extract get-pip.pyc await Assets.PyScriptGetPip.ExtractToDir(PythonDir); - + // We need to uncomment the #import site line in python310._pth for pip to work var pythonPthPath = Path.Combine(PythonDir, "python310._pth"); var pythonPthContent = await File.ReadAllTextAsync(pythonPthPath); pythonPthContent = pythonPthContent.Replace("#import site", "import site"); await File.WriteAllTextAsync(pythonPthPath, pythonPthContent); - + progress?.Report(new ProgressReport(1f, "Python install complete")); } finally @@ -225,7 +242,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper Logger.Debug("Git already installed at {GitExePath}", GitExePath); return; } - + Logger.Info("Git not found at {GitExePath}, downloading...", GitExePath); var portableGitUrl = @@ -233,7 +250,11 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper if (!File.Exists(PortableGitDownloadPath)) { - await downloadService.DownloadToFileAsync(portableGitUrl, PortableGitDownloadPath, progress: progress); + await downloadService.DownloadToFileAsync( + portableGitUrl, + PortableGitDownloadPath, + progress: progress + ); progress?.Report(new ProgressReport(progress: 1f, message: "Git download complete")); } @@ -245,7 +266,9 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper { var registry = Registry.LocalMachine; var key = registry.OpenSubKey( - @"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64", false); + @"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64", + false + ); if (key != null) { var buildId = Convert.ToUInt32(key.GetValue("Bld")); @@ -254,20 +277,44 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper return; } } - + Logger.Info("Downloading VC Redist"); - await downloadService.DownloadToFileAsync(VcRedistDownloadUrl, VcRedistDownloadPath, progress: progress); - progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ download complete", - type: ProgressType.Download)); - + await downloadService.DownloadToFileAsync( + VcRedistDownloadUrl, + VcRedistDownloadPath, + progress: progress + ); + progress?.Report( + new ProgressReport( + progress: 1f, + message: "Visual C++ download complete", + type: ProgressType.Download + ) + ); + Logger.Info("Installing VC Redist"); - progress?.Report(new ProgressReport(progress: 0.5f, isIndeterminate: true, type: ProgressType.Generic, message: "Installing prerequisites...")); - var process = ProcessRunner.StartAnsiProcess(VcRedistDownloadPath, "/install /quiet /norestart"); + progress?.Report( + new ProgressReport( + progress: 0.5f, + isIndeterminate: true, + type: ProgressType.Generic, + message: "Installing prerequisites..." + ) + ); + var process = ProcessRunner.StartAnsiProcess( + VcRedistDownloadPath, + "/install /quiet /norestart" + ); await process.WaitForExitAsync(); - progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ install complete", - type: ProgressType.Generic)); - + progress?.Report( + new ProgressReport( + progress: 1f, + message: "Visual C++ install complete", + type: ProgressType.Generic + ) + ); + File.Delete(VcRedistDownloadPath); } @@ -286,5 +333,4 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper File.Delete(PortableGitDownloadPath); } - } diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs index 33f92b9d..14ddce18 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs @@ -238,6 +238,8 @@ public partial class InstallerViewModel : ContentDialogViewModelBase downloadOptions.VersionTag = SelectedVersion?.TagName ?? throw new NullReferenceException("Selected version is null"); + downloadOptions.IsLatest = + AvailableVersions?.First().TagName == downloadOptions.VersionTag; installedVersion.InstalledReleaseVersion = downloadOptions.VersionTag; } @@ -245,6 +247,11 @@ public partial class InstallerViewModel : ContentDialogViewModelBase { downloadOptions.CommitHash = SelectedCommit?.Sha ?? throw new NullReferenceException("Selected commit is null"); + downloadOptions.BranchName = + SelectedVersion?.TagName + ?? throw new NullReferenceException("Selected version is null"); + downloadOptions.IsLatest = AvailableCommits?.First().Sha == SelectedCommit.Sha; + installedVersion.InstalledBranch = SelectedVersion?.TagName ?? throw new NullReferenceException("Selected version is null"); @@ -259,6 +266,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase var installStep = new InstallPackageStep( SelectedPackage, SelectedTorchVersion, + downloadOptions, installLocation ); var setupModelFoldersStep = new SetupModelFoldersStep( @@ -307,6 +315,10 @@ public partial class InstallerViewModel : ContentDialogViewModelBase { SelectedVersion = null; } + else if (SelectedPackage is FooocusMre) + { + SelectedVersion = AvailableVersions.FirstOrDefault(x => x.TagName == "moonride-main"); + } else { // First try to find master diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs index 9720cc8b..c4e34c99 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs @@ -145,7 +145,7 @@ public partial class OneClickInstallViewModel : ViewModelBase // get latest version & download & install var installLocation = Path.Combine(libraryDir, "Packages", SelectedPackage.Name); - var downloadVersion = new DownloadPackageVersionOptions(); + var downloadVersion = new DownloadPackageVersionOptions { IsLatest = true }; var installedVersion = new InstalledPackageVersion(); var versionOptions = await SelectedPackage.GetAllVersionOptions(); @@ -157,13 +157,19 @@ public partial class OneClickInstallViewModel : ViewModelBase else { downloadVersion.BranchName = await SelectedPackage.GetLatestVersion(); + downloadVersion.CommitHash = + (await SelectedPackage.GetAllCommits(downloadVersion.BranchName)) + ?.FirstOrDefault() + ?.Sha ?? string.Empty; + installedVersion.InstalledBranch = downloadVersion.BranchName; + installedVersion.InstalledCommitSha = downloadVersion.CommitHash; } var torchVersion = SelectedPackage.GetRecommendedTorchVersion(); await DownloadPackage(installLocation, downloadVersion); - await InstallPackage(installLocation, torchVersion); + await InstallPackage(installLocation, torchVersion, downloadVersion); var recommendedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod; await SelectedPackage.SetupModelFolders(installLocation, recommendedSharedFolderMethod); @@ -222,7 +228,11 @@ public partial class OneClickInstallViewModel : ViewModelBase OneClickInstallProgress = 100; } - private async Task InstallPackage(string installLocation, TorchVersion torchVersion) + private async Task InstallPackage( + string installLocation, + TorchVersion torchVersion, + DownloadPackageVersionOptions versionOptions + ) { var progress = new Progress(progress => { @@ -235,6 +245,7 @@ public partial class OneClickInstallViewModel : ViewModelBase await SelectedPackage.InstallPackage( installLocation, torchVersion, + versionOptions, progress, (output) => SubSubHeaderText = output.Text ); diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs index 9dac32ed..7eafc089 100644 --- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs @@ -56,6 +56,9 @@ public partial class OutputsPageViewModel : PageViewModelBase public IObservableCollection Outputs { get; set; } = new ObservableCollectionExtended(); + public IObservableCollection FilteredOutputs { get; set; } = + new ObservableCollectionExtended(); + public IEnumerable OutputTypes { get; } = Enum.GetValues(); [ObservableProperty] @@ -72,6 +75,9 @@ public partial class OutputsPageViewModel : PageViewModelBase [NotifyPropertyChangedFor(nameof(NumImagesSelected))] private int numItemsSelected; + [ObservableProperty] + private string searchQuery; + public bool CanShowOutputTypes => SelectedCategory.Name.Equals("Shared Output Folder"); public string NumImagesSelected => @@ -95,8 +101,24 @@ public partial class OutputsPageViewModel : PageViewModelBase OutputsCache .Connect() .DeferUntilLoaded() + .Filter(output => + { + if (string.IsNullOrWhiteSpace(SearchQuery)) + return true; + + return output.ImageFile.FileName.Contains( + SearchQuery, + StringComparison.OrdinalIgnoreCase + ) + || ( + output.ImageFile.GenerationParameters?.PositivePrompt != null + && output.ImageFile.GenerationParameters.PositivePrompt.Contains( + SearchQuery, + StringComparison.OrdinalIgnoreCase + ) + ); + }) .SortBy(x => x.ImageFile.CreatedAt, SortDirection.Descending) - .ObserveOn(SynchronizationContext.Current) .Bind(Outputs) .WhenPropertyChanged(p => p.IsSelected) .Subscribe(_ => @@ -135,6 +157,7 @@ public partial class OutputsPageViewModel : PageViewModelBase Categories = new ObservableCollection(packageCategories); SelectedCategory = Categories.First(); SelectedOutputType = SharedOutputType.All; + SearchQuery = string.Empty; } public override void OnLoaded() @@ -176,6 +199,11 @@ public partial class OutputsPageViewModel : PageViewModelBase GetOutputs(path); } + partial void OnSearchQueryChanged(string value) + { + OutputsCache.Refresh(); + } + public async Task OnImageClick(OutputImageViewModel item) { // Select image if we're in "select mode" @@ -280,16 +308,16 @@ public partial class OutputsPageViewModel : PageViewModelBase } } - public void SendToTextToImage(LocalImageFile image) + public void SendToTextToImage(OutputImageViewModel vm) { navigationService.NavigateTo(); - EventManager.Instance.OnInferenceTextToImageRequested(image); + EventManager.Instance.OnInferenceTextToImageRequested(vm.ImageFile); } - public void SendToUpscale(LocalImageFile image) + public void SendToUpscale(OutputImageViewModel vm) { navigationService.NavigateTo(); - EventManager.Instance.OnInferenceUpscaleRequested(image); + EventManager.Instance.OnInferenceUpscaleRequested(vm.ImageFile); } public void ClearSelection() diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs index cca0b036..03faa2d3 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs @@ -240,7 +240,29 @@ public partial class PackageCardViewModel : ProgressViewModel { ModificationCompleteMessage = $"{packageName} Update Complete" }; - var updatePackageStep = new UpdatePackageStep(settingsManager, Package, basePackage); + + var versionOptions = new DownloadPackageVersionOptions { IsLatest = true }; + if (Package.Version.IsReleaseMode) + { + versionOptions.VersionTag = await basePackage.GetLatestVersion(); + } + else + { + var commits = await basePackage.GetAllCommits(Package.Version.InstalledBranch); + var latest = commits?.FirstOrDefault(); + if (latest == null) + throw new Exception("Could not find latest commit"); + + versionOptions.BranchName = Package.Version.InstalledBranch; + versionOptions.CommitHash = latest.Sha; + } + + var updatePackageStep = new UpdatePackageStep( + settingsManager, + Package, + versionOptions, + basePackage + ); var steps = new List { updatePackageStep }; EventManager.Instance.OnPackageInstallProgressAdded(runner); diff --git a/StabilityMatrix.Avalonia/Views/OutputsPage.axaml b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml index 8fdcac4f..6a17f7b1 100644 --- a/StabilityMatrix.Avalonia/Views/OutputsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml @@ -25,7 +25,7 @@ + ColumnDefinitions="Auto, Auto, Auto, Auto, *, Auto, Auto"> + + +