From 4371da24b3e434fb1fb5e864cb4479283ce2fd97 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 30 Jul 2023 14:14:16 -0400 Subject: [PATCH 001/474] Add InferencePage --- StabilityMatrix.Avalonia/App.axaml.cs | 5 +- .../Views/InferencePage.axaml | 207 ++++++++++++++++++ .../Views/InferencePage.axaml.cs | 46 ++++ 3 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 StabilityMatrix.Avalonia/Views/InferencePage.axaml create mode 100644 StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 70f5dcc1f..42c1a5156 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -203,7 +203,8 @@ internal static void ConfigurePageViewModels(IServiceCollection services) .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); services.AddSingleton(provider => new MainWindowViewModel(provider.GetRequiredService(), @@ -212,6 +213,7 @@ internal static void ConfigurePageViewModels(IServiceCollection services) Pages = { provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), @@ -277,6 +279,7 @@ internal static void ConfigureViews(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Dialogs services.AddTransient(); diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml b/StabilityMatrix.Avalonia/Views/InferencePage.axaml new file mode 100644 index 000000000..07643ea0d --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + - - - - - + + + - + + + + - - - - - - - - + Margin="2" + FontSize="14" + Text="Prompt" /> + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -77,6 +74,7 @@ Text="Prompt" /> diff --git a/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml b/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml index 7b1266ea4..4c2ec82e0 100644 --- a/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml +++ b/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml @@ -5,6 +5,7 @@ xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:labs="clr-namespace:Avalonia.Labs.Controls;assembly=Avalonia.Labs.Controls" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:system="clr-namespace:System;assembly=System.Runtime" @@ -35,12 +36,11 @@ Margin="0,0,8,0" VerticalAlignment="Center" Text="Model" /> - + SelectedItem="{Binding SelectedModelName, Mode=TwoWay}" /> @@ -48,9 +48,22 @@ + + + + + + - @@ -60,61 +73,68 @@ Background="{DynamicResource ScrollBarTrackStroke}" /> - - - + Margin="8,8,8,16"> + + + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + VerticalAlignment="Stretch" + HorizontalAlignment="Center" + SelectionMode="AlwaysSelected" + SelectedItem="{Binding SelectedImage}" + ItemsSource="{Binding ImageSources}"> + + + + + + + + diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 350742359..ef74a2032 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -374,10 +374,9 @@ public static void Initialize() { vm.ImageSources.AddRange(new [] { - "https://picsum.photos/seed/i1/200/300", - "https://picsum.photos/seed/i2/200/300", - "https://picsum.photos/seed/i3/200/300", - "https://picsum.photos/seed/i4/200/300", + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/4a7e00a7-6f18-42d4-87c0-10e792df2640/width=1152", + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1024", + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/16588c94-6595-4be9-8806-d7e6e22d198c/width=1152", }); }); diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs index 13694a13d..5b36c629e 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs @@ -14,4 +14,5 @@ public partial class ImageGalleryCardViewModel : ViewModelBase [ObservableProperty] private IImage? previewImage; [ObservableProperty] private AvaloniaList imageSources = new(); + [ObservableProperty] private string? selectedImage; } From a02a04a3da8263ba3ec9d0be370001397eed9284 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 5 Aug 2023 02:40:18 -0400 Subject: [PATCH 033/474] Add open project command --- .../ViewModels/InferenceViewModel.cs | 65 ++++++++++++++++++- .../Views/InferencePage.axaml | 1 + 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs index 009fac8e4..ee2e07ab4 100644 --- a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs @@ -8,6 +8,7 @@ using FluentAvalonia.UI.Controls; using NLog; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.Views; @@ -147,6 +148,7 @@ private async Task MenuSaveAs() var provider = App.StorageProvider; var projectDir = new DirectoryPath(settingsManager.LibraryDir, "Projects"); + projectDir.Create(); var startDir = await provider.TryGetFolderFromPathAsync(projectDir); var result = await provider.SaveFilePickerAsync(new FilePickerSaveOptions @@ -157,7 +159,7 @@ private async Task MenuSaveAs() { new("StabilityMatrix Project") { - Patterns = new[] { ".smproj" }, + Patterns = new[] { "*.smproj" }, MimeTypes = new[] { "application/json" }, } }, @@ -181,4 +183,65 @@ private async Task MenuSaveAs() notificationService.Show("Saved", $"Saved project to {result.Name}", NotificationType.Success); } + + /// + /// Menu "Open Project" command. + /// + [RelayCommand] + private async Task MenuOpenProject() + { + // Prompt for open file dialog + var provider = App.StorageProvider; + + var projectDir = new DirectoryPath(settingsManager.LibraryDir, "Projects"); + projectDir.Create(); + var startDir = await provider.TryGetFolderFromPathAsync(projectDir); + + var results = await provider.OpenFilePickerAsync(new FilePickerOpenOptions + { + Title = "Open Project File", + FileTypeFilter = new FilePickerFileType[] + { + new("StabilityMatrix Project") + { + Patterns = new[] { "*.smproj" }, + MimeTypes = new[] { "application/json" }, + } + }, + SuggestedStartLocation = startDir, + }); + + if (results.Count == 0) + { + Logger.Trace("MenuOpenProject: No files selected"); + return; + } + + // Load from file + var file = results[0]; + await using var stream = await file.OpenReadAsync(); + + var document = await JsonSerializer.DeserializeAsync(stream); + if (document == null) + { + Logger.Warn("MenuOpenProject: Deserialize project file returned null"); + return; + } + + ViewModelBase? vm = null; + if (document.ProjectType is InferenceProjectType.TextToImage) + { + var textToImage = CreateTextToImageViewModel(); + textToImage.LoadState(document.State.Deserialize()!); + vm = textToImage; + } + + if (vm == null) + { + Logger.Warn("MenuOpenProject: Unknown project type"); + return; + } + + Tabs.Add(vm); + } } diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml b/StabilityMatrix.Avalonia/Views/InferencePage.axaml index 5034d210d..8fb9bbc5a 100644 --- a/StabilityMatrix.Avalonia/Views/InferencePage.axaml +++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml @@ -53,6 +53,7 @@ Date: Sat, 5 Aug 2023 02:40:34 -0400 Subject: [PATCH 034/474] Add image copy to clipboard flyout --- .../Controls/ImageGalleryCard.axaml | 21 +++- .../Helpers/Win32ClipboardFormat.cs | 21 ++++ .../Helpers/WindowsClipboard.cs | 99 +++++++++++++++++++ .../Inference/ImageGalleryCardViewModel.cs | 18 ++++ 4 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Helpers/Win32ClipboardFormat.cs create mode 100644 StabilityMatrix.Avalonia/Helpers/WindowsClipboard.cs diff --git a/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml b/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml index 9647270e9..028ee835c 100644 --- a/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml @@ -13,7 +13,8 @@ - + + @@ -39,10 +40,11 @@ + @@ -56,15 +58,28 @@ + + StretchDirection="Both"> + + + + + + + imageSources = new(); [ObservableProperty] private string? selectedImage; + + [RelayCommand] + private void FlyoutCopy(IImage? image) + { + if (image is null) + { + Logger.Trace("FlyoutCopy: image is null"); + return; + } + Logger.Trace($"FlyoutCopy is copying {image}"); + WindowsClipboard.SetBitmap((Bitmap) image); + } } From b123a5ffb4317c01581292e2571dfb655fc8d255 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 5 Aug 2023 02:43:43 -0400 Subject: [PATCH 035/474] Fix command name --- StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml b/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml index 028ee835c..da0e8d1a7 100644 --- a/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml @@ -70,7 +70,7 @@ From c4342a833670eb33dbaa1e04828b01a172c62f67 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 5 Aug 2023 02:52:58 -0400 Subject: [PATCH 036/474] Add compatibility guards for image copy --- .../Controls/ImageGalleryCard.axaml | 1 + .../Helpers/WindowsClipboard.cs | 2 ++ .../Inference/ImageGalleryCardViewModel.cs | 13 ++++++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml b/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml index da0e8d1a7..9c0b8d6c7 100644 --- a/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml @@ -70,6 +70,7 @@ WindowsClipboard.SetBitmap((Bitmap) image)); } } From c45febd887141511688f34ceceda4af33352d049 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 5 Aug 2023 03:53:17 -0400 Subject: [PATCH 037/474] Fix seed card randomize state --- StabilityMatrix.Avalonia/Controls/SeedCard.axaml | 2 +- .../ViewModels/Inference/SeedCardViewModel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/SeedCard.axaml b/StabilityMatrix.Avalonia/Controls/SeedCard.axaml index 096d67619..84e03ad77 100644 --- a/StabilityMatrix.Avalonia/Controls/SeedCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/SeedCard.axaml @@ -45,7 +45,7 @@ { [ObservableProperty, NotifyPropertyChangedFor(nameof(RandomizeButtonToolTip))] - private bool isRandomizeEnabled; + private bool isRandomizeEnabled = true; [ObservableProperty] private long seed; From 6527cb7506e6c6473bea400b65febec2e03a6ce2 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 5 Aug 2023 03:53:24 -0400 Subject: [PATCH 038/474] Add tab closing --- .../ViewModels/InferenceViewModel.cs | 11 +++++++++++ StabilityMatrix.Avalonia/Views/InferencePage.axaml | 1 + StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs | 7 +++++++ 3 files changed, 19 insertions(+) diff --git a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs index ee2e07ab4..8de934a8d 100644 --- a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs @@ -98,6 +98,17 @@ private void AddTab() { Tabs.Add(CreateTextToImageViewModel()); } + + /// + /// When the close button on the tab is clicked, remove the tab. + /// + public void OnTabCloseRequested(TabViewTabCloseRequestedEventArgs e) + { + if (e.Item is ViewModelBase vm) + { + Tabs.Remove(vm); + } + } /// /// Connect to the inference server. diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml b/StabilityMatrix.Avalonia/Views/InferencePage.axaml index 8fb9bbc5a..eb0d6a4c9 100644 --- a/StabilityMatrix.Avalonia/Views/InferencePage.axaml +++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml @@ -23,6 +23,7 @@ VerticalAlignment="Stretch" CanReorderTabs="True" CloseButtonOverlayMode="Auto" + TabCloseRequested="TabView_OnTabCloseRequested" AddTabButtonCommand="{Binding AddTabCommand}" TabItems="{Binding Tabs}" SelectedItem="{Binding SelectedTab, Mode=TwoWay}" diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs b/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs index bdf809042..e357ff6d1 100644 --- a/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs @@ -1,5 +1,7 @@ using Avalonia.Markup.Xaml; +using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.Views; @@ -14,4 +16,9 @@ private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void TabView_OnTabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args) + { + (DataContext as InferenceViewModel)?.OnTabCloseRequested(args); + } } From 604171acfa495b3ab0acbe2113e0941fba56cd46 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 6 Aug 2023 03:07:43 -0400 Subject: [PATCH 039/474] Add control views to DI --- StabilityMatrix.Avalonia/App.axaml.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index d6835c64d..d3f4135c7 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -294,6 +294,11 @@ internal static void ConfigureViews(IServiceCollection services) // Inference tabs services.AddTransient(); + // Inference controls + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + // Dialogs services.AddTransient(); services.AddTransient(); From 69a729bfd84a54ec74b803429995016ec14d82cb Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 6 Aug 2023 14:14:59 -0400 Subject: [PATCH 040/474] Hires Fix UI and Inference connection error handling --- .../Controls/ImageGalleryCard.axaml | 50 ++-- .../Controls/SamplerCard.axaml | 84 +++++- .../Controls/SeedCard.axaml | 1 + .../DesignData/DesignData.cs | 13 +- .../Models/Inference/SamplerCardModel.cs | 21 +- .../Services/InferenceClientManager.cs | 5 +- .../Inference/ImageGalleryCardViewModel.cs | 37 ++- .../InferenceTextToImageViewModel.cs | 45 +++- .../Inference/SamplerCardViewModel.cs | 27 ++ .../ViewModels/InferenceViewModel.cs | 10 +- .../Views/InferenceTextToImageView.axaml | 239 +++++++++++------- 11 files changed, 388 insertions(+), 144 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml b/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml index 9c0b8d6c7..f9f85ead8 100644 --- a/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml @@ -29,20 +29,15 @@ VerticalAlignment="{TemplateBinding VerticalAlignment}"> - - - + + @@ -92,14 +87,37 @@ StretchDirection="Both" /> - + + + + - - + HorizontalAlignment="Right"> + + + - + + + + + + + + + @@ -15,19 +23,25 @@ - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/StabilityMatrix.Avalonia/Controls/SeedCard.axaml b/StabilityMatrix.Avalonia/Controls/SeedCard.axaml index 84e03ad77..3736348fd 100644 --- a/StabilityMatrix.Avalonia/Controls/SeedCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/SeedCard.axaml @@ -29,6 +29,7 @@ DialogFactory.Get(vm => + { + vm.Steps = 20; + vm.CfgScale = 7; + vm.SelectedSampler = "Euler a"; + vm.IsScaleSizeMode = true; + vm.IsCfgScaleEnabled = false; + vm.IsSamplerSelectionEnabled = false; + vm.IsDenoiseStrengthEnabled = true; + }); + public static ImageGalleryCardViewModel ImageGalleryCardViewModel => DialogFactory.Get(vm => { diff --git a/StabilityMatrix.Avalonia/Models/Inference/SamplerCardModel.cs b/StabilityMatrix.Avalonia/Models/Inference/SamplerCardModel.cs index 6b907759e..7008b7215 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/SamplerCardModel.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/SamplerCardModel.cs @@ -5,9 +5,20 @@ namespace StabilityMatrix.Avalonia.Models.Inference; [JsonSerializable(typeof(SamplerCardModel))] public class SamplerCardModel { - public int Steps { get; set; } - public double CfgScale { get; set; } - public int Width { get; set; } - public int Height { get; set; } - public string? SelectedSampler { get; set; } + public int Steps { get; init; } + + public bool IsDenoiseStrengthEnabled { get; init; } = false; + public double DenoiseStrength { get; init; } + + public bool IsCfgScaleEnabled { get; init; } = true; + public double CfgScale { get; init; } + + public bool IsScaleSizeMode { get; init; } + public int Width { get; init; } + public int Height { get; init; } + + public double Scale { get; init; } + + public bool IsSamplerSelectionEnabled { get; init; } = true; + public string? SelectedSampler { get; init; } } diff --git a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs index 0cc67c42a..f643fe1ae 100644 --- a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs @@ -51,8 +51,9 @@ public async Task ConnectAsync() { if (IsConnected) return; - Client = new ComfyClient(apiFactory, new Uri("http://127.0.0.1:8188")); - await Client.ConnectAsync(); + var tempClient = new ComfyClient(apiFactory, new Uri("http://127.0.0.1:8188")); + await tempClient.ConnectAsync(); + Client = tempClient; await LoadSharedPropertiesAsync(); } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs index f81865f31..f0c2edb68 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageGalleryCardViewModel.cs @@ -17,27 +17,46 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference; public partial class ImageGalleryCardViewModel : ViewModelBase { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - - [ObservableProperty] private bool isPreviewOverlayEnabled; - [ObservableProperty] private IImage? previewImage; + [ObservableProperty] + private bool isPreviewOverlayEnabled; - [ObservableProperty] private AvaloniaList imageSources = new(); - [ObservableProperty] private string? selectedImage; + [ObservableProperty] + private IImage? previewImage; + + [ObservableProperty] + private AvaloniaList imageSources = new(); + + [ObservableProperty] + private string? selectedImage; + + [ + ObservableProperty, + NotifyPropertyChangedFor(nameof(CanNavigateBack), nameof(CanNavigateForward)) + ] + private int selectedImageIndex; + + public bool CanNavigateBack => SelectedImageIndex > 0; + public bool CanNavigateForward => SelectedImageIndex < ImageSources.Count - 1; [RelayCommand] - [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] // ReSharper disable once UnusedMember.Local private async Task FlyoutCopy(IImage? image) { - if (!Compat.IsWindows) return; - if (image is null) { Logger.Trace("FlyoutCopy: image is null"); return; } + Logger.Trace($"FlyoutCopy is copying {image}"); - await Task.Run(() => WindowsClipboard.SetBitmap((Bitmap) image)); + + await Task.Run(() => + { + if (Compat.IsWindows) + { + WindowsClipboard.SetBitmap((Bitmap)image); + } + }); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 70e58be90..1974d50bf 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Avalonia.Media.Imaging; using AvaloniaEdit.Document; @@ -31,6 +33,7 @@ public partial class InferenceTextToImageViewModel : ViewModelBase, ILoadableSta // These are set in OnLoaded due to needing the vmFactory [NotNull] public SeedCardViewModel? SeedCardViewModel { get; private set; } [NotNull] public SamplerCardViewModel? SamplerCardViewModel { get; private set; } + [NotNull] public SamplerCardViewModel? HiresFixSamplerCardViewModel { get; private set; } [NotNull] public ImageGalleryCardViewModel? ImageGalleryCardViewModel { get; private set; } public InferenceViewModel? Parent { get; set; } @@ -59,11 +62,20 @@ public InferenceTextToImageViewModel( // ReSharper disable twice NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract SeedCardViewModel ??= vmFactory.Get(); - OnPropertyChanged(nameof(SeedCardViewModel)); + // OnPropertyChanged(nameof(SeedCardViewModel)); SamplerCardViewModel ??= vmFactory.Get(); - OnPropertyChanged(nameof(SamplerCardViewModel)); - ImageGalleryCardViewModel ??= vmFactory.Get(); - OnPropertyChanged(nameof(ImageGalleryCardViewModel)); + // OnPropertyChanged(nameof(SamplerCardViewModel)); + HiresFixSamplerCardViewModel ??= vmFactory.Get(vm => + { + vm.IsScaleSizeMode = true; + vm.IsCfgScaleEnabled = false; + vm.IsSamplerSelectionEnabled = false; + vm.IsDenoiseStrengthEnabled = true; + }); + // OnPropertyChanged(nameof(HiresFixSamplerCardViewModel)); + // ImageGalleryCardViewModel ??= vmFactory.Get(); + ImageGalleryCardViewModel = new ImageGalleryCardViewModel(); + // OnPropertyChanged(nameof(ImageGalleryCardViewModel)); SeedCardViewModel.GenerateNewSeed(); @@ -201,8 +213,7 @@ private void OnPreviewImageReceived(object? sender, ComfyWebSocketImageData args ImageGalleryCardViewModel.IsPreviewOverlayEnabled = true; } - [RelayCommand] - private async Task GenerateImage() + private async Task GenerateImageImpl(CancellationToken cancellationToken = default) { if (!ClientManager.IsConnected) { @@ -227,15 +238,16 @@ private async Task GenerateImage() try { - var (response, promptTask) = await client.QueuePromptAsync(nodes); + var (response, promptTask) = await client.QueuePromptAsync(nodes, cancellationToken); Logger.Info(response); // Wait for prompt to finish - await promptTask; + await promptTask.WaitAsync(cancellationToken); Logger.Trace($"Prompt task {response.PromptId} finished"); // Get output images - var outputs = await client.GetImagesForExecutedPromptAsync(response.PromptId); + var outputs = await client + .GetImagesForExecutedPromptAsync(response.PromptId, cancellationToken); // Only get the SaveImage from node 9 var images = outputs["9"]; @@ -255,6 +267,19 @@ private async Task GenerateImage() client.PreviewImageReceived -= OnPreviewImageReceived; } } + + [RelayCommand(IncludeCancelCommand = true)] + private async Task GenerateImage(CancellationToken cancellationToken = default) + { + try + { + await GenerateImageImpl(cancellationToken); + } + catch (OperationCanceledException e) + { + Logger.Debug($"[Image Generation Canceled] {e.Message}"); + } + } /// public void LoadState(InferenceTextToImageModel state) diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs index a74109398..b9db3f5fa 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs @@ -12,10 +12,25 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference; public partial class SamplerCardViewModel : ViewModelBase, ILoadableState { [ObservableProperty] private int steps = 20; + + [ObservableProperty] private bool isDenoiseStrengthEnabled = true; + [ObservableProperty] private double denoiseStrength = 1; + + [ObservableProperty] private bool isCfgScaleEnabled = true; [ObservableProperty] private double cfgScale = 7; + + // Switch between the 2 size modes + [ObservableProperty] private bool isScaleSizeMode; + + // Absolute size mode [ObservableProperty] private int width = 512; [ObservableProperty] private int height = 512; + // Scale size mode + [ObservableProperty] private double scale = 1; + + [ObservableProperty] private bool isSamplerSelectionEnabled = true; + [ObservableProperty, Required] private string? selectedSampler; @@ -30,9 +45,15 @@ public SamplerCardViewModel(IInferenceClientManager clientManager) public void LoadState(SamplerCardModel state) { Steps = state.Steps; + IsDenoiseStrengthEnabled = state.IsDenoiseStrengthEnabled; + DenoiseStrength = state.DenoiseStrength; + IsCfgScaleEnabled = state.IsCfgScaleEnabled; CfgScale = state.CfgScale; + IsScaleSizeMode = state.IsScaleSizeMode; Width = state.Width; Height = state.Height; + Scale = state.Scale; + IsSamplerSelectionEnabled = state.IsSamplerSelectionEnabled; SelectedSampler = state.SelectedSampler; } @@ -42,9 +63,15 @@ public SamplerCardModel SaveState() return new SamplerCardModel { Steps = Steps, + IsDenoiseStrengthEnabled = IsDenoiseStrengthEnabled, + DenoiseStrength = DenoiseStrength, + IsCfgScaleEnabled = IsCfgScaleEnabled, CfgScale = CfgScale, + IsScaleSizeMode = IsScaleSizeMode, Width = Width, Height = Height, + Scale = Scale, + IsSamplerSelectionEnabled = IsSamplerSelectionEnabled, SelectedSampler = SelectedSampler }; } diff --git a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs index 8de934a8d..d845d2441 100644 --- a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System; +using System.Text.Json; using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Controls.Notifications; @@ -122,7 +123,9 @@ private async Task Connect() return; } // TODO: make address configurable - await ClientManager.ConnectAsync(); + + await notificationService.TryAsync(ClientManager.ConnectAsync(), + "Could not connect to ComfyUI backend"); } /// @@ -137,7 +140,8 @@ private async Task Disconnect() return; } - await ClientManager.CloseAsync(); + await notificationService.TryAsync(ClientManager.CloseAsync(), + "Could not disconnect from ComfyUI backend"); } /// diff --git a/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml b/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml index 6740eaa5f..fb41932ae 100644 --- a/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml +++ b/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml @@ -1,19 +1,37 @@  + + + + + + + + - - + + + + + + + + + + + + + + + + - - + + + + + + Increment="1" + ParsingNumberStyle="Integer" + Value="{Binding BatchSize}" + ClipValueToMinMax="True"/> + + + + + + - - - + - + + Margin="8,8,8,16" + Grid.RowDefinitions="*,Auto"> + - - + DockPanel.Dock="Top"> + + + + + + - + + - - - - - - + + + + + + + - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - + - + + + + + + - - - - + - - - - - - - public void OnTabCloseRequested(TabViewTabCloseRequestedEventArgs e) { - if (e.Item is ViewModelBase vm) + if (e.Item is LoadableViewModelBase vm) { Tabs.Remove(vm); } @@ -259,17 +260,17 @@ private async Task MenuOpenProject() await using var stream = await file.OpenReadAsync(); var document = await JsonSerializer.DeserializeAsync(stream); - if (document == null) + if (document is null) { Logger.Warn("MenuOpenProject: Deserialize project file returned null"); return; } - ViewModelBase? vm = null; - if (document.ProjectType is InferenceProjectType.TextToImage) + LoadableViewModelBase? vm = null; + if (document.ProjectType is InferenceProjectType.TextToImage && document.State is not null) { var textToImage = vmFactory.Get(); - textToImage.LoadState(document.State.Deserialize()!); + textToImage.LoadStateFromJsonObject(document.State); vm = textToImage; } diff --git a/StabilityMatrix.Avalonia/ViewModels/LoadableViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/LoadableViewModelBase.cs new file mode 100644 index 000000000..2c501715b --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/LoadableViewModelBase.cs @@ -0,0 +1,34 @@ +using System; +using System.Text.Json; +using System.Text.Json.Nodes; +using StabilityMatrix.Avalonia.Models; + +namespace StabilityMatrix.Avalonia.ViewModels; + +public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState +{ + /// + public abstract void LoadStateFromJsonObject(JsonObject state); + + /// + public abstract JsonObject SaveStateToJsonObject(); + + /// + /// Serialize a model to a JSON object. + /// + protected static JsonObject SerializeModel(T model) + { + var node = JsonSerializer.SerializeToNode(model); + return node?.AsObject() ?? throw new + NullReferenceException("Failed to serialize state to JSON object."); + } + + /// + /// Deserialize a model from a JSON object. + /// + protected static T DeserializeModel(JsonObject state) + { + return state.Deserialize() ?? throw new + NullReferenceException("Failed to deserialize state from JSON object."); + } +} diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs b/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs index e357ff6d1..f94b40189 100644 --- a/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs @@ -1,4 +1,5 @@ -using Avalonia.Markup.Xaml; +using Avalonia.Input; +using Avalonia.Markup.Xaml; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.ViewModels; @@ -10,6 +11,8 @@ public partial class InferencePage : UserControlBase public InferencePage() { InitializeComponent(); + AddHandler(DragDrop.DropEvent, DropHandler); + AddHandler(DragDrop.DragOverEvent, DragOverHandler); } private void InitializeComponent() @@ -21,4 +24,20 @@ private void TabView_OnTabCloseRequested(TabView sender, TabViewTabCloseRequeste { (DataContext as InferenceViewModel)?.OnTabCloseRequested(args); } + + private void DragOverHandler(object? sender, DragEventArgs e) + { + if (DataContext is IDropTarget dropTarget) + { + dropTarget.DragOver(sender, e); + } + } + + private void DropHandler(object? sender, DragEventArgs e) + { + if (DataContext is IDropTarget dropTarget) + { + dropTarget.Drop(sender, e); + } + } } diff --git a/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml b/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml index 96221bdbd..74f75dcdf 100644 --- a/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml +++ b/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml @@ -88,7 +88,7 @@ x:DataType="Tool" Id="ConfigTool"> + DataContext="{ReflectionBinding ElementName=Dock, Path=DataContext.StackCardViewModel}"/> @@ -122,7 +122,17 @@ Title="Image Output" x:DataType="Tool" Id="ImageGalleryTool"> - + + + + + + From f4b9f72cdde0de1b6a8d547bd31a6148fcb892a4 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 9 Aug 2023 19:55:08 -0400 Subject: [PATCH 061/474] Add Dock.Avalonia and Dock.Model.Avalonia nugets --- StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index c4c93d3db..2f932d79a 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -25,6 +25,8 @@ + + From 40bffe470abc037d744967cc58dfdaa088ac0f79 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 9 Aug 2023 19:56:56 -0400 Subject: [PATCH 062/474] Remove unused usings --- StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs index a27659e75..b9715c4c8 100644 --- a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs @@ -1,17 +1,13 @@ -using System; -using System.Text.Json; +using System.Text.Json; using System.Threading.Tasks; using Avalonia.Collections; -using Avalonia.Controls.Documents; using Avalonia.Controls.Notifications; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Dock.Model.Mvvm.Controls; using FluentAvalonia.UI.Controls; using NLog; using StabilityMatrix.Avalonia.Models; -using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.Views; From 278ed4b255618f8efa1270200307edf19334359f Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 9 Aug 2023 19:58:06 -0400 Subject: [PATCH 063/474] Create IDropTarget.cs --- StabilityMatrix.Avalonia/ViewModels/IDropTarget.cs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 StabilityMatrix.Avalonia/ViewModels/IDropTarget.cs diff --git a/StabilityMatrix.Avalonia/ViewModels/IDropTarget.cs b/StabilityMatrix.Avalonia/ViewModels/IDropTarget.cs new file mode 100644 index 000000000..6cc625bf8 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/IDropTarget.cs @@ -0,0 +1,9 @@ +using Avalonia.Input; + +namespace StabilityMatrix.Avalonia.ViewModels; + +public interface IDropTarget +{ + void DragOver(object? sender, DragEventArgs e); + void Drop(object? sender, DragEventArgs e); +} From 7d6810b213bb0cd29f19694e0714f34d54e866e5 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 9 Aug 2023 20:01:57 -0400 Subject: [PATCH 064/474] Add scrollviewer to stackcard --- .../Controls/StackCard.axaml | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/StackCard.axaml b/StabilityMatrix.Avalonia/Controls/StackCard.axaml index 6a74b2c93..c10137010 100644 --- a/StabilityMatrix.Avalonia/Controls/StackCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/StackCard.axaml @@ -25,23 +25,25 @@ HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}"> - + + - - - + + + - - - - - + + + + + - + + From 0d77aa80aae0b4b030f5746d625892d1bcb19c10 Mon Sep 17 00:00:00 2001 From: JT Date: Wed, 9 Aug 2023 17:39:50 -0700 Subject: [PATCH 065/474] added ModelCard & related stuffs --- StabilityMatrix.Avalonia/App.axaml | 1 + StabilityMatrix.Avalonia/App.axaml.cs | 3 ++ .../Controls/ModelCard.axaml | 36 +++++++++++++++++++ .../Controls/ModelCard.axaml.cs | 9 +++++ .../DesignData/DesignData.cs | 2 ++ .../Models/Inference/ModelCardModel.cs | 3 ++ .../InferenceTextToImageViewModel.cs | 2 ++ .../Inference/ModelCardViewModel.cs | 32 +++++++++++++++++ .../Views/InferenceTextToImageView.axaml | 4 --- 9 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Controls/ModelCard.axaml create mode 100644 StabilityMatrix.Avalonia/Controls/ModelCard.axaml.cs create mode 100644 StabilityMatrix.Avalonia/Models/Inference/ModelCardModel.cs create mode 100644 StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml index db5fd66c7..0200734c5 100644 --- a/StabilityMatrix.Avalonia/App.axaml +++ b/StabilityMatrix.Avalonia/App.axaml @@ -35,5 +35,6 @@ + diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 774bfffcc..c5680eb4a 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -263,6 +263,7 @@ internal static void ConfigureDialogViewModels(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Dialog factory services.AddSingleton>(provider => @@ -287,6 +288,7 @@ internal static void ConfigureDialogViewModels(IServiceCollection services) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) .Register(provider.GetRequiredService)); } @@ -312,6 +314,7 @@ internal static void ConfigureViews(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Dialogs services.AddTransient(); diff --git a/StabilityMatrix.Avalonia/Controls/ModelCard.axaml b/StabilityMatrix.Avalonia/Controls/ModelCard.axaml new file mode 100644 index 000000000..b78f76a73 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/ModelCard.axaml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/ModelCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/ModelCard.axaml.cs new file mode 100644 index 000000000..e997a8741 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/ModelCard.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; + +namespace StabilityMatrix.Avalonia.Controls; + +public class ModelCard : TemplatedControl +{ +} \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 58d142040..b010b45f9 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -382,6 +382,8 @@ public static void Initialize() vm.IsSamplerSelectionEnabled = false; vm.IsDenoiseStrengthEnabled = true; }); + + public static ModelCardViewModel ModelCardViewModel => DialogFactory.Get(); public static ImageGalleryCardViewModel ImageGalleryCardViewModel => DialogFactory.Get(vm => diff --git a/StabilityMatrix.Avalonia/Models/Inference/ModelCardModel.cs b/StabilityMatrix.Avalonia/Models/Inference/ModelCardModel.cs new file mode 100644 index 000000000..354c43cd2 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/Inference/ModelCardModel.cs @@ -0,0 +1,3 @@ +namespace StabilityMatrix.Avalonia.Models.Inference; + +public record ModelCardModel(string? SelectedModelName) { } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index a1c17c641..29aeac24d 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -76,6 +76,8 @@ ServiceManager vmFactory StackCardViewModel.AddCards(new LoadableViewModelBase[] { + // Model Card + vmFactory.Get(), // Sampler vmFactory.Get(), // Hires Fix diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs new file mode 100644 index 000000000..e7533ffb3 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Nodes; +using CommunityToolkit.Mvvm.ComponentModel; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Models.Inference; +using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.ViewModels.Inference; + +[View(typeof(ModelCard))] +public partial class ModelCardViewModel : LoadableViewModelBase +{ + [ObservableProperty] private string? selectedModelName; + + public ModelCardViewModel(IInferenceClientManager clientManager) + { + ClientManager = clientManager; + } + + public IInferenceClientManager ClientManager { get; } + + public override void LoadStateFromJsonObject(JsonObject state) + { + var model = DeserializeModel(state); + SelectedModelName = model.SelectedModelName; + } + + public override JsonObject SaveStateToJsonObject() + { + return SerializeModel(new ModelCardModel(SelectedModelName)); + } +} diff --git a/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml b/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml index 74f75dcdf..83d260ed0 100644 --- a/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml +++ b/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml @@ -2,14 +2,10 @@ x:Class="StabilityMatrix.Avalonia.Views.InferenceTextToImageView" xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox" - xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" - xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:vmInference="using:StabilityMatrix.Avalonia.ViewModels.Inference" x:Name="RootControl" d:DataContext="{x:Static mocks:DesignData.InferenceTextToImageViewModel}" From d152de46f0d195ece44fcc0f900036acb2dd0547 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 10 Aug 2023 23:28:25 -0400 Subject: [PATCH 066/474] Reorder number syntax resolution --- .../Assets/ImagePrompt.tmLanguage.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json b/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json index 16cb19f8b..49760be6e 100644 --- a/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json +++ b/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json @@ -100,10 +100,6 @@ } ] }, - "number": { - "match": "(?x) # turn on extended mode\n -? # an optional minus\n (?:\n 0 # a zero\n | # ...or...\n [1-9] # a 1-9 character\n \\d* # followed by zero or more digits\n )\n (?:\n (?:\n \\. # a period\n \\d+ # followed by one or more digits\n )?\n (?:\n [eE] # an e character\n [+-]? # followed by an option +/-\n \\d+ # followed by one or more digits\n )? # make exponent optional\n )? # make decimal portion optional", - "name": "constant.numeric" - }, "separator": { "match": ",\\s*", "name": "punctuation.separator.variable.prompt" @@ -112,6 +108,10 @@ "match": ":", "name": "punctuation.separator.variable.prompt" }, + "number": { + "match": "(?x) # turn on extended mode\n -? # an optional minus\n (?:\n 0 # a zero\n | # ...or...\n [1-9] # a 1-9 character\n \\d* # followed by zero or more digits\n )\n (?:\n (?:\n \\. # a period\n \\d+ # followed by one or more digits\n )?\n (?:\n [eE] # an e character\n [+-]? # followed by an option +/-\n \\d+ # followed by one or more digits\n )? # make exponent optional\n )? # make decimal portion optional", + "name": "constant.numeric" + }, "keyword": { "match": "\\b(?:BREAK|AND)\\b", "name": "keyword.control" From e6a738436e1c7371c2d693562e7bf39035acac4e Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 10 Aug 2023 23:28:36 -0400 Subject: [PATCH 067/474] Create InferenceSettings.cs --- StabilityMatrix.Core/Models/Settings/InferenceSettings.cs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 StabilityMatrix.Core/Models/Settings/InferenceSettings.cs diff --git a/StabilityMatrix.Core/Models/Settings/InferenceSettings.cs b/StabilityMatrix.Core/Models/Settings/InferenceSettings.cs new file mode 100644 index 000000000..9b2c2616a --- /dev/null +++ b/StabilityMatrix.Core/Models/Settings/InferenceSettings.cs @@ -0,0 +1,6 @@ +namespace StabilityMatrix.Core.Models.Settings; + +public class InferenceSettings +{ + +} \ No newline at end of file From b1830e219a7155d3e4651a9bcf4e67d13d883959 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 10 Aug 2023 23:29:01 -0400 Subject: [PATCH 068/474] Add default reflection implementation for LoadableViewModelBase --- .../ViewModels/LoadableViewModelBase.cs | 171 +++++++++++++++++- 1 file changed, 167 insertions(+), 4 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/LoadableViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/LoadableViewModelBase.cs index 2c501715b..cf06789d8 100644 --- a/StabilityMatrix.Avalonia/ViewModels/LoadableViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/LoadableViewModelBase.cs @@ -1,17 +1,180 @@ using System; +using System.Linq; +using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Windows.Input; +using CommunityToolkit.Mvvm.Input; +using NLog; using StabilityMatrix.Avalonia.Models; namespace StabilityMatrix.Avalonia.ViewModels; public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState { - /// - public abstract void LoadStateFromJsonObject(JsonObject state); + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + // ReSharper disable once MemberCanBePrivate.Global + protected static readonly Type[] SerializerIgnoredTypes = + { + typeof(ICommand), + typeof(IRelayCommand) + }; + + // ReSharper disable once MemberCanBePrivate.Global + protected static readonly string[] SerializerIgnoredNames = + { + nameof(HasErrors), + }; + + private static bool ShouldIgnoreProperty(PropertyInfo property) + { + // Check not JsonIgnore + if (property.GetCustomAttributes(typeof(JsonIgnoreAttribute), true).Length > 0) + { + Logger.Trace("Skipping {Property} - has [JsonIgnore]", property.Name); + return true; + } + // Check not excluded type + if (SerializerIgnoredTypes.Contains(property.PropertyType)) + { + Logger.Trace("Skipping {Property} - serializer ignored type {Type}", property.Name, property.PropertyType); + return true; + } + // Check not ignored name + if (SerializerIgnoredNames.Contains(property.Name, StringComparer.Ordinal)) + { + Logger.Trace("Skipping {Property} - serializer ignored name", property.Name); + return true; + } - /// - public abstract JsonObject SaveStateToJsonObject(); + return false; + } + + /// + /// Load the state of this view model from a JSON object. + /// The default implementation is a mirror of . + /// For the following properties on this class, we will try to set from the JSON object: + /// + /// Public + /// Not marked with [JsonIgnore] + /// Not a type within the SerializerIgnoredTypes + /// Not a name within the SerializerIgnoredNames + /// + /// + public virtual void LoadStateFromJsonObject(JsonObject state) + { + // Get all of our properties using reflection + var properties = GetType().GetProperties(); + Logger.Trace("Serializing {Type} with {Count} properties", GetType(), properties.Length); + + foreach (var property in properties) + { + // Check if property is in the JSON object + if (!state.TryGetPropertyValue(property.Name, out var value)) + { + Logger.Trace("Skipping {Property} - not in JSON object", property.Name); + continue; + } + + // Check if we should ignore this property + if (ShouldIgnoreProperty(property)) + { + continue; + } + + // For types that also implement IJsonLoadableState, defer to their load implementation + if (typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) + { + Logger.Trace("Loading {Property} ({Type}) with IJsonLoadableState", property.Name, property.PropertyType); + + // Value must be non-null + if (value is null) + { + throw new InvalidOperationException($"Property {property.Name} is IJsonLoadableState but value to be loaded is null"); + } + + // Check if the current object at this property is null + if (property.GetValue(this) is not IJsonLoadableState propertyValue) + { + // If null, it must have a default constructor + if (property.PropertyType.GetConstructor(Type.EmptyTypes) is not { } constructorInfo) + { + throw new InvalidOperationException($"Property {property.Name} is IJsonLoadableState but current object is null and has no default constructor"); + } + + // Create a new instance and set it + propertyValue = (IJsonLoadableState) constructorInfo.Invoke(null); + property.SetValue(this, propertyValue); + } + + // Load the state from the JSON object + propertyValue.LoadStateFromJsonObject(value.AsObject()); + } + else + { + Logger.Trace("Loading {Property} ({Type})", property.Name, property.PropertyType); + + var propertyValue = value.Deserialize(property.PropertyType); + property.SetValue(this, propertyValue); + } + } + } + + /// + /// Saves the state of this view model to a JSON object. + /// The default implementation uses reflection to + /// save all properties that are: + /// + /// Public + /// Not marked with [JsonIgnore] + /// Not a type within the SerializerIgnoredTypes + /// Not a name within the SerializerIgnoredNames + /// + /// + public virtual JsonObject SaveStateToJsonObject() + { + // Get all of our properties using reflection. + var properties = GetType().GetProperties(); + Logger.Trace("Serializing {Type} with {Count} properties", GetType(), properties.Length); + + // Create a JSON object to store the state. + var state = new JsonObject(); + + // Serialize each property marked with JsonIncludeAttribute. + foreach (var property in properties) + { + if (ShouldIgnoreProperty(property)) + { + continue; + } + + // For types that also implement IJsonLoadableState, defer to their implementation. + if (typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) + { + Logger.Trace("Serializing {Property} ({Type}) with IJsonLoadableState", property.Name, property.PropertyType); + var value = property.GetValue(this); + if (value is not null) + { + var model = (IJsonLoadableState) value; + var modelState = model.SaveStateToJsonObject(); + state.Add(property.Name, modelState); + } + } + else + { + Logger.Trace("Serializing {Property} ({Type})", property.Name, property.PropertyType); + var value = property.GetValue(this); + if (value is not null) + { + state.Add(property.Name, JsonSerializer.SerializeToNode(value)); + } + } + } + + return state; + } /// /// Serialize a model to a JSON object. From 081c9c77ace7c573f101f7e65cd357c93f36b4a7 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 10 Aug 2023 23:29:09 -0400 Subject: [PATCH 069/474] Add LoadableViewModel tests --- .../Avalonia/LoadableViewModelBaseTests.cs | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 StabilityMatrix.Tests/Avalonia/LoadableViewModelBaseTests.cs diff --git a/StabilityMatrix.Tests/Avalonia/LoadableViewModelBaseTests.cs b/StabilityMatrix.Tests/Avalonia/LoadableViewModelBaseTests.cs new file mode 100644 index 000000000..4de24799e --- /dev/null +++ b/StabilityMatrix.Tests/Avalonia/LoadableViewModelBaseTests.cs @@ -0,0 +1,203 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Moq; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.ViewModels; +#pragma warning disable CS0657 // Not a valid attribute location for this declaration + +namespace StabilityMatrix.Tests.Avalonia; + +// Example subclass +public class TestLoadableViewModel : LoadableViewModelBase +{ + [JsonInclude] + public string? Included { get; set; } + + public int Id { get; set; } + + [JsonIgnore] + public int Ignored { get; set; } +} + +public partial class TestLoadableViewModelObservable : LoadableViewModelBase +{ + [ObservableProperty] + [property: JsonIgnore] + private string? title; + + [ObservableProperty] + private int id; + + [RelayCommand] + private void TestCommand() + { + throw new NotImplementedException(); + } +} + +public class TestLoadableViewModelNestedInterface : LoadableViewModelBase +{ + public IJsonLoadableState? NestedState { get; set; } +} + +public class TestLoadableViewModelNested : LoadableViewModelBase +{ + public TestLoadableViewModel? NestedState { get; set; } +} + +[TestClass] +public class LoadableViewModelBaseTests +{ + [TestMethod] + public void TestSaveStateToJsonObject_JsonIgnoreAttribute() + { + var vm = new TestLoadableViewModel + { + Included = "abc", + Id = 123, + Ignored = 456, + }; + + var state = vm.SaveStateToJsonObject(); + + // [JsonInclude] and not marked property should be serialized. + // Ignored property should be ignored. + Assert.AreEqual(2, state.Count); + Assert.AreEqual("abc", state["Included"].Deserialize()); + Assert.AreEqual(123, state["Id"].Deserialize()); + } + + [TestMethod] + public void TestSaveStateToJsonObject_Observable() + { + // Mvvm ObservableProperty should be serialized. + var vm = new TestLoadableViewModelObservable + { + Title = "abc", + Id = 123, + }; + var state = vm.SaveStateToJsonObject(); + + // Title should be ignored since it has [JsonIgnore] + // Command should be ignored from excluded type rules + // Id should be serialized + + Assert.AreEqual(1, state.Count); + Assert.AreEqual(123, state["Id"].Deserialize()); + } + + [TestMethod] + public void TestSaveStateToJsonObject_IJsonLoadableState() + { + // Properties of type IJsonLoadableState should be serialized by calling their + // SaveStateToJsonObject method. + + // Make a mock IJsonLoadableState + var mockState = new Mock(); + + var vm = new TestLoadableViewModelNestedInterface + { + NestedState = mockState.Object + }; + + // Serialize + var state = vm.SaveStateToJsonObject(); + + // Check results + Assert.AreEqual(1, state.Count); + + // Check that SaveStateToJsonObject was called + mockState.Verify(x => x.SaveStateToJsonObject(), Times.Once); + } + + [TestMethod] + public void TestLoadStateFromJsonObject() + { + // Simple round trip save / load + var vm = new TestLoadableViewModel + { + Included = "abc", + Id = 123, + Ignored = 456, + }; + + var state = vm.SaveStateToJsonObject(); + + // Create a new instance and load the state + var vm2 = new TestLoadableViewModel(); + vm2.LoadStateFromJsonObject(state); + + // Check [JsonInclude] and not marked property was loaded + Assert.AreEqual("abc", vm2.Included); + Assert.AreEqual(123, vm2.Id); + // Check ignored property was not loaded + Assert.AreEqual(0, vm2.Ignored); + } + + [TestMethod] + public void TestLoadStateFromJsonObject_Nested_DefaultCtor() + { + // Round trip save / load with nested IJsonLoadableState property + var nested = new TestLoadableViewModel + { + Included = "abc", + Id = 123, + Ignored = 456, + }; + + var vm = new TestLoadableViewModelNested + { + NestedState = nested + }; + + var state = vm.SaveStateToJsonObject(); + + // Create a new instance with null NestedState, rely on default ctor + var vm2 = new TestLoadableViewModelNested(); + vm2.LoadStateFromJsonObject(state); + + // Check nested state was loaded + Assert.IsNotNull(vm2.NestedState); + + var loadedNested = (TestLoadableViewModel) vm2.NestedState; + Assert.AreEqual("abc", loadedNested.Included); + Assert.AreEqual(123, loadedNested.Id); + Assert.AreEqual(0, loadedNested.Ignored); + } + + [TestMethod] + public void TestLoadStateFromJsonObject_Nested_Existing() + { + // Round trip save / load with nested IJsonLoadableState property + var nested = new TestLoadableViewModel + { + Included = "abc", + Id = 123, + Ignored = 456, + }; + + var vm = new TestLoadableViewModelNestedInterface + { + NestedState = nested + }; + + var state = vm.SaveStateToJsonObject(); + + // Create a new instance with existing NestedState + var vm2 = new TestLoadableViewModelNestedInterface + { + NestedState = new TestLoadableViewModel() + }; + vm2.LoadStateFromJsonObject(state); + + // Check nested state was loaded + Assert.IsNotNull(vm2.NestedState); + + var loadedNested = (TestLoadableViewModel) vm2.NestedState; + Assert.AreEqual("abc", loadedNested.Included); + Assert.AreEqual(123, loadedNested.Id); + Assert.AreEqual(0, loadedNested.Ignored); + } +} From 6d49b9197c7d2f7fd2fc15b08827151c18eba5d2 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 10 Aug 2023 23:32:24 -0400 Subject: [PATCH 070/474] Use default json state load for PromptCard --- .../Inference/PromptCardViewModel.cs | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs index f1d703505..3d8b6a3fd 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs @@ -1,33 +1,17 @@ -using System.Text.Json.Nodes; -using AvaloniaEdit.Document; +using AvaloniaEdit.Document; +using CommunityToolkit.Mvvm.ComponentModel; using StabilityMatrix.Avalonia.Controls; -using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(PromptCard))] -public class PromptCardViewModel : LoadableViewModelBase +public partial class PromptCardViewModel : LoadableViewModelBase { public TextDocument PromptDocument { get; } = new(); public TextDocument NegativePromptDocument { get; } = new(); - /// - public override void LoadStateFromJsonObject(JsonObject state) - { - var model = DeserializeModel(state); - - PromptDocument.Text = model.Prompt ?? ""; - NegativePromptDocument.Text = model.NegativePrompt ?? ""; - } - - /// - public override JsonObject SaveStateToJsonObject() - { - return SerializeModel(new PromptCardModel - { - Prompt = PromptDocument.Text, - NegativePrompt = NegativePromptDocument.Text - }); - } + [ObservableProperty] private int editorFontSize = 14; + + [ObservableProperty] private string editorFontFamily = "Consolas"; } From f5637cd1f331b9201769fbdbfa8113843b7059f4 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 11 Aug 2023 01:22:59 -0400 Subject: [PATCH 071/474] Add read-only property skipping in serializer --- .../ViewModels/LoadableViewModelBase.cs | 8 ++++++ .../Avalonia/LoadableViewModelBaseTests.cs | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/StabilityMatrix.Avalonia/ViewModels/LoadableViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/LoadableViewModelBase.cs index cf06789d8..81fc5d1a8 100644 --- a/StabilityMatrix.Avalonia/ViewModels/LoadableViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/LoadableViewModelBase.cs @@ -30,6 +30,12 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState private static bool ShouldIgnoreProperty(PropertyInfo property) { + // Check not read-only + if (property.SetMethod is null) + { + Logger.Trace("Skipping {Property} - read-only", property.Name); + return true; + } // Check not JsonIgnore if (property.GetCustomAttributes(typeof(JsonIgnoreAttribute), true).Length > 0) { @@ -58,6 +64,7 @@ private static bool ShouldIgnoreProperty(PropertyInfo property) /// For the following properties on this class, we will try to set from the JSON object: /// /// Public + /// Not read-only /// Not marked with [JsonIgnore] /// Not a type within the SerializerIgnoredTypes /// Not a name within the SerializerIgnoredNames @@ -128,6 +135,7 @@ public virtual void LoadStateFromJsonObject(JsonObject state) /// save all properties that are: /// /// Public + /// Not read-only /// Not marked with [JsonIgnore] /// Not a type within the SerializerIgnoredTypes /// Not a name within the SerializerIgnoredNames diff --git a/StabilityMatrix.Tests/Avalonia/LoadableViewModelBaseTests.cs b/StabilityMatrix.Tests/Avalonia/LoadableViewModelBaseTests.cs index 4de24799e..83df2b4ce 100644 --- a/StabilityMatrix.Tests/Avalonia/LoadableViewModelBaseTests.cs +++ b/StabilityMatrix.Tests/Avalonia/LoadableViewModelBaseTests.cs @@ -21,6 +21,16 @@ public class TestLoadableViewModel : LoadableViewModelBase public int Ignored { get; set; } } +public class TestLoadableViewModelReadOnly : LoadableViewModelBase +{ + public int ReadOnly { get; } + + public TestLoadableViewModelReadOnly(int readOnly) + { + ReadOnly = readOnly; + } +} + public partial class TestLoadableViewModelObservable : LoadableViewModelBase { [ObservableProperty] @@ -200,4 +210,22 @@ public void TestLoadStateFromJsonObject_Nested_Existing() Assert.AreEqual(123, loadedNested.Id); Assert.AreEqual(0, loadedNested.Ignored); } + + [TestMethod] + public void TestLoadStateFromJsonObject_ReadOnly() + { + var vm = new TestLoadableViewModelReadOnly(456); + + var state = vm.SaveStateToJsonObject(); + + // Check no properties were serialized + Assert.AreEqual(0, state.Count); + + // Create a new instance and load the state + var vm2 = new TestLoadableViewModelReadOnly(123); + vm2.LoadStateFromJsonObject(state); + + // Read only property should have been ignored + Assert.AreEqual(123, vm2.ReadOnly); + } } From 69e20c7394cfa4277285bfab84bb502243e425bc Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 11 Aug 2023 01:23:11 -0400 Subject: [PATCH 072/474] Add BatchSizeCard --- StabilityMatrix.Avalonia/App.axaml | 1 + StabilityMatrix.Avalonia/App.axaml.cs | 3 + .../Controls/BatchSizeCard.axaml | 72 +++++++++++++++++++ .../Controls/BatchSizeCard.axaml.cs | 9 +++ .../Inference/BatchSizeCardViewModel.cs | 13 ++++ 5 files changed, 98 insertions(+) create mode 100644 StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml create mode 100644 StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml.cs create mode 100644 StabilityMatrix.Avalonia/ViewModels/Inference/BatchSizeCardViewModel.cs diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml index 0200734c5..d2c276cd9 100644 --- a/StabilityMatrix.Avalonia/App.axaml +++ b/StabilityMatrix.Avalonia/App.axaml @@ -36,5 +36,6 @@ + diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index c5680eb4a..dc58b4753 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -264,6 +264,7 @@ internal static void ConfigureDialogViewModels(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Dialog factory services.AddSingleton>(provider => @@ -289,6 +290,7 @@ internal static void ConfigureDialogViewModels(IServiceCollection services) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) .Register(provider.GetRequiredService)); } @@ -315,6 +317,7 @@ internal static void ConfigureViews(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Dialogs services.AddTransient(); diff --git a/StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml b/StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml new file mode 100644 index 000000000..b2cf9dc8e --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml @@ -0,0 +1,72 @@ + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml.cs new file mode 100644 index 000000000..ea7d573f8 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml.cs @@ -0,0 +1,9 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; + +namespace StabilityMatrix.Avalonia.Controls; + +public class BatchSizeCard : TemplatedControl +{ +} \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/BatchSizeCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/BatchSizeCardViewModel.cs new file mode 100644 index 000000000..2abc7022b --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/BatchSizeCardViewModel.cs @@ -0,0 +1,13 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.ViewModels.Inference; + +[View(typeof(BatchSizeCard))] +public partial class BatchSizeCardViewModel : LoadableViewModelBase +{ + [ObservableProperty] private int batchSize = 1; + + [ObservableProperty] private int batchCount = 1; +} From ab388cb3c170044fad09369f2156b8ab5ea86082 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 11 Aug 2023 01:23:32 -0400 Subject: [PATCH 073/474] StackCard UI improvements --- .../Controls/PromptCard.axaml | 92 ++++++++++++++----- .../Controls/StackCard.axaml | 39 ++++---- .../Controls/UpscalerCard.axaml | 4 +- 3 files changed, 87 insertions(+), 48 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/PromptCard.axaml b/StabilityMatrix.Avalonia/Controls/PromptCard.axaml index 89c2dfff0..2cde12069 100644 --- a/StabilityMatrix.Avalonia/Controls/PromptCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/PromptCard.axaml @@ -4,6 +4,7 @@ xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit" xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" + xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" x:DataType="vmInference:PromptCardViewModel"> @@ -11,46 +12,91 @@ + + + + - + - - + + + + + + + + + + + + + + + - - + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/StackCard.axaml b/StabilityMatrix.Avalonia/Controls/StackCard.axaml index c10137010..aa32d6cc1 100644 --- a/StabilityMatrix.Avalonia/Controls/StackCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/StackCard.axaml @@ -20,32 +20,25 @@ - - - - + + - - - + + + - - - - - + + + + + - - - - + + diff --git a/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml b/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml index 7fdb1e2d3..5fb7c4154 100644 --- a/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml @@ -24,9 +24,9 @@ + Text="Upscaler" /> Date: Fri, 11 Aug 2023 01:23:53 -0400 Subject: [PATCH 074/474] Add Generation buttons back --- .../Views/InferenceTextToImageView.axaml | 109 +++++++++++++++--- 1 file changed, 93 insertions(+), 16 deletions(-) diff --git a/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml b/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml index 83d260ed0..382c2c7a4 100644 --- a/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml +++ b/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml @@ -4,6 +4,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:vmInference="using:StabilityMatrix.Avalonia.ViewModels.Inference" @@ -25,6 +26,11 @@ + + + @@ -82,9 +88,11 @@ x:Name="ConfigTool" Title="Config" x:DataType="Tool" - Id="ConfigTool"> + Id="ConfigTool" + CanClose="False"> + Opacity="1" + DataContext="{ReflectionBinding ElementName=Dock, Path=DataContext.StackCardViewModel}" /> @@ -100,8 +108,84 @@ x:Name="PromptTool" Title="Prompt" x:DataType="Tool" - Id="PromptTool"> - + Id="PromptTool" + CanClose="False"> + + + + + + + + + + + + + + + + public void OnTabCloseRequested(TabViewTabCloseRequestedEventArgs e) { - if (e.Item is LoadableViewModelBase vm) + if (e.Item is not InferenceTabViewModelBase vm) { - Tabs.Remove(vm); + Logger.Warn("Tab close requested for unknown item {@Item}", e); + return; } + + Logger.Trace("Closing tab {Title}", vm.TabTitle); + Tabs.Remove(vm); } /// @@ -157,18 +169,16 @@ await notificationService.TryAsync( /// /// Menu "Save As" command. /// - [RelayCommand] + [RelayCommand(FlowExceptionsToTaskScheduler = true)] private async Task MenuSaveAs() { var currentTab = SelectedTab; if (currentTab == null) { - Logger.Trace("MenuSaveAs: currentTab is null"); + Logger.Warn("MenuSaveAs: currentTab is null"); return; } - var document = InferenceProjectDocument.FromLoadable(currentTab); - // Prompt for save file dialog var provider = App.StorageProvider; @@ -200,26 +210,42 @@ private async Task MenuSaveAs() Logger.Trace("MenuSaveAs: user cancelled"); return; } + + var document = InferenceProjectDocument.FromLoadable(currentTab); // Save to file - await using var stream = await result.OpenWriteAsync(); - await JsonSerializer.SerializeAsync( - stream, - document, - new JsonSerializerOptions { WriteIndented = true, } - ); - + try + { + await using var stream = await result.OpenWriteAsync(); + await JsonSerializer.SerializeAsync( + stream, + document, + new JsonSerializerOptions { WriteIndented = true } + ); + } + catch (Exception e) + { + notificationService.ShowPersistent( + "Could not save to file", + $"[{e.GetType().Name}] {e.Message}", + NotificationType.Error); + return; + } + notificationService.Show( "Saved", $"Saved project to {result.Name}", NotificationType.Success ); } - + + /*public AsyncRelayCommand MenuOpenProjectCommand => + commandFactory.CreateWithNotificationErrorHandling(MenuOpenProject);*/ + /// /// Menu "Open Project" command. /// - [RelayCommand] + [RelayCommand(FlowExceptionsToTaskScheduler = true)] private async Task MenuOpenProject() { // Prompt for open file dialog @@ -262,7 +288,7 @@ private async Task MenuOpenProject() return; } - LoadableViewModelBase? vm = null; + InferenceTabViewModelBase? vm = null; if (document.ProjectType is InferenceProjectType.TextToImage && document.State is not null) { var textToImage = vmFactory.Get(); From 5d9bd31f5c9b6e6670b864b0be2c5c91373f2788 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 12 Aug 2023 01:15:15 -0400 Subject: [PATCH 092/474] Add DialogHelper.CreateApiExceptionDialog --- StabilityMatrix.Avalonia/DialogHelper.cs | 82 ++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/StabilityMatrix.Avalonia/DialogHelper.cs b/StabilityMatrix.Avalonia/DialogHelper.cs index ebeb1aa7e..c0f53113f 100644 --- a/StabilityMatrix.Avalonia/DialogHelper.cs +++ b/StabilityMatrix.Avalonia/DialogHelper.cs @@ -4,15 +4,23 @@ using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; using Avalonia; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Media; using Avalonia.Threading; +using AvaloniaEdit; +using AvaloniaEdit.TextMate; +using CommunityToolkit.Mvvm.ComponentModel.__Internals; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Markdown.Avalonia; +using Markdown.Avalonia.SyntaxHigh.Extensions; +using Refit; using StabilityMatrix.Avalonia.Controls; +using TextMateSharp.Grammars; namespace StabilityMatrix.Avalonia; @@ -135,6 +143,80 @@ public static BetterContentDialog CreateMarkdownDialog(string markdown, string? }; } + /// + /// Create a dialog for displaying an ApiException + /// + public static BetterContentDialog CreateApiExceptionDialog(ApiException exception, string? title = null) + { + Dispatcher.UIThread.VerifyAccess(); + + // Setup text editor + var textEditor = new TextEditor + { + IsReadOnly = true, + WordWrap = true, + Options = + { + ShowColumnRulers = false, + AllowScrollBelowDocument = false + } + }; + var registryOptions = new RegistryOptions(ThemeName.DarkPlus); + textEditor.InstallTextMate(registryOptions).SetGrammar(registryOptions.GetScopeByLanguageId("json")); + + var mainGrid = new StackPanel + { + Spacing = 8, + Margin = new Thickness(16), + Children = + { + new TextBlock + { + Text = $"{(int) exception.StatusCode} - {exception.ReasonPhrase}", + FontSize = 18, + FontWeight = FontWeight.Medium, + Margin = new Thickness(0,8), + }, + textEditor + } + }; + + var dialog = new BetterContentDialog + { + Title = title, + Content = mainGrid, + CloseButtonText = "Close", + IsPrimaryButtonEnabled = false, + }; + + // Try to deserialize to json element + if (exception.Content != null) + { + try + { + // Deserialize to json element then re-serialize to ensure indentation + var jsonElement = JsonSerializer.Deserialize(exception.Content, new JsonSerializerOptions + { + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip + }); + var formatted = JsonSerializer.Serialize(jsonElement, new JsonSerializerOptions() + { + WriteIndented = true + }); + + textEditor.Document.Text = formatted; + } + catch (JsonException) + { + // Otherwise just add the content as a code block + textEditor.Document.Text = exception.Content; + } + } + + return dialog; + } + /// /// Create a simple title and description task dialog. /// Sets the XamlRoot to the current top level window. From 37a00fda441b32542379f849f5aa6dc91466de02 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 12 Aug 2023 01:15:39 -0400 Subject: [PATCH 093/474] Add PostInterrupt api route --- StabilityMatrix.Core/Api/IComfyApi.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/StabilityMatrix.Core/Api/IComfyApi.cs b/StabilityMatrix.Core/Api/IComfyApi.cs index 48371ff73..2fb26882c 100644 --- a/StabilityMatrix.Core/Api/IComfyApi.cs +++ b/StabilityMatrix.Core/Api/IComfyApi.cs @@ -12,6 +12,9 @@ Task PostPrompt( CancellationToken cancellationToken = default ); + [Post("/interrupt")] + Task PostInterrupt(CancellationToken cancellationToken = default); + [Get("/history/{promptId}")] Task> GetHistory( string promptId, From e5e8706cde176a538dc505a545f1c84569ce4cd8 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 12 Aug 2023 01:16:23 -0400 Subject: [PATCH 094/474] Add ComfyTask, interrupts, improve progress reports --- .../InferenceTextToImageViewModel.cs | 52 +++++++++++++++---- .../Views/InferenceTextToImageView.axaml | 20 ++++--- StabilityMatrix.Core/Inference/ComfyClient.cs | 47 ++++++++++++++--- .../Inference/ComfyProgressUpdateEventArgs.cs | 7 +++ StabilityMatrix.Core/Inference/ComfyTask.cs | 33 ++++++++++++ 5 files changed, 136 insertions(+), 23 deletions(-) create mode 100644 StabilityMatrix.Core/Inference/ComfyProgressUpdateEventArgs.cs create mode 100644 StabilityMatrix.Core/Inference/ComfyTask.cs diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 6e3a74148..09b1c59a8 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -8,17 +8,22 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using AsyncAwaitBestPractices; using Avalonia.Media.Imaging; using AvaloniaEdit.Document; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NLog; +using Refit; using SkiaSharp; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Inference; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; #pragma warning disable CS0657 // Not a valid attribute location for this declaration @@ -107,6 +112,8 @@ ServiceManager vmFactory // Batch Size vmFactory.Get(), }); + + GenerateImageCommand.WithNotificationErrorHandler(notificationService); } private Dictionary GetCurrentPrompt() @@ -236,11 +243,14 @@ private Dictionary GetCurrentPrompt() return prompt; } - private void OnProgressUpdateReceived(object? sender, ComfyWebSocketProgressData args) + private void OnProgressUpdateReceived(object? sender, ComfyProgressUpdateEventArgs args) { OutputProgress.Value = args.Value; - OutputProgress.Maximum = args.Max; + OutputProgress.Maximum = args.Maximum; OutputProgress.IsIndeterminate = false; + + OutputProgress.Text = $"({args.Value} / {args.Maximum})" + + (args.RunningNode != null ? $" {args.RunningNode}" : ""); } private void OnPreviewImageReceived(object? sender, ComfyWebSocketImageData args) @@ -274,21 +284,40 @@ private async Task GenerateImageImpl(CancellationToken cancellationToken = defau var nodes = GetCurrentPrompt(); // Connect progress handler - client.ProgressUpdateReceived += OnProgressUpdateReceived; + // client.ProgressUpdateReceived += OnProgressUpdateReceived; client.PreviewImageReceived += OnPreviewImageReceived; + ComfyTask? promptTask = null; try { - var (response, promptTask) = await client.QueuePromptAsync(nodes, cancellationToken); - Logger.Info(response); + // Register to interrupt if user cancels + cancellationToken.Register(() => + { + Logger.Info("Cancelling prompt"); + client.InterruptPromptAsync(new CancellationTokenSource(5000).Token).SafeFireAndForget(); + }); + + try + { + promptTask = await client.QueuePromptAsync(nodes, cancellationToken); + } + catch (ApiException e) + { + Logger.Warn(e, "Api exception while queuing prompt"); + await DialogHelper.CreateApiExceptionDialog(e, "Api Error").ShowAsync(); + return; + } + + // Register progress handler + promptTask.ProgressUpdate += OnProgressUpdateReceived; // Wait for prompt to finish - await promptTask.WaitAsync(cancellationToken); - Logger.Trace($"Prompt task {response.PromptId} finished"); + await promptTask.Task.WaitAsync(cancellationToken); + Logger.Trace($"Prompt task {promptTask.Id} finished"); // Get output images var outputs = await client.GetImagesForExecutedPromptAsync( - response.PromptId, + promptTask.Id, cancellationToken ); @@ -339,15 +368,18 @@ private async Task GenerateImageImpl(CancellationToken cancellationToken = defau { // Disconnect progress handler OutputProgress.Value = 0; + OutputProgress.Text = ""; ImageGalleryCardViewModel.PreviewImage?.Dispose(); ImageGalleryCardViewModel.PreviewImage = null; ImageGalleryCardViewModel.IsPreviewOverlayEnabled = false; - client.ProgressUpdateReceived -= OnProgressUpdateReceived; + + // client.ProgressUpdateReceived -= OnProgressUpdateReceived; + promptTask?.Dispose(); client.PreviewImageReceived -= OnPreviewImageReceived; } } - [RelayCommand(IncludeCancelCommand = true)] + [RelayCommand(IncludeCancelCommand = true, FlowExceptionsToTaskScheduler = true)] private async Task GenerateImage(CancellationToken cancellationToken = default) { try diff --git a/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml b/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml index 1a54caf11..0e758be44 100644 --- a/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml +++ b/StabilityMatrix.Avalonia/Views/InferenceTextToImageView.axaml @@ -201,13 +201,21 @@ - + Margin="2,1,2,4" + Spacing="4" + VerticalAlignment="Top"> + + + + diff --git a/StabilityMatrix.Core/Inference/ComfyClient.cs b/StabilityMatrix.Core/Inference/ComfyClient.cs index 243ac2b77..48090cb85 100644 --- a/StabilityMatrix.Core/Inference/ComfyClient.cs +++ b/StabilityMatrix.Core/Inference/ComfyClient.cs @@ -31,7 +31,12 @@ public class ComfyClient : InferenceClientBase /// /// Dictionary of ongoing prompt execution tasks /// - public ConcurrentDictionary PromptTasks { get; } = new(); + public ConcurrentDictionary PromptTasks { get; } = new(); + + /// + /// Current running prompt task + /// + private ComfyTask? currentPromptTask; /// /// Event raised when a progress update is received from the server @@ -138,13 +143,23 @@ private void HandleTextMessage(string text) { if (PromptTasks.TryRemove(executingData.PromptId, out var task)) { + task.RunningNode = null; task.SetResult(); + currentPromptTask = null; } else { Logger.Warn($"Could not find task for prompt {executingData.PromptId}, skipping"); } } + // Otherwise set the task's active node to the one received + else + { + if (PromptTasks.TryGetValue(executingData.PromptId, out var task)) + { + task.RunningNode = executingData.Node; + } + } ExecutingUpdateReceived?.Invoke(this, executingData); } @@ -167,7 +182,10 @@ private void HandleTextMessage(string text) Logger.Warn($"Could not parse progress data {json.Data}, skipping"); return; } - + + // Set for the current prompt task + currentPromptTask?.OnProgressUpdate(progressData); + ProgressUpdateReceived?.Invoke(this, progressData); } else @@ -221,7 +239,7 @@ await webSocketClient .ConfigureAwait(false); } - public async Task<(ComfyPromptResponse, Task)> QueuePromptAsync( + public async Task QueuePromptAsync( Dictionary nodes, CancellationToken cancellationToken = default ) @@ -229,11 +247,26 @@ await webSocketClient var request = new ComfyPromptRequest { ClientId = ClientId, Prompt = nodes }; var result = await comfyApi.PostPrompt(request, cancellationToken).ConfigureAwait(false); - // Add task to dictionary - var tcs = new TaskCompletionSource(); - PromptTasks[result.PromptId] = tcs; + // Add task to dictionary and set it as the current task + var task = new ComfyTask(result.PromptId); + PromptTasks[result.PromptId] = task; + currentPromptTask = task; - return (result, tcs.Task); + return task; + } + + public async Task InterruptPromptAsync(CancellationToken cancellationToken = default) + { + await comfyApi.PostInterrupt(cancellationToken).ConfigureAwait(false); + + // Set the current task to null, and remove it from the dictionary + if (currentPromptTask is { } task) + { + PromptTasks.TryRemove(task.Id, out _); + task.TrySetCanceled(cancellationToken); + task.Dispose(); + currentPromptTask = null; + } } public async Task?>> GetImagesForExecutedPromptAsync( diff --git a/StabilityMatrix.Core/Inference/ComfyProgressUpdateEventArgs.cs b/StabilityMatrix.Core/Inference/ComfyProgressUpdateEventArgs.cs new file mode 100644 index 000000000..1eaebbe64 --- /dev/null +++ b/StabilityMatrix.Core/Inference/ComfyProgressUpdateEventArgs.cs @@ -0,0 +1,7 @@ +namespace StabilityMatrix.Core.Inference; + +public readonly record struct ComfyProgressUpdateEventArgs( + int Value, + int Maximum, + string? TaskId, + string? RunningNode); diff --git a/StabilityMatrix.Core/Inference/ComfyTask.cs b/StabilityMatrix.Core/Inference/ComfyTask.cs new file mode 100644 index 000000000..0fbd613eb --- /dev/null +++ b/StabilityMatrix.Core/Inference/ComfyTask.cs @@ -0,0 +1,33 @@ +using System.Reactive; +using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; + +namespace StabilityMatrix.Core.Inference; + +public class ComfyTask : TaskCompletionSource, IDisposable +{ + public string Id { get; set; } + + public string? RunningNode { get; set; } + + public EventHandler? ProgressUpdate; + + public ComfyTask(string id) + { + Id = id; + } + + /// + /// Handler for progress updates + /// + public void OnProgressUpdate(ComfyWebSocketProgressData update) + { + ProgressUpdate?.Invoke(this, new ComfyProgressUpdateEventArgs(update.Value, update.Max, Id, RunningNode)); + } + + /// + public void Dispose() + { + ProgressUpdate = null; + GC.SuppressFinalize(this); + } +} From 7b4ec0dc8ba79c900d5b8e3f298c69cf2e82f6e6 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 12 Aug 2023 01:16:40 -0400 Subject: [PATCH 095/474] Formatting --- StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index c801be541..2e2e485fc 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -418,14 +418,16 @@ private async Task DebugMakeImageGrid() var grid = ImageProcessor.CreateImageGrid(images.ToImmutableArray()); // Show preview - var image = new Image(); using var peekPixels = grid.PeekPixels(); using var data = peekPixels.Encode(SKEncodedImageFormat.Jpeg, 100); await using var stream = data.AsStream(); - image.Source = WriteableBitmap.Decode(stream); - + var image = new Image + { + Source = WriteableBitmap.Decode(stream) + }; + var dialog = new BetterContentDialog { Content = image, From cbb78a4af1d5b42479bc3490a2345e09b68217d9 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 12 Aug 2023 15:25:54 -0400 Subject: [PATCH 096/474] Version bump to v2.2.0-dev.2 --- 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 e61941031..932faa2e7 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -8,7 +8,7 @@ app.manifest true ./Assets/Icon.ico - 2.1.1-dev.1 + 2.2.0-dev.2 $(Version) true From f9abadfd1b488ceb1017cc83ac96f56b98bbda71 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 14 Aug 2023 22:23:12 -0400 Subject: [PATCH 097/474] Add Code completion namespaces --- .../Behaviors/TextEditorCompletionBehavior.cs | 162 ++++++ .../Controls/CodeCompletion/CompletionData.cs | 128 +++++ .../CodeCompletion/CompletionIcons.cs | 69 +++ .../Controls/CodeCompletion/CompletionList.cs | 480 ++++++++++++++++++ .../CodeCompletion/CompletionListBox.cs | 109 ++++ .../CodeCompletion/CompletionListThemes.axaml | 223 ++++++++ .../CodeCompletion/CompletionWindow.axaml | 55 ++ .../CodeCompletion/CompletionWindow.axaml.cs | 258 ++++++++++ .../CodeCompletion/CompletionWindowBase.cs | 421 +++++++++++++++ .../CodeCompletion/ICompletionData.cs | 95 ++++ .../CodeCompletion/PopupWithCustomPosition.cs | 19 + .../Controls/PromptCard.axaml | 18 +- .../DesignData/DesignData.cs | 23 + StabilityMatrix.Avalonia/Models/IconData.cs | 12 + .../Models/TagCompletion/TagCompletionData.cs | 17 + .../Models/TagCompletion/TagType.cs | 38 ++ .../Styles/ThemeColors.axaml | 39 +- .../Styles/ThemeColors.cs | 16 +- .../InferenceTextToImageViewModel.cs | 7 +- 19 files changed, 2178 insertions(+), 11 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs create mode 100644 StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs create mode 100644 StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionIcons.cs create mode 100644 StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs create mode 100644 StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs create mode 100644 StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml create mode 100644 StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml create mode 100644 StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs create mode 100644 StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindowBase.cs create mode 100644 StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs create mode 100644 StabilityMatrix.Avalonia/Controls/CodeCompletion/PopupWithCustomPosition.cs create mode 100644 StabilityMatrix.Avalonia/Models/IconData.cs create mode 100644 StabilityMatrix.Avalonia/Models/TagCompletion/TagCompletionData.cs create mode 100644 StabilityMatrix.Avalonia/Models/TagCompletion/TagType.cs diff --git a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs new file mode 100644 index 000000000..23a0dee39 --- /dev/null +++ b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs @@ -0,0 +1,162 @@ +using System; +using System.Linq; +using Avalonia; +using Avalonia.Input; +using Avalonia.Xaml.Interactivity; +using AvaloniaEdit; +using AvaloniaEdit.Document; +using AvaloniaEdit.Editing; +using NLog; +using StabilityMatrix.Avalonia.Controls.CodeCompletion; +using StabilityMatrix.Avalonia.Models; +using CompletionWindow = StabilityMatrix.Avalonia.Controls.CodeCompletion.CompletionWindow; + +namespace StabilityMatrix.Avalonia.Behaviors; + +public class TextEditorCompletionBehavior : Behavior +{ + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private TextEditor textEditor = null!; + + private CompletionWindow? completionWindow; + + // ReSharper disable once MemberCanBePrivate.Global + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text)); + + public string Text + { + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + protected override void OnAttached() + { + base.OnAttached(); + + if (AssociatedObject is not { } editor) + { + throw new NullReferenceException("AssociatedObject is null"); + } + + textEditor = editor; + textEditor.TextArea.TextEntered += TextArea_TextEntered; + textEditor.TextArea.TextEntering += TextArea_TextEntering; + } + + protected override void OnDetaching() + { + base.OnDetaching(); + + textEditor.TextArea.TextEntered -= TextArea_TextEntered; + textEditor.TextArea.TextEntering -= TextArea_TextEntering; + } + + private CompletionWindow CreateCompletionWindow(TextArea textArea) + { + var window = new CompletionWindow(textArea) + { + WindowManagerAddShadowHint = false, + CloseWhenCaretAtBeginning = true, + CloseAutomatically = true, + IsLightDismissEnabled = true, + CompletionList = + { + IsFiltering = true + } + }; + + var completionList = window.CompletionList; + + completionList.CompletionData.Add(new CompletionData("item1")); + completionList.CompletionData.Add(new CompletionData("item2")); + completionList.CompletionData.Add(new CompletionData("item3")); + + return window; + } + + private void TextArea_TextEntered(object? sender, TextInputEventArgs e) + { + if (e.Text is not { } triggerText) return; + + if (triggerText.All(char.IsLetterOrDigit)) + { + // Create completion window if its not already created + if (completionWindow == null) + { + // Get the segment of the token the caret is currently in + if (GetCaretToken(textEditor) is not { } tokenSegment) + { + Logger.Trace("Token segment not found"); + return; + } + + var token = textEditor.Document.GetText(tokenSegment); + Logger.Trace("Using token {Token} ({@Segment})", token, tokenSegment); + + completionWindow = CreateCompletionWindow(textEditor.TextArea); + completionWindow.StartOffset = tokenSegment.Offset; + completionWindow.EndOffset = tokenSegment.EndOffset; + + completionWindow.CompletionList.SelectItem(token); + + completionWindow.Closed += delegate + { + completionWindow = null; + }; + + completionWindow.Show(); + } + } + } + + private void TextArea_TextEntering(object? sender, TextInputEventArgs e) + { + if (completionWindow is null) return; + + // When completion window is open, parse and update token offsets + if (GetCaretToken(textEditor) is not { } tokenSegment) + { + Logger.Trace("Token segment not found"); + return; + } + + completionWindow.StartOffset = tokenSegment.Offset; + completionWindow.EndOffset = tokenSegment.EndOffset; + + /*if (e.Text?.Length > 0) { + if (!char.IsLetterOrDigit(e.Text[0])) { + // Whenever a non-letter is typed while the completion window is open, + // insert the currently selected element. + completionWindow?.CompletionList.RequestInsertion(e); + } + }*/ + // Do not set e.Handled=true. + // We still want to insert the character that was typed. + } + + /// + /// Gets a segment of the token the caret is currently in. + /// + private static ISegment? GetCaretToken(TextEditor textEditor) + { + var caret = textEditor.CaretOffset; + + // Search for the start and end of a token + // A token is defined as either alphanumeric chars or a space + var start = caret; + while (start > 0 && char.IsLetterOrDigit(textEditor.Document.GetCharAt(start - 1))) + { + start--; + } + + var end = caret; + while (end < textEditor.Document.TextLength && char.IsLetterOrDigit(textEditor.Document.GetCharAt(end))) + { + end++; + } + + return start < end ? new TextSegment { StartOffset = start, EndOffset = end } : null; + } +} diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs new file mode 100644 index 000000000..f0cb3474b --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs @@ -0,0 +1,128 @@ +using System; +using System.Diagnostics; +using Avalonia.Controls; +using Avalonia.Controls.Documents; +using Avalonia.Media; +using AvaloniaEdit.Document; +using AvaloniaEdit.Editing; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Styles; +using StabilityMatrix.Core.Extensions; + +namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; + +/// +/// Provides entries in AvaloniaEdit completion window. +/// +public class CompletionData : ICompletionData +{ + /// + public string Text { get; } + + /// + public string? Description { get; init; } + + /// + public IImage? Image { get; set; } + + /// + public IconData? Icon { get; init; } + + /// + /// Cached instance. + /// + private object? _content; + + /// + public object Content => _content ??= new TextBlock + { + Inlines = CreateInlines() + }; + + /// + /// Get the current inlines + /// + public InlineCollection TextInlines => ((TextBlock) Content).Inlines!; + + /// + public double Priority { get; } + + public CompletionData(string text) + { + Text = text; + } + + /// + /// Create text block inline runs from text. + /// + private InlineCollection CreateInlines() + { + // Create a span for each character in the text. + var chars = Text.ToCharArray(); + var inlines = new InlineCollection(); + + foreach (var c in chars) + { + var run = new Run(c.ToString()); + inlines.Add(run); + } + + return inlines; + } + + /// + public void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs) + { + textArea.Document.Replace(completionSegment, Text); + } + + /// + public void UpdateCharHighlighting(string searchText) + { + if (TextInlines is null) + { + throw new NullReferenceException("TextContent is null"); + } + + Debug.WriteLine($"Updating char highlighting for {Text} with search text {searchText}"); + + var defaultColor = ThemeColors.CompletionForegroundBrush; + var highlightColor = ThemeColors.CompletionSelectionForegroundBrush; + + // Match characters in the text with the search text from the start + foreach (var (i, currentChar) in Text.Enumerate()) + { + var inline = TextInlines[i]; + + // If longer than text, set to default color + if (i >= searchText.Length) + { + inline.Foreground = defaultColor; + continue; + } + + // If char matches, highlight it + if (currentChar == searchText[i]) + { + inline.Foreground = highlightColor; + } + // For mismatch, set to default color + else + { + inline.Foreground = defaultColor; + } + } + } + + /// + public void ResetCharHighlighting() + { + // TODO: handle light theme foreground variant + var defaultColor = ThemeColors.CompletionForegroundBrush; + + foreach (var inline in TextInlines) + { + inline.Foreground = defaultColor; + } + } +} diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionIcons.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionIcons.cs new file mode 100644 index 000000000..8446b929f --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionIcons.cs @@ -0,0 +1,69 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Projektanker.Icons.Avalonia; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Models.TagCompletion; +using StabilityMatrix.Avalonia.Styles; + +namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public static class CompletionIcons +{ + public static readonly IconData General = new() + { + FAIcon = "fa-solid fa-star-of-life", + Foreground = ThemeColors.LightSteelBlue, + }; + + public static readonly IconData Artist = new() + { + FAIcon = "fa-solid fa-palette", + Foreground = ThemeColors.AmericanYellow, + }; + + public static readonly IconData Character = new() + { + FAIcon = "fa-solid fa-user", + Foreground = ThemeColors.LuminousGreen, + }; + + public static readonly IconData Copyright = new() + { + FAIcon = "fa-solid fa-copyright", + Foreground = ThemeColors.DeepMagenta, + }; + + public static readonly IconData Species = new() + { + FAIcon = "fa-solid fa-dragon", + FontSize = 14, + Foreground = ThemeColors.HalloweenOrange, + }; + + public static readonly IconData Invalid = new() + { + FAIcon = "fa-solid fa-question", + Foreground = ThemeColors.CompletionForegroundBrush, + }; + + public static readonly IconData Keyword = new() + { + FAIcon = "fa-solid fa-key", + Foreground = ThemeColors.CompletionForegroundBrush, + }; + + public static IconData? GetIconForTagType(TagType tagType) + { + return tagType switch + { + TagType.General => General, + TagType.Artist => Artist, + TagType.Character => Character, + TagType.Species => Species, + TagType.Invalid => Invalid, + TagType.Copyright => Copyright, + _ => null + }; + } +} diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs new file mode 100644 index 000000000..785ec0419 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs @@ -0,0 +1,480 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml.Templates; +using AvaloniaEdit.Utils; + +namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; + +/// +/// The listbox used inside the CompletionWindow, contains CompletionListBox. +/// +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public class CompletionList : TemplatedControl +{ + public CompletionList() + { + DoubleTapped += OnDoubleTapped; + + CompletionAcceptKeys = new[] { Key.Enter, Key.Tab, }; + } + + /// + /// If true, the CompletionList is filtered to show only matching items. Also enables search by substring. + /// If false, enables the old behavior: no filtering, search by string.StartsWith. + /// + public bool IsFiltering { get; set; } = true; + + /// + /// Dependency property for . + /// + public static readonly StyledProperty EmptyTemplateProperty = + AvaloniaProperty.Register(nameof(EmptyTemplate)); + + /// + /// Content of EmptyTemplate will be shown when CompletionList contains no items. + /// If EmptyTemplate is null, nothing will be shown. + /// + public ControlTemplate EmptyTemplate + { + get => GetValue(EmptyTemplateProperty); + set => SetValue(EmptyTemplateProperty, value); + } + + /// + /// Dependency property for . + /// + public static readonly StyledProperty FooterTextProperty = AvaloniaProperty.Register( + "FooterText", "Press Enter to insert, Tab to replace"); + + /// + /// Gets/Sets the text displayed in the footer of the completion list. + /// + public string? FooterText + { + get => GetValue(FooterTextProperty); + set => SetValue(FooterTextProperty, value); + } + + /// + /// Is raised when the completion list indicates that the user has chosen + /// an entry to be completed. + /// + public event EventHandler? InsertionRequested; + + /// + /// Raises the InsertionRequested event. + /// + public void RequestInsertion(EventArgs e) + { + InsertionRequested?.Invoke(this, e); + } + + private CompletionListBox? _listBox; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _listBox = e.NameScope.Find("PART_ListBox") as CompletionListBox; + if (_listBox is not null) + { + _listBox.ItemsSource = _completionData; + } + } + + /// + /// Gets the list box. + /// + public CompletionListBox? ListBox + { + get + { + if (_listBox == null) + ApplyTemplate(); + return _listBox; + } + } + + /// + /// Gets or sets the array of keys that are supposed to request insertation of the completion + /// + public Key[] CompletionAcceptKeys { get; set; } + + /// + /// Gets the scroll viewer used in this list box. + /// + public ScrollViewer? ScrollViewer => _listBox?.ScrollViewer; + + private readonly ObservableCollection _completionData = new(); + + /// + /// Gets the list to which completion data can be added. + /// + public IList CompletionData => _completionData; + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (!e.Handled) + { + HandleKey(e); + } + } + + /// + /// Handles a key press. Used to let the completion list handle key presses while the + /// focus is still on the text editor. + /// + public void HandleKey(KeyEventArgs e) + { + if (_listBox == null) + return; + + // We have to do some key handling manually, because the default doesn't work with + // our simulated events. + // Also, the default PageUp/PageDown implementation changes the focus, so we avoid it. + switch (e.Key) + { + case Key.Down: + e.Handled = true; + _listBox.SelectIndex(_listBox.SelectedIndex + 1); + break; + case Key.Up: + e.Handled = true; + _listBox.SelectIndex(_listBox.SelectedIndex - 1); + break; + case Key.PageDown: + e.Handled = true; + _listBox.SelectIndex(_listBox.SelectedIndex + _listBox.VisibleItemCount); + break; + case Key.PageUp: + e.Handled = true; + _listBox.SelectIndex(_listBox.SelectedIndex - _listBox.VisibleItemCount); + break; + case Key.Home: + e.Handled = true; + _listBox.SelectIndex(0); + break; + case Key.End: + e.Handled = true; + _listBox.SelectIndex(_listBox.ItemCount - 1); + break; + default: + if (CompletionAcceptKeys.Contains(e.Key) && CurrentList.Count > 0) + { + e.Handled = true; + RequestInsertion(e); + } + + break; + } + } + + protected void OnDoubleTapped(object? sender, RoutedEventArgs e) + { + //TODO TEST + if ( + ((AvaloniaObject?) e.Source) + .VisualAncestorsAndSelf() + .TakeWhile(obj => obj != this) + .Any(obj => obj is ListBoxItem) + ) + { + e.Handled = true; + RequestInsertion(e); + } + } + + /// + /// Gets/Sets the selected item. + /// + /// + /// The setter of this property does not scroll to the selected item. + /// You might want to also call . + /// + public ICompletionData? SelectedItem + { + get => _listBox?.SelectedItem as ICompletionData; + set + { + if (_listBox == null && value != null) + ApplyTemplate(); + if (_listBox != null) // may still be null if ApplyTemplate fails, or if listBox and value both are null + _listBox.SelectedItem = value; + } + } + + /// + /// Scrolls the specified item into view. + /// + public void ScrollIntoView(ICompletionData item) + { + if (_listBox == null) + ApplyTemplate(); + _listBox?.ScrollIntoView(item); + } + + /// + /// Occurs when the SelectedItem property changes. + /// + public event EventHandler SelectionChanged + { + add => AddHandler(SelectingItemsControl.SelectionChangedEvent, value); + remove => RemoveHandler(SelectingItemsControl.SelectionChangedEvent, value); + } + + // SelectItem gets called twice for every typed character (once from FormatLine), this helps execute SelectItem only once + private string _currentText; + + private ObservableCollection? _currentList; + + public List? CurrentList => ListBox?.Items.Cast().ToList(); + + /// + /// Selects the best match, and filter the items if turned on using . + /// + public void SelectItem(string text) + { + if (text == _currentText) + return; + if (_listBox == null) + ApplyTemplate(); + + if (IsFiltering) + { + SelectItemFiltering(text); + } + else + { + SelectItemWithStart(text); + } + _currentText = text; + } + + /// + /// Filters CompletionList items to show only those matching given query, and selects the best match. + /// + private void SelectItemFiltering(string query) + { + // if the user just typed one more character, don't filter all data but just filter what we are already displaying + var listToFilter = + _currentList != null + && !string.IsNullOrEmpty(_currentText) + && !string.IsNullOrEmpty(query) + && query.StartsWith(_currentText, StringComparison.Ordinal) + ? _currentList + : _completionData; + + var matchingItems = + from item in listToFilter + let quality = GetMatchQuality(item.Text, query) + where quality > 0 + select new { Item = item, Quality = quality }; + + // e.g. "DateTimeKind k = (*cc here suggests DateTimeKind*)" + var suggestedItem = + _listBox.SelectedIndex != -1 ? (ICompletionData)_listBox.SelectedItem : null; + + var listBoxItems = new ObservableCollection(); + var bestIndex = -1; + var bestQuality = -1; + double bestPriority = 0; + var i = 0; + foreach (var matchingItem in matchingItems) + { + var priority = + matchingItem.Item == suggestedItem + ? double.PositiveInfinity + : matchingItem.Item.Priority; + var quality = matchingItem.Quality; + if (quality > bestQuality || quality == bestQuality && priority > bestPriority) + { + bestIndex = i; + bestPriority = priority; + bestQuality = quality; + } + // Add to listbox + listBoxItems.Add(matchingItem.Item); + // Update the character highlighting + matchingItem.Item.UpdateCharHighlighting(query); + i++; + } + + _currentList = listBoxItems; + //_listBox.Items = null; Makes no sense? Tooltip disappeared because of this + _listBox.ItemsSource = listBoxItems; + SelectIndexCentered(bestIndex); + } + + /// + /// Selects the item that starts with the specified query. + /// + private void SelectItemWithStart(string query) + { + if (string.IsNullOrEmpty(query)) + return; + + var suggestedIndex = _listBox.SelectedIndex; + + var bestIndex = -1; + var bestQuality = -1; + double bestPriority = 0; + for (var i = 0; i < _completionData.Count; ++i) + { + var quality = GetMatchQuality(_completionData[i].Text, query); + if (quality < 0) + continue; + + var priority = _completionData[i].Priority; + bool useThisItem; + if (bestQuality < quality) + { + useThisItem = true; + } + else + { + if (bestIndex == suggestedIndex) + { + useThisItem = false; + } + else if (i == suggestedIndex) + { + // prefer recommendedItem, regardless of its priority + useThisItem = bestQuality == quality; + } + else + { + useThisItem = bestQuality == quality && bestPriority < priority; + } + } + if (useThisItem) + { + bestIndex = i; + bestPriority = priority; + bestQuality = quality; + } + } + SelectIndexCentered(bestIndex); + } + + private void SelectIndexCentered(int bestIndex) + { + if (bestIndex < 0) + { + _listBox.ClearSelection(); + } + else + { + var firstItem = _listBox.FirstVisibleItem; + if (bestIndex < firstItem || firstItem + _listBox.VisibleItemCount <= bestIndex) + { + // CenterViewOn does nothing as CompletionListBox.ScrollViewer is null + _listBox.CenterViewOn(bestIndex); + _listBox.SelectIndex(bestIndex); + } + else + { + _listBox.SelectIndex(bestIndex); + } + } + } + + private int GetMatchQuality(string itemText, string query) + { + if (itemText == null) + throw new ArgumentNullException(nameof(itemText), "ICompletionData.Text returned null"); + + // Qualities: + // 8 = full match case sensitive + // 7 = full match + // 6 = match start case sensitive + // 5 = match start + // 4 = match CamelCase when length of query is 1 or 2 characters + // 3 = match substring case sensitive + // 2 = match substring + // 1 = match CamelCase + // -1 = no match + if (query == itemText) + return 8; + if (string.Equals(itemText, query, StringComparison.CurrentCultureIgnoreCase)) + return 7; + + if (itemText.StartsWith(query, StringComparison.CurrentCulture)) + return 6; + if (itemText.StartsWith(query, StringComparison.CurrentCultureIgnoreCase)) + return 5; + + bool? camelCaseMatch = null; + if (query.Length <= 2) + { + camelCaseMatch = CamelCaseMatch(itemText, query); + if (camelCaseMatch == true) + return 4; + } + + // search by substring, if filtering (i.e. new behavior) turned on + if (IsFiltering) + { + if (itemText.IndexOf(query, StringComparison.CurrentCulture) >= 0) + return 3; + if (itemText.IndexOf(query, StringComparison.CurrentCultureIgnoreCase) >= 0) + return 2; + } + + if (!camelCaseMatch.HasValue) + camelCaseMatch = CamelCaseMatch(itemText, query); + if (camelCaseMatch == true) + return 1; + + return -1; + } + + private static bool CamelCaseMatch(string text, string query) + { + // We take the first letter of the text regardless of whether or not it's upper case so we match + // against camelCase text as well as PascalCase text ("cct" matches "camelCaseText") + var theFirstLetterOfEachWord = text.AsEnumerable() + .Take(1) + .Concat(text.AsEnumerable().Skip(1).Where(char.IsUpper)); + + var i = 0; + foreach (var letter in theFirstLetterOfEachWord) + { + if (i > query.Length - 1) + return true; // return true here for CamelCase partial match ("CQ" matches "CodeQualityAnalysis") + if (char.ToUpperInvariant(query[i]) != char.ToUpperInvariant(letter)) + return false; + i++; + } + if (i >= query.Length) + return true; + return false; + } +} diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs new file mode 100644 index 000000000..39d68d59e --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs @@ -0,0 +1,109 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using AvaloniaEdit.Utils; + +namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; + +/// +/// The list box used inside the CompletionList. +/// +public class CompletionListBox : ListBox +{ + internal ScrollViewer ScrollViewer; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + ScrollViewer = e.NameScope.Find("PART_ScrollViewer") as ScrollViewer; + } + + /// + /// Gets the number of the first visible item. + /// + public int FirstVisibleItem + { + get + { + if (ScrollViewer == null || ScrollViewer.Extent.Height == 0) + { + return 0; + } + + return (int)(ItemCount * ScrollViewer.Offset.Y / ScrollViewer.Extent.Height); + } + set + { + value = value.CoerceValue(0, ItemCount - VisibleItemCount); + if (ScrollViewer != null) + { + ScrollViewer.Offset = ScrollViewer.Offset.WithY((double)value / ItemCount * ScrollViewer.Extent.Height); + } + } + } + + /// + /// Gets the number of visible items. + /// + public int VisibleItemCount + { + get + { + if (ScrollViewer == null || ScrollViewer.Extent.Height == 0) + { + return 10; + } + return Math.Max( + 3, + (int)Math.Ceiling(ItemCount * ScrollViewer.Viewport.Height + / ScrollViewer.Extent.Height)); + } + } + + /// + /// Removes the selection. + /// + public void ClearSelection() + { + SelectedIndex = -1; + } + + /// + /// Selects the item with the specified index and scrolls it into view. + /// + public void SelectIndex(int index) + { + if (index >= ItemCount) + index = ItemCount - 1; + if (index < 0) + index = 0; + SelectedIndex = index; + ScrollIntoView(SelectedItem); + } + + /// + /// Centers the view on the item with the specified index. + /// + public void CenterViewOn(int index) + { + FirstVisibleItem = index - VisibleItemCount / 2; + } +} diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml new file mode 100644 index 000000000..bf12be876 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml new file mode 100644 index 000000000..32b22eb79 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml @@ -0,0 +1,55 @@ + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs new file mode 100644 index 000000000..e8a315f7b --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs @@ -0,0 +1,258 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Diagnostics; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Media; +using AvaloniaEdit.Document; +using AvaloniaEdit.Editing; + +namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; + +/// +/// The code completion window. +/// +public class CompletionWindow : CompletionWindowBase +{ + private PopupWithCustomPosition _toolTip; + private ContentControl _toolTipContent; + + + /// + /// Gets the completion list used in this completion window. + /// + public CompletionList CompletionList { get; } + + /// + /// Creates a new code completion window. + /// + public CompletionWindow(TextArea textArea) : base(textArea) + { + CompletionList = new CompletionList(); + // keep height automatic + CloseAutomatically = true; + MaxHeight = 225; + // Width = 175; + Width = 350; + Child = CompletionList; + // prevent user from resizing window to 0x0 + MinHeight = 15; + MinWidth = 30; + + _toolTipContent = new ContentControl(); + _toolTipContent.Classes.Add("ToolTip"); + + _toolTip = new PopupWithCustomPosition + { + IsLightDismissEnabled = true, + PlacementTarget = this, + // Placement = PlacementMode.RightEdgeAlignedTop, + Placement = PlacementMode.LeftEdgeAlignedBottom, + Child = _toolTipContent, + }; + + LogicalChildren.Add(_toolTip); + + //_toolTip.Closed += (o, e) => ((Popup)o).Child = null; + + AttachEvents(); + } + + protected override void OnClosed() + { + base.OnClosed(); + + if (_toolTip != null) + { + _toolTip.IsOpen = false; + _toolTip = null; + _toolTipContent = null; + } + } + + #region ToolTip handling + + private void CompletionList_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (_toolTipContent == null) return; + + var item = CompletionList.SelectedItem; + var description = item?.Description; + if (description != null) + { + if (description is string descriptionText) + { + _toolTipContent.Content = new TextBlock + { + Text = descriptionText, + TextWrapping = TextWrapping.Wrap + }; + } + else + { + _toolTipContent.Content = description; + } + + _toolTip.IsOpen = false; //Popup needs to be closed to change position + + // Calculate offset for tooltip + var popupRoot = Host as PopupRoot; + if (CompletionList.CurrentList != null) + { + double yOffset = 0; + var itemContainer = CompletionList.ListBox.ContainerFromItem(item); + if (popupRoot != null && itemContainer != null) + { + var position = itemContainer.TranslatePoint(new Point(0, 0), popupRoot); + if (position.HasValue) + yOffset = position.Value.Y; + } + + _toolTip.Offset = new Point(2, yOffset); + } + + _toolTip.PlacementTarget = popupRoot; + _toolTip.IsOpen = true; + } + else + { + _toolTip.IsOpen = false; + } + } + + #endregion + + private void CompletionList_InsertionRequested(object sender, EventArgs e) + { + Hide(); + // The window must close before Complete() is called. + // If the Complete callback pushes stacked input handlers, we don't want to pop those when the CC window closes. + var item = CompletionList.SelectedItem; + item?.Complete(TextArea, new AnchorSegment(TextArea.Document, StartOffset, EndOffset - StartOffset), e); + } + + private void AttachEvents() + { + CompletionList.InsertionRequested += CompletionList_InsertionRequested; + CompletionList.SelectionChanged += CompletionList_SelectionChanged; + TextArea.Caret.PositionChanged += CaretPositionChanged; + TextArea.PointerWheelChanged += TextArea_MouseWheel; + TextArea.TextInput += TextArea_PreviewTextInput; + } + + /// + protected override void DetachEvents() + { + CompletionList.InsertionRequested -= CompletionList_InsertionRequested; + CompletionList.SelectionChanged -= CompletionList_SelectionChanged; + TextArea.Caret.PositionChanged -= CaretPositionChanged; + TextArea.PointerWheelChanged -= TextArea_MouseWheel; + TextArea.TextInput -= TextArea_PreviewTextInput; + base.DetachEvents(); + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (!e.Handled) + { + CompletionList.HandleKey(e); + } + } + + private void TextArea_PreviewTextInput(object sender, TextInputEventArgs e) + { + e.Handled = RaiseEventPair(this, null, TextInputEvent, + new TextInputEventArgs { Text = e.Text }); + } + + private void TextArea_MouseWheel(object sender, PointerWheelEventArgs e) + { + e.Handled = RaiseEventPair(GetScrollEventTarget(), + null, PointerWheelChangedEvent, e); + } + + private Control GetScrollEventTarget() + { + if (CompletionList == null) + return this; + return CompletionList.ScrollViewer ?? CompletionList.ListBox ?? (Control)CompletionList; + } + + /// + /// Gets/Sets whether the completion window should close automatically. + /// The default value is true. + /// + public bool CloseAutomatically { get; set; } + + /// + protected override bool CloseOnFocusLost => CloseAutomatically; + + /// + /// When this flag is set, code completion closes if the caret moves to the + /// beginning of the allowed range. This is useful in Ctrl+Space and "complete when typing", + /// but not in dot-completion. + /// Has no effect if CloseAutomatically is false. + /// + public bool CloseWhenCaretAtBeginning { get; set; } + + private void CaretPositionChanged(object sender, EventArgs e) + { + Debug.WriteLine($"Caret Position changed: {e}"); + var offset = TextArea.Caret.Offset; + if (offset == StartOffset) + { + if (CloseAutomatically && CloseWhenCaretAtBeginning) + { + Hide(); + } + else + { + CompletionList.SelectItem(string.Empty); + + if (CompletionList.ListBox.ItemCount == 0) IsVisible = false; + else IsVisible = true; + } + return; + } + if (offset < StartOffset || offset > EndOffset) + { + if (CloseAutomatically) + { + Hide(); + } + } + else + { + var document = TextArea.Document; + if (document != null) + { + var newText = document.GetText(StartOffset, offset - StartOffset); + Debug.WriteLine("CaretPositionChanged newText: " + newText); + CompletionList.SelectItem(newText); + + IsVisible = CompletionList.ListBox.ItemCount != 0; + } + } + } +} diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindowBase.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindowBase.cs new file mode 100644 index 000000000..47ef3efed --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindowBase.cs @@ -0,0 +1,421 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Avalonia.Threading; +using Avalonia.VisualTree; +using AvaloniaEdit; +using AvaloniaEdit.Document; +using AvaloniaEdit.Editing; +using AvaloniaEdit.Rendering; + +namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; + +/// +/// Base class for completion windows. Handles positioning the window at the caret. +/// +public class CompletionWindowBase : Popup +{ + static CompletionWindowBase() + { + //BackgroundProperty.OverrideDefaultValue(typeof(CompletionWindowBase), Brushes.White); + } + + protected override Type StyleKeyOverride => typeof(PopupRoot); + + /// + /// Gets the parent TextArea. + /// + public TextArea TextArea { get; } + + private readonly Window _parentWindow; + private TextDocument _document; + + /// + /// Gets/Sets the start of the text range in which the completion window stays open. + /// This text portion is used to determine the text used to select an entry in the completion list by typing. + /// + public int StartOffset { get; set; } + + /// + /// Gets/Sets the end of the text range in which the completion window stays open. + /// This text portion is used to determine the text used to select an entry in the completion list by typing. + /// + public int EndOffset { get; set; } + + /// + /// Gets whether the window was opened above the current line. + /// + protected bool IsUp { get; private set; } + + /// + /// Creates a new CompletionWindowBase. + /// + public CompletionWindowBase(TextArea textArea) : base() + { + TextArea = textArea ?? throw new ArgumentNullException(nameof(textArea)); + _parentWindow = textArea.GetVisualRoot() as Window; + + + AddHandler(PointerReleasedEvent, OnMouseUp, handledEventsToo: true); + + StartOffset = EndOffset = TextArea.Caret.Offset; + + PlacementTarget = TextArea.TextView; + Placement = PlacementMode.AnchorAndGravity; + PlacementAnchor = global::Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor.TopLeft; + PlacementGravity = global::Avalonia.Controls.Primitives.PopupPositioning.PopupGravity.BottomRight; + + //Deactivated += OnDeactivated; //Not needed? + + Closed += (sender, args) => DetachEvents(); + + AttachEvents(); + + Initialize(); + } + + protected virtual void OnClosed() + { + DetachEvents(); + } + + private void Initialize() + { + if (_document != null && StartOffset != TextArea.Caret.Offset) + { + SetPosition(new TextViewPosition(_document.GetLocation(StartOffset))); + } + else + { + SetPosition(TextArea.Caret.Position); + } + } + + public void Show() + { + UpdatePosition(); + + Open(); + Height = double.NaN; + MinHeight = 0; + } + + public void Hide() + { + Close(); + OnClosed(); + } + + #region Event Handlers + + private void AttachEvents() + { + ((ISetLogicalParent)this).SetParent(TextArea.GetVisualRoot() as ILogical); + + _document = TextArea.Document; + if (_document != null) + { + _document.Changing += TextArea_Document_Changing; + } + + // LostKeyboardFocus seems to be more reliable than PreviewLostKeyboardFocus - see SD-1729 + TextArea.LostFocus += TextAreaLostFocus; + TextArea.TextView.ScrollOffsetChanged += TextViewScrollOffsetChanged; + TextArea.DocumentChanged += TextAreaDocumentChanged; + if (_parentWindow != null) + { + _parentWindow.PositionChanged += ParentWindow_LocationChanged; + _parentWindow.Deactivated += ParentWindow_Deactivated; + } + + // close previous completion windows of same type + foreach (var x in TextArea.StackedInputHandlers.OfType()) + { + if (x.Window.GetType() == GetType()) + TextArea.PopStackedInputHandler(x); + } + + _myInputHandler = new InputHandler(this); + TextArea.PushStackedInputHandler(_myInputHandler); + } + + /// + /// Detaches events from the text area. + /// + protected virtual void DetachEvents() + { + ((ISetLogicalParent)this).SetParent(null); + + if (_document != null) + { + _document.Changing -= TextArea_Document_Changing; + } + TextArea.LostFocus -= TextAreaLostFocus; + TextArea.TextView.ScrollOffsetChanged -= TextViewScrollOffsetChanged; + TextArea.DocumentChanged -= TextAreaDocumentChanged; + if (_parentWindow != null) + { + _parentWindow.PositionChanged -= ParentWindow_LocationChanged; + _parentWindow.Deactivated -= ParentWindow_Deactivated; + } + TextArea.PopStackedInputHandler(_myInputHandler); + } + + #region InputHandler + + private InputHandler _myInputHandler; + + /// + /// A dummy input handler (that justs invokes the default input handler). + /// This is used to ensure the completion window closes when any other input handler + /// becomes active. + /// + private sealed class InputHandler : TextAreaStackedInputHandler + { + internal readonly CompletionWindowBase Window; + + public InputHandler(CompletionWindowBase window) + : base(window.TextArea) + { + Debug.Assert(window != null); + Window = window; + } + + public override void Detach() + { + base.Detach(); + Window.Hide(); + } + + public override void OnPreviewKeyDown(KeyEventArgs e) + { + // prevents crash when typing deadchar while CC window is open + if (e.Key == Key.DeadCharProcessed) + return; + e.Handled = RaiseEventPair(Window, null, KeyDownEvent, + new KeyEventArgs { Key = e.Key }); + } + + public override void OnPreviewKeyUp(KeyEventArgs e) + { + if (e.Key == Key.DeadCharProcessed) + return; + e.Handled = RaiseEventPair(Window, null, KeyUpEvent, + new KeyEventArgs { Key = e.Key }); + } + } + #endregion + + private void TextViewScrollOffsetChanged(object sender, EventArgs e) + { + ILogicalScrollable textView = TextArea; + var visibleRect = new Rect(textView.Offset.X, textView.Offset.Y, textView.Viewport.Width, textView.Viewport.Height); + //close completion window when the user scrolls so far that the anchor position is leaving the visible area + if (visibleRect.Contains(_visualLocation) || visibleRect.Contains(_visualLocationTop)) + { + UpdatePosition(); + } + else + { + Hide(); + } + } + + private void TextAreaDocumentChanged(object sender, EventArgs e) + { + Hide(); + } + + private void TextAreaLostFocus(object sender, RoutedEventArgs e) + { + Dispatcher.UIThread.Post(CloseIfFocusLost, DispatcherPriority.Background); + } + + private void ParentWindow_Deactivated(object sender, EventArgs e) + { + Hide(); + } + + private void ParentWindow_LocationChanged(object sender, EventArgs e) + { + UpdatePosition(); + } + + /// + private void OnDeactivated(object sender, EventArgs e) + { + Dispatcher.UIThread.Post(CloseIfFocusLost, DispatcherPriority.Background); + } + + #endregion + + /// + /// Raises a tunnel/bubble event pair for a control. + /// + /// The control for which the event should be raised. + /// The tunneling event. + /// The bubbling event. + /// The event args to use. + /// The value of the event args. + [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate")] + protected static bool RaiseEventPair(Control target, RoutedEvent previewEvent, RoutedEvent @event, RoutedEventArgs args) + { + if (target == null) + throw new ArgumentNullException(nameof(target)); + if (args == null) + throw new ArgumentNullException(nameof(args)); + if (previewEvent != null) + { + args.RoutedEvent = previewEvent; + target.RaiseEvent(args); + } + args.RoutedEvent = @event ?? throw new ArgumentNullException(nameof(@event)); + target.RaiseEvent(args); + return args.Handled; + } + + // Special handler: handledEventsToo + private void OnMouseUp(object sender, PointerReleasedEventArgs e) + { + ActivateParentWindow(); + } + + /// + /// Activates the parent window. + /// + protected virtual void ActivateParentWindow() + { + _parentWindow?.Activate(); + } + + private void CloseIfFocusLost() + { + if (CloseOnFocusLost) + { + Debug.WriteLine("CloseIfFocusLost: this.IsFocues=" + IsFocused + " IsTextAreaFocused=" + IsTextAreaFocused); + if (!IsFocused && !IsTextAreaFocused) + { + Hide(); + } + } + } + + /// + /// Gets whether the completion window should automatically close when the text editor looses focus. + /// + protected virtual bool CloseOnFocusLost => true; + + private bool IsTextAreaFocused + { + get + { + if (_parentWindow != null && !_parentWindow.IsActive) + return false; + return TextArea.IsFocused; + } + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (!e.Handled && e.Key == Key.Escape) + { + e.Handled = true; + Hide(); + } + } + + private Point _visualLocation; + private Point _visualLocationTop; + + /// + /// Positions the completion window at the specified position. + /// + protected void SetPosition(TextViewPosition position) + { + var textView = TextArea.TextView; + + _visualLocation = textView.GetVisualPosition(position, VisualYPosition.LineBottom); + _visualLocationTop = textView.GetVisualPosition(position, VisualYPosition.LineTop); + + UpdatePosition(); + } + + /// + /// Updates the position of the CompletionWindow based on the parent TextView position and the screen working area. + /// It ensures that the CompletionWindow is completely visible on the screen. + /// + protected void UpdatePosition() + { + var textView = TextArea.TextView; + + var position = _visualLocation - textView.ScrollOffset; + + this.HorizontalOffset = position.X; + this.VerticalOffset = position.Y; + } + + // TODO: check if needed + ///// + //protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) + //{ + // base.OnRenderSizeChanged(sizeInfo); + // if (sizeInfo.HeightChanged && IsUp) + // { + // this.Top += sizeInfo.PreviousSize.Height - sizeInfo.NewSize.Height; + // } + //} + + /// + /// Gets/sets whether the completion window should expect text insertion at the start offset, + /// which not go into the completion region, but before it. + /// + /// This property allows only a single insertion, it is reset to false + /// when that insertion has occurred. + public bool ExpectInsertionBeforeStart { get; set; } + + protected virtual void TextArea_Document_Changing(object? sender, DocumentChangeEventArgs e) + { + if (e.Offset + e.RemovalLength == StartOffset && e.RemovalLength > 0) + { + Hide(); // removal immediately in front of completion segment: close the window + // this is necessary when pressing backspace after dot-completion + } + if (e.Offset == StartOffset && e.RemovalLength == 0 && ExpectInsertionBeforeStart) + { + StartOffset = e.GetNewOffset(StartOffset, AnchorMovementType.AfterInsertion); + ExpectInsertionBeforeStart = false; + } + else + { + StartOffset = e.GetNewOffset(StartOffset, AnchorMovementType.BeforeInsertion); + } + EndOffset = e.GetNewOffset(EndOffset, AnchorMovementType.AfterInsertion); + } +} diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs new file mode 100644 index 000000000..04cd7f95c --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs @@ -0,0 +1,95 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using Avalonia.Controls.Documents; +using Avalonia.Media; +using AvaloniaEdit.Document; +using AvaloniaEdit.Editing; +using FluentAvalonia.UI.Controls; +using StabilityMatrix.Avalonia.Models; + +namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; + +/// +/// Describes an entry in the . +/// +/// +/// Note that the CompletionList uses data binding against the properties in this interface. +/// Thus, your implementation of the interface must use public properties; not explicit interface implementation. +/// +public interface ICompletionData +{ + /// + /// Gets the text. This property is used to filter the list of visible elements. + /// + string Text { get; } + + /// + /// Gets the description. + /// + public string? Description { get; } + + /// + /// Gets the image. + /// + IImage? Image { get; } + + /// + /// Gets the icon shown on the left. + /// + IconData? Icon { get; } + + /// + /// The displayed content. This can be the same as 'Text', or a control if + /// you want to display rich content. + /// + object Content { get; } + + /// + /// Gets inline text fragments. + /// + InlineCollection TextInlines { get; } + + /// + /// Gets the priority. This property is used in the selection logic. You can use it to prefer selecting those items + /// which the user is accessing most frequently. + /// + double Priority { get; } + + /// + /// Perform the completion. + /// + /// The text area on which completion is performed. + /// The text segment that was used by the completion window if + /// the user types (segment between CompletionWindow.StartOffset and CompletionWindow.EndOffset). + /// The EventArgs used for the insertion request. + /// These can be TextCompositionEventArgs, KeyEventArgs, MouseEventArgs, depending on how + /// the insertion was triggered. + void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs); + + /// + /// Update the text character highlighting + /// + void UpdateCharHighlighting(string searchText); + + /// + /// Reset the text character highlighting + /// + void ResetCharHighlighting(); +} diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/PopupWithCustomPosition.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/PopupWithCustomPosition.cs new file mode 100644 index 000000000..e26b50db9 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/PopupWithCustomPosition.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls.Primitives; + +namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; + +internal class PopupWithCustomPosition : Popup +{ + public Point Offset + { + get => new(HorizontalOffset, VerticalOffset); + set + { + HorizontalOffset = value.X; + VerticalOffset = value.Y; + + // this.Revalidate(VerticalOffsetProperty); + } + } +} diff --git a/StabilityMatrix.Avalonia/Controls/PromptCard.axaml b/StabilityMatrix.Avalonia/Controls/PromptCard.axaml index 2cde12069..423e2f2c2 100644 --- a/StabilityMatrix.Avalonia/Controls/PromptCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/PromptCard.axaml @@ -4,6 +4,8 @@ xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit" xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" + xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity" + xmlns:behaviors="clr-namespace:StabilityMatrix.Avalonia.Behaviors" xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" x:DataType="vmInference:PromptCardViewModel"> @@ -64,8 +66,12 @@ - + FontFamily="Cascadia Code,Consolas,Menlo,Monospace"> + + + + + @@ -93,8 +99,12 @@ - + FontFamily="Cascadia Code,Consolas,Menlo,Monospace"> + + + + + diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index fffa651de..11a35ca41 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -5,8 +5,11 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Http; +using AvaloniaEdit.Utils; using Microsoft.Extensions.DependencyInjection; +using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels.Dialogs; @@ -430,6 +433,26 @@ public static void Initialize() public static BatchSizeCardViewModel BatchSizeCardViewModel => DialogFactory.Get(); + public static IList SampleCompletionData => new List + { + new TagCompletionData("test1", TagType.General), + new TagCompletionData("test2", TagType.Artist), + new TagCompletionData("test3", TagType.Character), + new TagCompletionData("test4", TagType.Copyright), + new TagCompletionData("test5", TagType.Species), + new TagCompletionData("test_unknown", TagType.Invalid), + }; + + public static CompletionList SampleCompletionList + { + get + { + var list = new CompletionList(); + list.CompletionData.AddRange(SampleCompletionData); + return list; + } + } + public static Indexer Types => new(); public class Indexer diff --git a/StabilityMatrix.Avalonia/Models/IconData.cs b/StabilityMatrix.Avalonia/Models/IconData.cs new file mode 100644 index 000000000..94fc2df46 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/IconData.cs @@ -0,0 +1,12 @@ +using Avalonia.Media; + +namespace StabilityMatrix.Avalonia.Models; + +public record IconData +{ + public string? FAIcon { get; init; } + + public int? FontSize { get; init; } + + public SolidColorBrush? Foreground { get; init; } +} diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/TagCompletionData.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/TagCompletionData.cs new file mode 100644 index 000000000..1c19c91ef --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/TagCompletionData.cs @@ -0,0 +1,17 @@ +using StabilityMatrix.Avalonia.Controls.CodeCompletion; +using StabilityMatrix.Core.Extensions; + +namespace StabilityMatrix.Avalonia.Models.TagCompletion; + +public class TagCompletionData : CompletionData +{ + protected TagType TagType { get; } + + /// + public TagCompletionData(string text, TagType tagType) : base(text) + { + TagType = tagType; + Icon = CompletionIcons.GetIconForTagType(tagType) ?? CompletionIcons.Invalid; + Description = tagType.GetStringValue(); + } +} diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/TagType.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/TagType.cs new file mode 100644 index 000000000..f6f289512 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/TagType.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using StabilityMatrix.Core.Converters.Json; + +namespace StabilityMatrix.Avalonia.Models.TagCompletion; + +[JsonConverter(typeof(DefaultUnknownEnumConverter))] +public enum TagType +{ + Unknown, + Invalid, + General, + Artist, + Copyright, + Character, + Species, + Meta, + Lore +} + +public static class TagTypeExtensions +{ + public static TagType FromE621(int tag) + { + return tag switch + { + -1 => TagType.Invalid, + 0 => TagType.General, + 1 => TagType.Artist, + 3 => TagType.Copyright, + 4 => TagType.Character, + 5 => TagType.Species, + 6 => TagType.Invalid, + 7 => TagType.Meta, + 8 => TagType.Lore, + _ => TagType.Unknown + }; + } +} diff --git a/StabilityMatrix.Avalonia/Styles/ThemeColors.axaml b/StabilityMatrix.Avalonia/Styles/ThemeColors.axaml index ae50d3417..87ba83726 100644 --- a/StabilityMatrix.Avalonia/Styles/ThemeColors.axaml +++ b/StabilityMatrix.Avalonia/Styles/ThemeColors.axaml @@ -1,6 +1,7 @@  - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:sty="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia"> + #333333 #952923 #C2362E @@ -30,4 +31,38 @@ #795548 #9E9E9E #607D8B + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Styles/ThemeColors.cs b/StabilityMatrix.Avalonia/Styles/ThemeColors.cs index f77232d76..511db0e9e 100644 --- a/StabilityMatrix.Avalonia/Styles/ThemeColors.cs +++ b/StabilityMatrix.Avalonia/Styles/ThemeColors.cs @@ -1,4 +1,5 @@ -using Avalonia.Media; +using Avalonia; +using Avalonia.Media; namespace StabilityMatrix.Avalonia.Styles; @@ -7,4 +8,17 @@ public static class ThemeColors public static readonly SolidColorBrush ThemeGreen = SolidColorBrush.Parse("#4caf50"); public static readonly SolidColorBrush ThemeRed = SolidColorBrush.Parse("#f44336"); public static readonly SolidColorBrush ThemeYellow = SolidColorBrush.Parse("#ffeb3b"); + + public static readonly SolidColorBrush AmericanYellow = SolidColorBrush.Parse("#f2ac08"); + public static readonly SolidColorBrush HalloweenOrange = SolidColorBrush.Parse("#ed5D1f"); + public static readonly SolidColorBrush LightSteelBlue = SolidColorBrush.Parse("#b4c7d9"); + public static readonly SolidColorBrush DeepMagenta = SolidColorBrush.Parse("#dd00dd"); + public static readonly SolidColorBrush LuminousGreen = SolidColorBrush.Parse("#00aa00"); + + public static readonly SolidColorBrush CompletionSelectionBackgroundBrush = + SolidColorBrush.Parse("#2E436E"); + public static readonly SolidColorBrush CompletionSelectionForegroundBrush = + SolidColorBrush.Parse("#5389F4"); + public static readonly SolidColorBrush CompletionForegroundBrush = + SolidColorBrush.Parse("#B4B8BF"); } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 09b1c59a8..2b7738119 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -286,7 +286,7 @@ private async Task GenerateImageImpl(CancellationToken cancellationToken = defau // Connect progress handler // client.ProgressUpdateReceived += OnProgressUpdateReceived; client.PreviewImageReceived += OnPreviewImageReceived; - + ComfyTask? promptTask = null; try { @@ -356,12 +356,11 @@ private async Task GenerateImageImpl(CancellationToken cancellationToken = defau await using var fileStream = gridPath.Info.OpenWrite(); await fileStream.WriteAsync(grid.Encode().ToArray(), cancellationToken); - // Insert to start of gallery + // Insert to start of images ImageGalleryCardViewModel.ImageSources.Add(new ImageSource(gridPath)); - // var bitmaps = (await outputImages.SelectAsync(async i => await i.GetBitmapAsync())).ToImmutableArray(); } - // Insert rest of images + // Add rest of images ImageGalleryCardViewModel.ImageSources.AddRange(outputImages); } finally From 4fa844e7dd378c655a4f96e840f1197fc2ce3613 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 14 Aug 2023 22:23:48 -0400 Subject: [PATCH 098/474] Add tag csv entry for tag based autocomplete --- .../Models/TagCompletion/TagCsvEntry.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 StabilityMatrix.Avalonia/Models/TagCompletion/TagCsvEntry.cs diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/TagCsvEntry.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/TagCsvEntry.cs new file mode 100644 index 000000000..065819165 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/TagCsvEntry.cs @@ -0,0 +1,12 @@ +namespace StabilityMatrix.Avalonia.Models.TagCompletion; + +public record TagCsvEntry +{ + public string? Name { get; init; } + + public int? Type { get; init; } + + public int? Count { get; init; } + + public string? Aliases { get; init; } +} From a9f9e460ba70bc731883171894f220858484c321 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 15 Aug 2023 01:03:07 -0400 Subject: [PATCH 099/474] Add Completion styles --- StabilityMatrix.Avalonia/App.axaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml index d2c276cd9..879e63286 100644 --- a/StabilityMatrix.Avalonia/App.axaml +++ b/StabilityMatrix.Avalonia/App.axaml @@ -15,8 +15,12 @@ + 700 + + + @@ -37,5 +41,6 @@ + From 7fc4b7b4eb3457b8cd74b0c0d90134ccd6fb66a5 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 15 Aug 2023 01:04:17 -0400 Subject: [PATCH 100/474] Add CompletionProvider, debug option for loading --- StabilityMatrix.Avalonia/App.axaml.cs | 3 + .../Behaviors/TextEditorCompletionBehavior.cs | 24 ++-- .../Controls/CodeCompletion/CompletionList.cs | 7 +- .../CodeCompletion/CompletionWindow.axaml.cs | 29 ++++- .../Controls/PromptCard.axaml | 6 +- .../DesignData/DesignData.cs | 1 + .../Helpers/TagCsvParser.cs | 70 +++++++++++ .../TagCompletion/CompletionProvider.cs | 119 ++++++++++++++++++ .../TagCompletion/ICompletionProvider.cs | 28 +++++ .../StabilityMatrix.Avalonia.csproj | 4 + .../Inference/PromptCardViewModel.cs | 14 ++- .../ViewModels/SettingsViewModel.cs | 19 ++- .../Views/SettingsPage.axaml | 8 ++ 13 files changed, 302 insertions(+), 30 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Helpers/TagCsvParser.cs create mode 100644 StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs create mode 100644 StabilityMatrix.Avalonia/Models/TagCompletion/ICompletionProvider.cs diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 3274f04ad..22800d440 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -32,9 +32,11 @@ using Refit; using Sentry; using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.DesignData; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels.Dialogs; @@ -365,6 +367,7 @@ private static IServiceCollection ConfigureServices() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Rich presence services.AddSingleton(); diff --git a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs index 23a0dee39..0e8aa7548 100644 --- a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs +++ b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs @@ -9,6 +9,7 @@ using NLog; using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Models.TagCompletion; using CompletionWindow = StabilityMatrix.Avalonia.Controls.CodeCompletion.CompletionWindow; namespace StabilityMatrix.Avalonia.Behaviors; @@ -22,13 +23,13 @@ public class TextEditorCompletionBehavior : Behavior private CompletionWindow? completionWindow; // ReSharper disable once MemberCanBePrivate.Global - public static readonly StyledProperty TextProperty = - AvaloniaProperty.Register(nameof(Text)); + public static readonly StyledProperty CompletionProviderProperty = + AvaloniaProperty.Register(nameof(CompletionProvider)); - public string Text + public ICompletionProvider CompletionProvider { - get => GetValue(TextProperty); - set => SetValue(TextProperty, value); + get => GetValue(CompletionProviderProperty); + set => SetValue(CompletionProviderProperty, value); } protected override void OnAttached() @@ -55,7 +56,7 @@ protected override void OnDetaching() private CompletionWindow CreateCompletionWindow(TextArea textArea) { - var window = new CompletionWindow(textArea) + var window = new CompletionWindow(textArea, CompletionProvider) { WindowManagerAddShadowHint = false, CloseWhenCaretAtBeginning = true, @@ -66,13 +67,6 @@ private CompletionWindow CreateCompletionWindow(TextArea textArea) IsFiltering = true } }; - - var completionList = window.CompletionList; - - completionList.CompletionData.Add(new CompletionData("item1")); - completionList.CompletionData.Add(new CompletionData("item2")); - completionList.CompletionData.Add(new CompletionData("item3")); - return window; } @@ -98,8 +92,8 @@ private void TextArea_TextEntered(object? sender, TextInputEventArgs e) completionWindow = CreateCompletionWindow(textEditor.TextArea); completionWindow.StartOffset = tokenSegment.Offset; completionWindow.EndOffset = tokenSegment.EndOffset; - - completionWindow.CompletionList.SelectItem(token); + + completionWindow.UpdateQuery(token); completionWindow.Closed += delegate { diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs index 785ec0419..c14302dc5 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs @@ -284,14 +284,15 @@ public void SelectItem(string text) private void SelectItemFiltering(string query) { // if the user just typed one more character, don't filter all data but just filter what we are already displaying - var listToFilter = + /*var listToFilter = _currentList != null && !string.IsNullOrEmpty(_currentText) && !string.IsNullOrEmpty(query) && query.StartsWith(_currentText, StringComparison.Ordinal) ? _currentList - : _completionData; - + : _completionData;*/ + var listToFilter = _completionData; + var matchingItems = from item in listToFilter let quality = GetMatchQuality(item.Text, query) diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs index e8a315f7b..20b1391e7 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs @@ -25,6 +25,8 @@ using Avalonia.Media; using AvaloniaEdit.Document; using AvaloniaEdit.Editing; +using AvaloniaEdit.Utils; +using StabilityMatrix.Avalonia.Models.TagCompletion; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; @@ -35,7 +37,8 @@ public class CompletionWindow : CompletionWindowBase { private PopupWithCustomPosition _toolTip; private ContentControl _toolTipContent; - + + private ICompletionProvider completionProvider; /// /// Gets the completion list used in this completion window. @@ -45,9 +48,15 @@ public class CompletionWindow : CompletionWindowBase /// /// Creates a new code completion window. /// - public CompletionWindow(TextArea textArea) : base(textArea) + public CompletionWindow(TextArea textArea, ICompletionProvider completionProvider) : base(textArea) { + this.completionProvider = completionProvider; + CompletionList = new CompletionList(); + + // For using our own UpdateQuery + CompletionList.IsFiltering = false; + // keep height automatic CloseAutomatically = true; MaxHeight = 225; @@ -249,10 +258,22 @@ private void CaretPositionChanged(object sender, EventArgs e) { var newText = document.GetText(StartOffset, offset - StartOffset); Debug.WriteLine("CaretPositionChanged newText: " + newText); - CompletionList.SelectItem(newText); - + // CompletionList.SelectItem(newText); + UpdateQuery(newText); + IsVisible = CompletionList.ListBox.ItemCount != 0; } } } + + /// + /// Update the completion window's current search term. + /// + public void UpdateQuery(string searchTerm) + { + var results = completionProvider.GetCompletions(searchTerm, 30, false); + CompletionList.CompletionData.Clear(); + CompletionList.CompletionData.AddRange(results); + CompletionList.SelectItem(searchTerm); + } } diff --git a/StabilityMatrix.Avalonia/Controls/PromptCard.axaml b/StabilityMatrix.Avalonia/Controls/PromptCard.axaml index 423e2f2c2..348d907a4 100644 --- a/StabilityMatrix.Avalonia/Controls/PromptCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/PromptCard.axaml @@ -68,7 +68,8 @@ Document="{Binding PromptDocument}" FontFamily="Cascadia Code,Consolas,Menlo,Monospace"> - + @@ -101,7 +102,8 @@ Document="{Binding NegativePromptDocument}" FontFamily="Cascadia Code,Consolas,Menlo,Monospace"> - + diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 11a35ca41..783f61107 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -79,6 +79,7 @@ public static void Initialize() services.AddLogging() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); diff --git a/StabilityMatrix.Avalonia/Helpers/TagCsvParser.cs b/StabilityMatrix.Avalonia/Helpers/TagCsvParser.cs new file mode 100644 index 000000000..1afa66e9f --- /dev/null +++ b/StabilityMatrix.Avalonia/Helpers/TagCsvParser.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Data.Common; +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using StabilityMatrix.Avalonia.Models.TagCompletion; +using Sylvan.Data.Csv; +using Sylvan; +using Sylvan.Data; + +namespace StabilityMatrix.Avalonia.Helpers; + +public class TagCsvParser +{ + private readonly Stream stream; + + public TagCsvParser(Stream stream) + { + this.stream = stream; + } + + public async IAsyncEnumerable ParseAsync() + { + var pool = new StringPool(); + var options = new CsvDataReaderOptions + { + StringFactory = pool.GetString, + HasHeaders = false, + }; + + using var textReader = new StreamReader(stream); + await using var dataReader = await CsvDataReader.CreateAsync(textReader, options); + + while (await dataReader.ReadAsync()) + { + var entry = new TagCsvEntry + { + Name = dataReader.GetString(0), + Type = dataReader.GetInt32(1), + Count = dataReader.GetInt32(2), + Aliases = dataReader.GetString(3), + }; + yield return entry; + } + + /*var dataBinderOptions = new DataBinderOptions + { + BindingMode = DataBindingMode.Any + };*/ + /*var results = dataReader.GetRecordsAsync(dataBinderOptions); + return results;*/ + } + + public async Task> GetDictionaryAsync() + { + var dict = new Dictionary(); + + await foreach (var entry in ParseAsync()) + { + if (entry.Name is null || entry.Type is null) + { + continue; + } + + dict.Add(entry.Name, entry); + } + + return dict; + } +} diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs new file mode 100644 index 000000000..e3275a93b --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoComplete.Builders; +using AutoComplete.Clients.IndexSearchers; +using AutoComplete.DataStructure; +using AutoComplete.Domain; +using NLog; +using StabilityMatrix.Avalonia.Controls.CodeCompletion; +using StabilityMatrix.Avalonia.Helpers; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.FileInterfaces; + +namespace StabilityMatrix.Avalonia.Models.TagCompletion; + +public class CompletionProvider : ICompletionProvider +{ + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private readonly Dictionary entries = new(); + + private InMemoryIndexSearcher? searcher; + + public bool IsLoaded => searcher is not null; + + public async Task LoadFromFile(FilePath path, bool recreate = false) + { + // Get Blake3 hash of file + var hash = await FileHash.GetBlake3Async(path); + + Logger.Trace("Loading tags from {Path} with Blake3 hash {Hash}", path, hash); + + // Check for AppData/StabilityMatrix/Temp/Tags//*.bin + var tempTagsDir = GlobalConfig.HomeDir.JoinDir("Temp", "Tags"); + tempTagsDir.Create(); + var hashDir = tempTagsDir.JoinDir(hash); + + var headerFile = hashDir.JoinFile("header.bin"); + var indexFile = hashDir.JoinFile("index.bin"); + var tailFile = hashDir.JoinFile("tail.bin"); + + entries.Clear(); + + // If directory or any file is missing, rebuild the index + if (recreate || !(hashDir.Exists && headerFile.Exists && indexFile.Exists && tailFile.Exists)) + { + Logger.Trace("Creating new index for {Path}", hashDir); + hashDir.Create(); + + await using var headerStream = headerFile.Info.OpenWrite(); + await using var indexStream = indexFile.Info.OpenWrite(); + await using var tailStream = tailFile.Info.OpenWrite(); + + var builder = new IndexBuilder(headerStream, indexStream, tailStream); + + // Parse csv + var csvStream = path.Info.OpenRead(); + var parser = new TagCsvParser(csvStream); + + await foreach (var entry in parser.ParseAsync()) + { + if (string.IsNullOrWhiteSpace(entry.Name)) + { + continue; + } + // Add to index + builder.Add(entry.Name); + // Add to local dictionary + entries.Add(entry.Name, entry); + } + + builder.Build(); + } + + searcher = new InMemoryIndexSearcher(headerFile, indexFile, tailFile); + searcher.Init(); + } + + /// + public IEnumerable GetCompletions(string searchTerm, int itemsCount, bool suggest) + { + if (searcher is null) + { + throw new InvalidOperationException("Index is not loaded"); + } + + var searchOptions = new SearchOptions + { + Term = searchTerm, + MaxItemCount = itemsCount, + SuggestWhenFoundStartsWith = suggest + }; + + var result = searcher.Search(searchOptions); + + // No results + if (result.ResultType == TrieNodeSearchResultType.NotFound) + { + Logger.Trace("No results for {Term}", searchTerm); + return Array.Empty(); + } + + Logger.Trace("Got {Count} results for {Term}", result.Items.Length, searchTerm); + + // Get entry for each result + var completions = new List(); + foreach (var item in result.Items) + { + if (entries.TryGetValue(item, out var entry)) + { + var entryType = TagTypeExtensions.FromE621(entry.Type.GetValueOrDefault(-1)); + completions.Add(new TagCompletionData(entry.Name!, entryType)); + } + } + + return completions; + } +} diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/ICompletionProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/ICompletionProvider.cs new file mode 100644 index 000000000..4ad77f5e1 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/ICompletionProvider.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using StabilityMatrix.Avalonia.Controls.CodeCompletion; +using StabilityMatrix.Core.Models.FileInterfaces; + +namespace StabilityMatrix.Avalonia.Models.TagCompletion; + +public interface ICompletionProvider +{ + /// + /// Whether the completion provider is loaded. + /// + bool IsLoaded { get; } + + /// + /// Load the completion provider from a file. + /// + Task LoadFromFile(FilePath path, bool recreate = false); + + /// + /// Returns a list of completion items for the given text. + /// + public IEnumerable GetCompletions( + string searchTerm, + int itemsCount, + bool suggest + ); +} diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index 932faa2e7..61adfd90f 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -15,6 +15,7 @@ + @@ -48,6 +49,9 @@ + + + diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs index 95f1c13cc..2e3684136 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs @@ -1,21 +1,25 @@ using System.Text.Json.Nodes; using AvaloniaEdit.Document; -using CommunityToolkit.Mvvm.ComponentModel; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models.Inference; +using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(PromptCard))] -public partial class PromptCardViewModel : LoadableViewModelBase +public class PromptCardViewModel : LoadableViewModelBase { + public ICompletionProvider CompletionProvider { get; } + public TextDocument PromptDocument { get; } = new(); public TextDocument NegativePromptDocument { get; } = new(); - [ObservableProperty] private int editorFontSize = 14; - - [ObservableProperty] private string editorFontFamily = "Consolas"; + /// + public PromptCardViewModel(ICompletionProvider completionProvider) + { + CompletionProvider = completionProvider; + } /// public override JsonObject SaveStateToJsonObject() diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index 2e2e485fc..c888dbd44 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -24,6 +24,7 @@ using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.Views; @@ -50,6 +51,7 @@ public partial class SettingsViewModel : PageViewModelBase private readonly IPrerequisiteHelper prerequisiteHelper; private readonly IPyRunner pyRunner; private readonly ServiceManager dialogFactory; + private readonly ICompletionProvider completionProvider; public SharedState SharedState { get; } @@ -93,13 +95,15 @@ public SettingsViewModel( IPrerequisiteHelper prerequisiteHelper, IPyRunner pyRunner, ServiceManager dialogFactory, - SharedState sharedState) + SharedState sharedState, + ICompletionProvider completionProvider) { this.notificationService = notificationService; this.settingsManager = settingsManager; this.prerequisiteHelper = prerequisiteHelper; this.pyRunner = pyRunner; this.dialogFactory = dialogFactory; + this.completionProvider = completionProvider; SharedState = sharedState; @@ -436,6 +440,19 @@ private async Task DebugMakeImageGrid() await dialog.ShowAsync(); } + + [RelayCommand] + private async Task DebugLoadCompletionCsv() + { + var provider = App.StorageProvider; + var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions()); + + if (files.Count == 0) return; + + await completionProvider.LoadFromFile(files[0].TryGetLocalPath()!, true); + + notificationService.Show("Loaded completion file", ""); + } #endregion #region Info Section diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml index f75923af0..63a5545cf 100644 --- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml @@ -252,6 +252,14 @@ + + + Task LoadFromFile(FilePath path, bool recreate = false); + + /// + /// Load the completion provider from a file in the background. + /// + void BackgroundLoadFromFile(FilePath path, bool recreate = false); /// /// Returns a list of completion items for the given text. From 5368371b426c1408f6128a64901b4f5fb471adb1 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 17 Aug 2023 17:25:38 -0400 Subject: [PATCH 113/474] Add Mock completion provider --- .../DesignData/MockCompletionProvider.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 StabilityMatrix.Avalonia/DesignData/MockCompletionProvider.cs diff --git a/StabilityMatrix.Avalonia/DesignData/MockCompletionProvider.cs b/StabilityMatrix.Avalonia/DesignData/MockCompletionProvider.cs new file mode 100644 index 000000000..8dd435a21 --- /dev/null +++ b/StabilityMatrix.Avalonia/DesignData/MockCompletionProvider.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using StabilityMatrix.Avalonia.Controls.CodeCompletion; +using StabilityMatrix.Avalonia.Models.TagCompletion; +using StabilityMatrix.Core.Models.FileInterfaces; + +namespace StabilityMatrix.Avalonia.DesignData; + +public class MockCompletionProvider : ICompletionProvider +{ + /// + public bool IsLoaded => false; + + /// + public Task LoadFromFile(FilePath path, bool recreate = false) + { + return Task.CompletedTask; + } + + /// + public void BackgroundLoadFromFile(FilePath path, bool recreate = false) + { + } + + /// + public IEnumerable GetCompletions(string searchTerm, int itemsCount, bool suggest) + { + return Array.Empty(); + } +} From 88335f87a8c4cb2afb21e91984295f2778c6c463 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 17 Aug 2023 17:25:56 -0400 Subject: [PATCH 114/474] Add SettingsManager Loaded event --- .../Services/ISettingsManager.cs | 8 +++++- .../Services/SettingsManager.cs | 25 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/StabilityMatrix.Core/Services/ISettingsManager.cs b/StabilityMatrix.Core/Services/ISettingsManager.cs index c65da5c68..98192fc18 100644 --- a/StabilityMatrix.Core/Services/ISettingsManager.cs +++ b/StabilityMatrix.Core/Services/ISettingsManager.cs @@ -27,6 +27,11 @@ public interface ISettingsManager /// event EventHandler? SettingsPropertyChanged; + /// + /// Event fired when Settings are loaded from disk + /// + event EventHandler? Loaded; + /// /// Return a SettingsTransaction that can be used to modify Settings /// Saves on Dispose. @@ -65,7 +70,8 @@ void RegisterPropertyChangedHandler( /// Attempts to locate and set the library path /// Return true if found, false otherwise /// - bool TryFindLibrary(); + /// Force reload even if library is already set + bool TryFindLibrary(bool forceReload = false); /// /// Save a new library path to %APPDATA%/StabilityMatrix/library.json diff --git a/StabilityMatrix.Core/Services/SettingsManager.cs b/StabilityMatrix.Core/Services/SettingsManager.cs index a2b7db474..b7afd4aee 100644 --- a/StabilityMatrix.Core/Services/SettingsManager.cs +++ b/StabilityMatrix.Core/Services/SettingsManager.cs @@ -39,8 +39,15 @@ public string LibraryDir } private set { + var isChanged = libraryDir != value; + libraryDir = value; - LibraryDirChanged?.Invoke(this, value); + + // Only invoke if different + if (isChanged) + { + LibraryDirChanged?.Invoke(this, value); + } } } public bool IsLibraryDirSet => !string.IsNullOrWhiteSpace(libraryDir); @@ -53,9 +60,15 @@ private set public Settings Settings { get; private set; } = new(); - public event EventHandler? LibraryDirChanged; + /// + public event EventHandler? LibraryDirChanged; + + /// public event EventHandler? SettingsPropertyChanged; + /// + public event EventHandler? Loaded; + /// public SettingsTransaction BeginTransaction() { @@ -155,7 +168,7 @@ public void RelayPropertyFor( SaveSettingsAsync().SafeFireAndForget(); // Invoke property changed event - SettingsPropertyChanged?.Invoke(this, new RelayPropertyChangedEventArgs(propertyName, true)); + SettingsPropertyChanged?.Invoke(this, new RelayPropertyChangedEventArgs(targetPropertyName, true)); }; } @@ -180,8 +193,10 @@ public void RegisterPropertyChangedHandler( /// Attempts to locate and set the library path /// Return true if found, false otherwise /// - public bool TryFindLibrary() + public bool TryFindLibrary(bool forceReload = false) { + if (IsLibraryDirSet && !forceReload) return true; + // 1. Check portable mode var appDir = Compat.AppCurrentDir; IsPortableMode = File.Exists(Path.Combine(appDir, "Data", ".sm-portable")); @@ -503,6 +518,8 @@ protected virtual void LoadSettings() Settings = JsonSerializer.Deserialize(settingsContent, modifiedDefaultSerializerOptions)!; + + Loaded?.Invoke(this, EventArgs.Empty); } finally { From 537d0ffad3a8b11cc946323ad05b8e94d5e5cd4a Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 17 Aug 2023 17:26:40 -0400 Subject: [PATCH 115/474] Settings config for tag csv source --- .../Controls/CodeCompletion/CompletionList.cs | 6 ++ .../CodeCompletion/CompletionListThemes.axaml | 22 +++--- .../ViewModels/SettingsViewModel.cs | 79 ++++++++++++++++++- .../Views/SettingsPage.axaml | 51 ++++++++++-- .../Models/Settings/InferenceSettings.cs | 6 -- .../Models/Settings/Settings.cs | 8 +- 6 files changed, 145 insertions(+), 27 deletions(-) delete mode 100644 StabilityMatrix.Core/Models/Settings/InferenceSettings.cs diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs index c14302dc5..1f7987199 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs @@ -263,9 +263,14 @@ public event EventHandler SelectionChanged public void SelectItem(string text) { if (text == _currentText) + { return; + } + if (_listBox == null) + { ApplyTemplate(); + } if (IsFiltering) { @@ -275,6 +280,7 @@ public void SelectItem(string text) { SelectItemWithStart(text); } + _currentText = text; } diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml index bf12be876..d91c9aa30 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml @@ -16,6 +16,10 @@ + + - - - + availableTagCompletionCsvs = Array.Empty(); + + [ObservableProperty] + private string? selectedTagCompletionCsv; + // Integrations section [ObservableProperty] private bool isDiscordRichPresenceEnabled; @@ -136,12 +145,26 @@ public SettingsViewModel( vm => vm.IsDiscordRichPresenceEnabled, settings => settings.IsDiscordRichPresenceEnabled); - DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn); settingsManager.RelayPropertyFor(this, vm => vm.SelectedAnimationScale, settings => settings.AnimationScale); + + settingsManager.RelayPropertyFor(this, + vm => vm.SelectedTagCompletionCsv, + settings => settings.TagCompletionCsv); + + DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn); + ImportTagCsvCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn); } - + + /// + public override void OnLoaded() + { + base.OnLoaded(); + + UpdateAvailableTagCompletionCsvs(); + } + partial void OnSelectedThemeChanged(string? value) { // In case design / tests @@ -237,6 +260,58 @@ private async Task CheckPythonVersion() #endregion + #region Inference UI + + private void UpdateAvailableTagCompletionCsvs() + { + var tagsDir = settingsManager.TagsDirectory; + if (!tagsDir.Exists) return; + + var csvFiles = tagsDir.Info.EnumerateFiles("*.csv"); + AvailableTagCompletionCsvs = csvFiles.Select(f => f.Name).ToImmutableArray(); + + // Set selected to current if exists + var settingsCsv = settingsManager.Settings.TagCompletionCsv; + if (settingsCsv is not null && AvailableTagCompletionCsvs.Contains(settingsCsv)) + { + SelectedTagCompletionCsv = settingsCsv; + } + } + + [RelayCommand(FlowExceptionsToTaskScheduler = true)] + private async Task ImportTagCsv() + { + var storage = App.StorageProvider; + var files = await storage.OpenFilePickerAsync(new FilePickerOpenOptions + { + FileTypeFilter = new List + { + new("CSV") + { + Patterns = new[] {"*.csv"}, + } + } + }); + + if (files.Count == 0) return; + + var sourceFile = new FilePath(files[0].TryGetLocalPath()!); + + var tagsDir = settingsManager.TagsDirectory; + tagsDir.Create(); + + // Copy to tags directory + await sourceFile.CopyToAsync(tagsDir.JoinFile(sourceFile.Name)); + + // Update index + UpdateAvailableTagCompletionCsvs(); + + notificationService.Show($"Imported {sourceFile.Name}", + $"The {sourceFile.Name} file has been imported.", NotificationType.Success); + } + + #endregion + #region System /// diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml index 5527bf206..9cf268df3 100644 --- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml @@ -6,6 +6,7 @@ xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" + xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="700" x:DataType="vm:SettingsViewModel" x:CompileBindings="True" @@ -14,7 +15,7 @@ - @@ -104,8 +105,42 @@ - + + + + + + + + + + + + public void UpdateQuery(string searchTerm) { - var results = completionProvider.GetCompletions(searchTerm, 30, false); + var results = completionProvider.GetCompletions(searchTerm, 30, true); CompletionList.CompletionData.Clear(); CompletionList.CompletionData.AddRange(results); - CompletionList.SelectItem(searchTerm); + CompletionList.SelectItem(searchTerm, true); } } diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindowBase.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindowBase.cs index 68a466889..de486c57c 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindowBase.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindowBase.cs @@ -39,6 +39,7 @@ namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; /// Base class for completion windows. Handles positioning the window at the caret. /// [SuppressMessage("ReSharper", "MemberCanBeProtected.Global")] +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class CompletionWindowBase : Popup { protected override Type StyleKeyOverride => typeof(PopupRoot); @@ -377,7 +378,6 @@ protected void UpdatePosition() } // TODO: check if needed - ///// //protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) //{ // base.OnRenderSizeChanged(sizeInfo); diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs index 04cd7f95c..55c43620a 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs @@ -55,12 +55,6 @@ public interface ICompletionData /// IconData? Icon { get; } - /// - /// The displayed content. This can be the same as 'Text', or a control if - /// you want to display rich content. - /// - object Content { get; } - /// /// Gets inline text fragments. /// diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs index fbd235778..d1eef5ab0 100644 --- a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs @@ -100,22 +100,20 @@ public async Task LoadFromFile(FilePath path, bool recreate = false) var headerFile = hashDir.JoinFile("header.bin"); var indexFile = hashDir.JoinFile("index.bin"); - var tailFile = hashDir.JoinFile("tail.bin"); entries.Clear(); var timer = Stopwatch.StartNew(); // If directory or any file is missing, rebuild the index - if (recreate || !(hashDir.Exists && headerFile.Exists && indexFile.Exists && tailFile.Exists)) + if (recreate || !(hashDir.Exists && headerFile.Exists && indexFile.Exists)) { Logger.Debug("Creating new index for {Path}", hashDir); await using var headerStream = headerFile.Info.OpenWrite(); await using var indexStream = indexFile.Info.OpenWrite(); - await using var tailStream = tailFile.Info.OpenWrite(); - var builder = new IndexBuilder(headerStream, indexStream, tailStream); + var builder = new IndexBuilder(headerStream, indexStream); // Parse csv await using var csvStream = path.Info.OpenRead(); @@ -150,7 +148,7 @@ public async Task LoadFromFile(FilePath path, bool recreate = false) } } - searcher = new InMemoryIndexSearcher(headerFile, indexFile, tailFile); + searcher = new InMemoryIndexSearcher(headerFile, indexFile); searcher.Init(); var elapsed = timer.Elapsed; @@ -204,6 +202,8 @@ private IEnumerable GetCompletionsImpl_Index(string searchTerm, throw new InvalidOperationException("Index is not loaded"); } + var timer = Stopwatch.StartNew(); + var searchOptions = new SearchOptions { Term = searchTerm, @@ -240,6 +240,9 @@ private IEnumerable GetCompletionsImpl_Index(string searchTerm, } } + timer.Stop(); + Logger.Trace("Completions for {Term} took {Time:F2}ms", searchTerm, timer.Elapsed.TotalMilliseconds); + return completions; } } diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index 5bec83cf2..854392c4c 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -310,11 +310,15 @@ private async Task ImportTagCsv() tagsDir.Create(); // Copy to tags directory - await sourceFile.CopyToAsync(tagsDir.JoinFile(sourceFile.Name)); + var targetFile = tagsDir.JoinFile(sourceFile.Name); + await sourceFile.CopyToAsync(targetFile); // Update index UpdateAvailableTagCompletionCsvs(); + // Trigger load + completionProvider.BackgroundLoadFromFile(targetFile, true); + notificationService.Show($"Imported {sourceFile.Name}", $"The {sourceFile.Name} file has been imported.", NotificationType.Success); } From ffb7fc451fe00a5e0f1f591ff4739c34a2c49225 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 17 Aug 2023 20:54:38 -0400 Subject: [PATCH 125/474] Fix underscore token parsing --- .../Behaviors/TextEditorCompletionBehavior.cs | 13 ++++++--- .../Controls/CodeCompletion/CompletionList.cs | 5 ++-- .../CodeCompletion/CompletionWindow.axaml.cs | 28 +++++++++++++++++-- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs index c52fab103..01da25b71 100644 --- a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs +++ b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs @@ -123,7 +123,7 @@ private void TextArea_TextEntering(object? sender, TextInputEventArgs e) { if (completionWindow is null) return; - Dispatcher.UIThread.Post(() => + /*Dispatcher.UIThread.Post(() => { // When completion window is open, parse and update token offsets if (GetCaretToken(textEditor) is not { } tokenSegment) @@ -134,7 +134,7 @@ private void TextArea_TextEntering(object? sender, TextInputEventArgs e) completionWindow.StartOffset = tokenSegment.Offset; completionWindow.EndOffset = tokenSegment.EndOffset; - }); + });*/ /*if (e.Text?.Length > 0) { if (!char.IsLetterOrDigit(e.Text[0])) { @@ -147,6 +147,11 @@ private void TextArea_TextEntering(object? sender, TextInputEventArgs e) // We still want to insert the character that was typed. } + private static bool IsCompletionChar(char c) + { + return char.IsLetterOrDigit(c) || c == '_' || c == '-'; + } + /// /// Gets a segment of the token the caret is currently in. /// @@ -157,13 +162,13 @@ private void TextArea_TextEntering(object? sender, TextInputEventArgs e) // Search for the start and end of a token // A token is defined as either alphanumeric chars or a space var start = caret; - while (start > 0 && char.IsLetterOrDigit(textEditor.Document.GetCharAt(start - 1))) + while (start > 0 && IsCompletionChar(textEditor.Document.GetCharAt(start - 1))) { start--; } var end = caret; - while (end < textEditor.Document.TextLength && char.IsLetterOrDigit(textEditor.Document.GetCharAt(end))) + while (end < textEditor.Document.TextLength && IsCompletionChar(textEditor.Document.GetCharAt(end))) { end++; } diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs index 5d09fb210..6b5388177 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs @@ -122,7 +122,7 @@ public CompletionListBox? ListBox } /// - /// Gets or sets the array of keys that are supposed to request insertation of the completion + /// Gets or sets the array of keys that request insertion of the completion /// public Key[] CompletionAcceptKeys { get; set; } @@ -187,7 +187,8 @@ public void HandleKey(KeyEventArgs e) _listBox.SelectIndex(_listBox.ItemCount - 1); break; default: - if (CompletionAcceptKeys.Contains(e.Key) && CurrentList.Count > 0) + // Check insertion keys + if (CompletionAcceptKeys.Contains(e.Key) && CurrentList?.Count > 0) { e.Handled = true; RequestInsertion(e); diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs index ea7bce6ef..057b769e0 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs @@ -43,6 +43,11 @@ public class CompletionWindow : CompletionWindowBase private PopupWithCustomPosition? _toolTip; private ContentControl? _toolTipContent; + /// + /// Max number of items in the completion list. + /// + public int MaxListLength { get; set; } = 40; + /// /// Gets the completion list used in this completion window. /// @@ -254,22 +259,41 @@ private void CaretPositionChanged(object? sender, EventArgs e) Debug.WriteLine("CaretPositionChanged newText: " + newText); // CompletionList.SelectItem(newText); - Dispatcher.UIThread.Post(() => UpdateQuery(newText)); + // UpdateQuery(newText); IsVisible = CompletionList.ListBox!.ItemCount != 0; } } } + private string? lastSearchTerm; + private int lastCompletionLength; + /// /// Update the completion window's current search term. /// public void UpdateQuery(string searchTerm) { - var results = completionProvider.GetCompletions(searchTerm, 30, true); + // Fast path if the search term starts with the last search term + // and the last completion count was less than the max list length + // (such we won't get new results by searching again) + if (lastSearchTerm is not null + && searchTerm.StartsWith(lastSearchTerm) + && lastCompletionLength < MaxListLength) + { + CompletionList.SelectItem(searchTerm); + lastSearchTerm = searchTerm; + return; + } + + var results = completionProvider.GetCompletions(searchTerm, MaxListLength, true); CompletionList.CompletionData.Clear(); CompletionList.CompletionData.AddRange(results); + CompletionList.SelectItem(searchTerm, true); + + lastSearchTerm = searchTerm; + lastCompletionLength = CompletionList.CompletionData.Count; } } From 5d10799408fafc630042fe4260ec7c56a519ec6d Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 18 Aug 2023 00:54:30 -0400 Subject: [PATCH 126/474] Add count to entry as priority --- .../Models/TagCompletion/CompletionProvider.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs index d1eef5ab0..ec356d10d 100644 --- a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs @@ -188,7 +188,10 @@ private IEnumerable GetCompletionsImpl_Fuzzy(string searchTerm, if (entries.TryGetValue(item, out var entry)) { var entryType = TagTypeExtensions.FromE621(entry.Type.GetValueOrDefault(-1)); - completions.Add(new TagCompletionData(entry.Name!, entryType)); + completions.Add(new TagCompletionData(entry.Name!, entryType) + { + Priority = entry.Count ?? 0 + }); } } From 2a9ddc4a8e161e22b778046ecb2ff0edf3f835ba Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 18 Aug 2023 00:59:10 -0400 Subject: [PATCH 127/474] completion performance improvements --- .../Controls/CodeCompletion/CompletionData.cs | 4 +- .../Controls/CodeCompletion/CompletionList.cs | 143 ++++++++++++++++-- .../CodeCompletion/CompletionListThemes.axaml | 8 +- 3 files changed, 134 insertions(+), 21 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs index 424f4b125..1796f4e82 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs @@ -36,7 +36,7 @@ public class CompletionData : ICompletionData public InlineCollection TextInlines => _textInlines ??= CreateInlines(); /// - public double Priority { get; } + public double Priority { get; init; } public CompletionData(string text) { @@ -75,8 +75,6 @@ public void UpdateCharHighlighting(string searchText) throw new NullReferenceException("TextContent is null"); } - Debug.WriteLine($"Updating char highlighting for {Text} with search text {searchText}"); - var defaultColor = ThemeColors.CompletionForegroundBrush; var highlightColor = ThemeColors.CompletionSelectionForegroundBrush; diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs index 6b5388177..2a80cf075 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs @@ -18,9 +18,12 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -28,6 +31,8 @@ using Avalonia.Interactivity; using Avalonia.Markup.Xaml.Templates; using AvaloniaEdit.Utils; +using StabilityMatrix.Avalonia.Models.TagCompletion; +using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; @@ -104,7 +109,8 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _listBox = e.NameScope.Find("PART_ListBox") as CompletionListBox; if (_listBox is not null) { - _listBox.ItemsSource = _completionData; + // _listBox.ItemsSource = _completionData; + _listBox.ItemsSource = FilteredCompletionData; } } @@ -137,6 +143,8 @@ public CompletionListBox? ListBox /// Gets the list to which completion data can be added. /// public IList CompletionData => _completionData; + + public ObservableCollection FilteredCompletionData { get; } = new(); /// protected override void OnKeyDown(KeyEventArgs e) @@ -272,9 +280,12 @@ public void SelectItem(string text, bool fullUpdate = false) { ApplyTemplate(); } - + + var timer = Stopwatch.StartNew(); + if (IsFiltering) { + // SelectItemFilteringLive(text, fullUpdate); SelectItemFiltering(text, fullUpdate); } else @@ -282,14 +293,85 @@ public void SelectItem(string text, bool fullUpdate = false) SelectItemWithStart(text); } + Debug.WriteLine($"SelectItem for '{text}' took {timer.Elapsed.TotalMilliseconds:F2} ms"); + _currentText = text; } + /// + /// Filters CompletionList items to show only those matching given query, and selects the best match. + /// + private void SelectItemFilteringLive(string query, bool fullUpdate = false) + { + if (_listBox is null) throw new NullReferenceException("ListBox not set"); + + var listToFilter = _completionData; + + // if the user just typed one more character, don't filter all data but just filter what we are already displaying + if (!fullUpdate + && FilteredCompletionData.Count > 0 + && !string.IsNullOrEmpty(_currentText) + && !string.IsNullOrEmpty(query) + && query.StartsWith(_currentText, StringComparison.Ordinal)) + { + listToFilter = FilteredCompletionData; + } + + var matchingItems = listToFilter + .Select(item => new {Item = item, Quality = GetMatchQuality(item.Text, query)}) + .Where(x => x.Quality > 0) + .ToImmutableArray(); + + /*var matchingItems = + from item in listToFilter + let quality = GetMatchQuality(item.Text, query) + where quality > 0 + select new { Item = item, Quality = quality };*/ + + // e.g. "DateTimeKind k = (*cc here suggests DateTimeKind*)" + var suggestedItem = + _listBox.SelectedIndex != -1 ? (ICompletionData)_listBox.SelectedItem! : null; + + // Clear current items + FilteredCompletionData.Clear(); + + var bestIndex = -1; + var bestQuality = -1; + double bestPriority = 0; + var i = 0; + foreach (var matchingItem in matchingItems) + { + var priority = + matchingItem.Item == suggestedItem + ? double.PositiveInfinity + : matchingItem.Item.Priority; + var quality = matchingItem.Quality; + if (quality > bestQuality || quality == bestQuality && priority > bestPriority) + { + bestIndex = i; + bestPriority = priority; + bestQuality = quality; + } + + // Add to filtered list + FilteredCompletionData.Add(matchingItem.Item); + + // Update the character highlighting + matchingItem.Item.UpdateCharHighlighting(query); + + i++; + } + + SelectIndex(bestIndex); + } + /// /// Filters CompletionList items to show only those matching given query, and selects the best match. /// private void SelectItemFiltering(string query, bool fullUpdate = false) { + if (_listBox is null) throw new NullReferenceException("ListBox not set"); + var listToFilter = _completionData; // if the user just typed one more character, don't filter all data but just filter what we are already displaying @@ -302,16 +384,24 @@ private void SelectItemFiltering(string query, bool fullUpdate = false) listToFilter = _currentList; } + // Order first by quality, then by + var matchingItems = listToFilter + .Select(item => new { Item = item, Quality = GetMatchQuality(item.Text, query) }) + .Where(x => x.Quality > 0) + .OrderByDescending(x => x.Quality) + .ThenByDescending(x => x.Item.Priority) + .ToImmutableArray(); - var matchingItems = + /*var matchingItems = from item in listToFilter let quality = GetMatchQuality(item.Text, query) where quality > 0 - select new { Item = item, Quality = quality }; + orderby quality + select new { Item = item, Quality = quality };*/ // e.g. "DateTimeKind k = (*cc here suggests DateTimeKind*)" var suggestedItem = - _listBox.SelectedIndex != -1 ? (ICompletionData)_listBox.SelectedItem : null; + _listBox.SelectedIndex != -1 ? (ICompletionData)_listBox.SelectedItem! : null; var listBoxItems = new ObservableCollection(); var bestIndex = -1; @@ -331,17 +421,20 @@ where quality > 0 bestPriority = priority; bestQuality = quality; } + // Add to listbox listBoxItems.Add(matchingItem.Item); + // Update the character highlighting matchingItem.Item.UpdateCharHighlighting(query); + i++; } _currentList = listBoxItems; //_listBox.Items = null; Makes no sense? Tooltip disappeared because of this _listBox.ItemsSource = listBoxItems; - SelectIndexCentered(bestIndex); + SelectIndex(bestIndex); } /// @@ -395,28 +488,50 @@ private void SelectItemWithStart(string query) SelectIndexCentered(bestIndex); } - private void SelectIndexCentered(int bestIndex) + private void SelectIndexCentered(int index) { - if (bestIndex < 0) + if (_listBox is null) + { + throw new NullReferenceException("ListBox not set"); + } + + if (index < 0) { _listBox.ClearSelection(); } else { var firstItem = _listBox.FirstVisibleItem; - if (bestIndex < firstItem || firstItem + _listBox.VisibleItemCount <= bestIndex) + if (index < firstItem || firstItem + _listBox.VisibleItemCount <= index) { // CenterViewOn does nothing as CompletionListBox.ScrollViewer is null - _listBox.CenterViewOn(bestIndex); - _listBox.SelectIndex(bestIndex); + _listBox.CenterViewOn(index); + _listBox.SelectIndex(index); } else { - _listBox.SelectIndex(bestIndex); + _listBox.SelectIndex(index); } } } - + + private void SelectIndex(int index) + { + if (_listBox is null) + { + throw new NullReferenceException("ListBox not set"); + } + + if (index < 0) + { + _listBox.ClearSelection(); + } + else + { + _listBox.SelectedIndex = index; + } + } + private int GetMatchQuality(string itemText, string query) { if (itemText == null) @@ -466,7 +581,7 @@ private int GetMatchQuality(string itemText, string query) return -1; } - + private static bool CamelCaseMatch(string text, string query) { // We take the first letter of the text regardless of whether or not it's upper case so we match diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml index d91c9aa30..6e4a937b3 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListThemes.axaml @@ -162,12 +162,12 @@ - + Source="{Binding Image}" />--> + Inlines="{Binding TextInlines}" + FontSize="13"/> Date: Fri, 18 Aug 2023 03:07:41 -0400 Subject: [PATCH 128/474] Add EditorSelectionBrush --- StabilityMatrix.Avalonia/Styles/ThemeColors.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/StabilityMatrix.Avalonia/Styles/ThemeColors.cs b/StabilityMatrix.Avalonia/Styles/ThemeColors.cs index 511db0e9e..539f77d95 100644 --- a/StabilityMatrix.Avalonia/Styles/ThemeColors.cs +++ b/StabilityMatrix.Avalonia/Styles/ThemeColors.cs @@ -21,4 +21,7 @@ public static class ThemeColors SolidColorBrush.Parse("#5389F4"); public static readonly SolidColorBrush CompletionForegroundBrush = SolidColorBrush.Parse("#B4B8BF"); + + public static readonly SolidColorBrush EditorSelectionBrush = + SolidColorBrush.Parse("#214283"); } From c10efb7d5c3bf03d0d192fa5f0aec1183a5fbf8c Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 18 Aug 2023 03:08:19 -0400 Subject: [PATCH 129/474] Fix SettingsManager property relay names --- StabilityMatrix.Core/Services/SettingsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Core/Services/SettingsManager.cs b/StabilityMatrix.Core/Services/SettingsManager.cs index 4874efb84..b92581f34 100644 --- a/StabilityMatrix.Core/Services/SettingsManager.cs +++ b/StabilityMatrix.Core/Services/SettingsManager.cs @@ -142,7 +142,7 @@ public void RelayPropertyFor( // Update source when settings change SettingsPropertyChanged += (sender, args) => { - if (args.PropertyName != propertyName) return; + if (args.PropertyName != targetPropertyName) return; // Skip if event is relay and the sender is the source, to prevent duplicate if (args.IsRelay && ReferenceEquals(sender, source)) return; From 8c0a84dbb5c3668fb2ce628ffcc7a1057652d4a6 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 18 Aug 2023 03:08:35 -0400 Subject: [PATCH 130/474] Change selection color of prompt card editors --- StabilityMatrix.Avalonia/Controls/PromptCard.axaml.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/StabilityMatrix.Avalonia/Controls/PromptCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/PromptCard.axaml.cs index b0b00f607..0cef232e8 100644 --- a/StabilityMatrix.Avalonia/Controls/PromptCard.axaml.cs +++ b/StabilityMatrix.Avalonia/Controls/PromptCard.axaml.cs @@ -6,6 +6,7 @@ using AvaloniaEdit; using AvaloniaEdit.TextMate; using StabilityMatrix.Avalonia.Extensions; +using StabilityMatrix.Avalonia.Styles; using TextMateSharp.Grammars; using TextMateSharp.Internal.Themes.Reader; using TextMateSharp.Registry; @@ -64,6 +65,7 @@ private void InitializeEditors(TemplateAppliedEventArgs e) editorOptions.EnableHyperlinks = true; editorOptions.RequireControlModifierForHyperlinkClick = true; editor.TextArea.TextView.LinkTextForegroundBrush = Brushes.Coral; + editor.TextArea.SelectionBrush = ThemeColors.EditorSelectionBrush; var installation = editor.InstallTextMate(registryOptions); From 83884a78b4e134a6871958d5b6118c89c2f0d8a9 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 18 Aug 2023 18:57:06 -0400 Subject: [PATCH 131/474] Add TokenizerProvider, Conditional selection tooltip --- StabilityMatrix.Avalonia/App.axaml.cs | 1 + .../Behaviors/TextEditorCompletionBehavior.cs | 11 ++- .../Controls/CodeCompletion/CompletionList.cs | 84 ++++++++++--------- .../CodeCompletion/CompletionWindow.axaml.cs | 27 +++--- .../Controls/PromptCard.axaml | 6 +- .../DesignData/DesignData.cs | 7 +- .../TagCompletion/ITokenizerProvider.cs | 11 +++ .../Models/TagCompletion/TokenizerProvider.cs | 35 ++++++++ .../Inference/PromptCardViewModel.cs | 7 +- 9 files changed, 134 insertions(+), 55 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Models/TagCompletion/ITokenizerProvider.cs create mode 100644 StabilityMatrix.Avalonia/Models/TagCompletion/TokenizerProvider.cs diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 5099218c1..66edfa1b9 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -380,6 +380,7 @@ private static IServiceCollection ConfigureServices() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Rich presence services.AddSingleton(); diff --git a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs index 01da25b71..c8cc5bd1a 100644 --- a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs +++ b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs @@ -34,6 +34,15 @@ public ICompletionProvider CompletionProvider set => SetValue(CompletionProviderProperty, value); } + public static readonly StyledProperty TokenizerProviderProperty = AvaloniaProperty.Register( + "TokenizerProvider"); + + public ITokenizerProvider TokenizerProvider + { + get => GetValue(TokenizerProviderProperty); + set => SetValue(TokenizerProviderProperty, value); + } + public static readonly StyledProperty IsEnabledProperty = AvaloniaProperty.Register( "IsEnabled", true); @@ -67,7 +76,7 @@ protected override void OnDetaching() private CompletionWindow CreateCompletionWindow(TextArea textArea) { - var window = new CompletionWindow(textArea, CompletionProvider) + var window = new CompletionWindow(textArea, CompletionProvider, TokenizerProvider) { WindowManagerAddShadowHint = false, CloseWhenCaretAtBeginning = true, diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs index 2a80cf075..58284fc2e 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs @@ -285,8 +285,8 @@ public void SelectItem(string text, bool fullUpdate = false) if (IsFiltering) { - // SelectItemFilteringLive(text, fullUpdate); - SelectItemFiltering(text, fullUpdate); + SelectItemFilteringLive(text, fullUpdate); + // SelectItemFiltering(text, fullUpdate); } else { @@ -317,52 +317,57 @@ private void SelectItemFilteringLive(string query, bool fullUpdate = false) listToFilter = FilteredCompletionData; } + // Order first by quality, then by priority var matchingItems = listToFilter - .Select(item => new {Item = item, Quality = GetMatchQuality(item.Text, query)}) + .Select(item => new { Item = item, Quality = GetMatchQuality(item.Text, query) }) .Where(x => x.Quality > 0) - .ToImmutableArray(); + .OrderByDescending(x => x.Quality) + .ThenByDescending(x => x.Item.Priority) + .ToList(); - /*var matchingItems = - from item in listToFilter - let quality = GetMatchQuality(item.Text, query) - where quality > 0 - select new { Item = item, Quality = quality };*/ - - // e.g. "DateTimeKind k = (*cc here suggests DateTimeKind*)" var suggestedItem = _listBox.SelectedIndex != -1 ? (ICompletionData)_listBox.SelectedItem! : null; - // Clear current items - FilteredCompletionData.Clear(); - - var bestIndex = -1; - var bestQuality = -1; - double bestPriority = 0; - var i = 0; - foreach (var matchingItem in matchingItems) + // Fast path if both only 1 item + if (FilteredCompletionData.Count == 1 && matchingItems.Count == 1) { - var priority = - matchingItem.Item == suggestedItem - ? double.PositiveInfinity - : matchingItem.Item.Priority; - var quality = matchingItem.Quality; - if (quality > bestQuality || quality == bestQuality && priority > bestPriority) + // Just update the character highlighting + matchingItems[0].Item.UpdateCharHighlighting(query); + } + else + { + // Clear current items + FilteredCompletionData.Clear(); + + var bestIndex = -1; + var bestQuality = -1; + double bestPriority = 0; + var i = 0; + foreach (var matchingItem in matchingItems) { - bestIndex = i; - bestPriority = priority; - bestQuality = quality; - } + var priority = + matchingItem.Item == suggestedItem + ? double.PositiveInfinity + : matchingItem.Item.Priority; + var quality = matchingItem.Quality; + if (quality > bestQuality || quality == bestQuality && priority > bestPriority) + { + bestIndex = i; + bestPriority = priority; + bestQuality = quality; + } - // Add to filtered list - FilteredCompletionData.Add(matchingItem.Item); + // Add to filtered list + FilteredCompletionData.Add(matchingItem.Item); - // Update the character highlighting - matchingItem.Item.UpdateCharHighlighting(query); + // Update the character highlighting + matchingItem.Item.UpdateCharHighlighting(query); - i++; - } + i++; + } - SelectIndex(bestIndex); + SelectIndex(bestIndex); + } } /// @@ -384,7 +389,7 @@ private void SelectItemFiltering(string query, bool fullUpdate = false) listToFilter = _currentList; } - // Order first by quality, then by + // Order first by quality, then by priority var matchingItems = listToFilter .Select(item => new { Item = item, Quality = GetMatchQuality(item.Text, query) }) .Where(x => x.Quality > 0) @@ -398,8 +403,7 @@ from item in listToFilter where quality > 0 orderby quality select new { Item = item, Quality = quality };*/ - - // e.g. "DateTimeKind k = (*cc here suggests DateTimeKind*)" + var suggestedItem = _listBox.SelectedIndex != -1 ? (ICompletionData)_listBox.SelectedItem! : null; @@ -521,6 +525,8 @@ private void SelectIndex(int index) { throw new NullReferenceException("ListBox not set"); } + + if (index == _listBox.SelectedIndex) return; if (index < 0) { diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs index 057b769e0..5128238e4 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs @@ -39,6 +39,7 @@ namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; public class CompletionWindow : CompletionWindowBase { private readonly ICompletionProvider completionProvider; + private readonly ITokenizerProvider tokenizerProvider; private PopupWithCustomPosition? _toolTip; private ContentControl? _toolTipContent; @@ -47,28 +48,30 @@ public class CompletionWindow : CompletionWindowBase /// Max number of items in the completion list. /// public int MaxListLength { get; set; } = 40; - + /// /// Gets the completion list used in this completion window. /// - public CompletionList CompletionList { get; } + public CompletionList CompletionList { get; } = new(); + + /// + /// Whether selection tooltips are shown. + /// + public bool IsSelectionTooltipEnabled { get; set; } /// /// Creates a new code completion window. /// - public CompletionWindow(TextArea textArea, ICompletionProvider completionProvider) : base(textArea) + public CompletionWindow( + TextArea textArea, + ICompletionProvider completionProvider, + ITokenizerProvider tokenizerProvider) : base(textArea) { this.completionProvider = completionProvider; + this.tokenizerProvider = tokenizerProvider; - CompletionList = new CompletionList - { - IsFiltering = true - }; - - // keep height automatic CloseAutomatically = true; MaxHeight = 225; - // Width = 175; Width = 350; Child = CompletionList; // prevent user from resizing window to 0x0 @@ -83,7 +86,6 @@ public CompletionWindow(TextArea textArea, ICompletionProvider completionProvide IsLightDismissEnabled = true, PlacementTarget = this, Placement = PlacementMode.RightEdgeAlignedTop, - // Placement = PlacementMode.LeftEdgeAlignedBottom, Child = _toolTipContent, }; @@ -110,6 +112,9 @@ protected override void OnClosed() private void CompletionList_SelectionChanged(object? sender, SelectionChangedEventArgs e) { + // Skip if tooltip not enabled + if (!IsSelectionTooltipEnabled) return; + if (_toolTipContent == null || _toolTip == null) return; var item = CompletionList.SelectedItem; diff --git a/StabilityMatrix.Avalonia/Controls/PromptCard.axaml b/StabilityMatrix.Avalonia/Controls/PromptCard.axaml index 865c9d66b..6a87dc2ad 100644 --- a/StabilityMatrix.Avalonia/Controls/PromptCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/PromptCard.axaml @@ -70,7 +70,8 @@ + CompletionProvider="{Binding CompletionProvider}" + TokenizerProvider="{Binding TokenizerProvider}"/> @@ -105,7 +106,8 @@ + CompletionProvider="{Binding CompletionProvider}" + TokenizerProvider="{Binding TokenizerProvider}"/> diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index deb47d37c..714e23583 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -88,6 +88,7 @@ public static void Initialize() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); @@ -474,8 +475,12 @@ public static CompletionList SampleCompletionList { get { - var list = new CompletionList(); + var list = new CompletionList + { + IsFiltering = true + }; list.CompletionData.AddRange(SampleCompletionData); + list.SelectItem("te", true); return list; } } diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/ITokenizerProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/ITokenizerProvider.cs new file mode 100644 index 000000000..b29fbcace --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/ITokenizerProvider.cs @@ -0,0 +1,11 @@ +using TextMateSharp.Grammars; + +namespace StabilityMatrix.Avalonia.Models.TagCompletion; + +public interface ITokenizerProvider +{ + /// + /// Returns a for the given line. + /// + ITokenizeLineResult TokenizeLine(string lineText); +} diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/TokenizerProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/TokenizerProvider.cs new file mode 100644 index 000000000..3a9588786 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/TokenizerProvider.cs @@ -0,0 +1,35 @@ +using System.Diagnostics.CodeAnalysis; +using StabilityMatrix.Avalonia.Extensions; +using TextMateSharp.Grammars; +using TextMateSharp.Registry; + +namespace StabilityMatrix.Avalonia.Models.TagCompletion; + +public class TokenizerProvider : ITokenizerProvider +{ + private readonly Registry registry = new(new RegistryOptions(ThemeName.DarkPlus)); + private IGrammar grammar; + + public TokenizerProvider() + { + SetPromptGrammar(); + } + + /// + public ITokenizeLineResult TokenizeLine(string lineText) + { + return grammar.TokenizeLine(lineText); + } + + [MemberNotNull(nameof(grammar))] + public void SetPromptGrammar() + { + using var stream = Assets.ImagePromptLanguageJson.Open(); + grammar = registry.LoadGrammarFromStream(stream); + } + + public void SetGrammar(string scopeName) + { + grammar = registry.LoadGrammar(scopeName); + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs index c0b5a8cca..8136b1394 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs @@ -14,6 +14,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference; public partial class PromptCardViewModel : LoadableViewModelBase { public ICompletionProvider CompletionProvider { get; } + public ITokenizerProvider TokenizerProvider { get; } public TextDocument PromptDocument { get; } = new(); public TextDocument NegativePromptDocument { get; } = new(); @@ -22,9 +23,13 @@ public partial class PromptCardViewModel : LoadableViewModelBase private bool isAutoCompletionEnabled; /// - public PromptCardViewModel(ICompletionProvider completionProvider, ISettingsManager settingsManager) + public PromptCardViewModel( + ICompletionProvider completionProvider, + ITokenizerProvider tokenizerProvider, + ISettingsManager settingsManager) { CompletionProvider = completionProvider; + TokenizerProvider = tokenizerProvider; settingsManager.RelayPropertyFor(this, vm => vm.IsAutoCompletionEnabled, From c143c506ef24b727be7cf2b7600fa6b167dd18d6 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 18 Aug 2023 20:27:36 -0400 Subject: [PATCH 132/474] Add CodeTimer --- StabilityMatrix.Core/Helper/CodeTimer.cs | 105 +++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 StabilityMatrix.Core/Helper/CodeTimer.cs diff --git a/StabilityMatrix.Core/Helper/CodeTimer.cs b/StabilityMatrix.Core/Helper/CodeTimer.cs new file mode 100644 index 000000000..8f67da65e --- /dev/null +++ b/StabilityMatrix.Core/Helper/CodeTimer.cs @@ -0,0 +1,105 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; + +namespace StabilityMatrix.Core.Helper; + +public class CodeTimer : IDisposable +{ + private static readonly Stack RunningTimers = new(); + + private readonly string name; + private readonly Stopwatch stopwatch; + + private CodeTimer? ParentTimer { get; } + private List SubTimers { get; } = new(); + + public CodeTimer([CallerMemberName] string? name = null) + { + this.name = name ?? ""; + stopwatch = Stopwatch.StartNew(); + + // Set parent as the top of the stack + if (RunningTimers.TryPeek(out var timer)) + { + ParentTimer = timer; + timer.SubTimers.Add(this); + } + + // Add ourselves to the stack + RunningTimers.Push(this); + } + + /// + /// Formats a TimeSpan into a string. Chooses the most appropriate unit of time. + /// + private static string FormatTime(TimeSpan duration) + { + if (duration.TotalSeconds < 1) + { + return $"{duration.TotalMilliseconds:0.00}ms"; + } + + if (duration.TotalMinutes < 1) + { + return $"{duration.TotalSeconds:0.00}s"; + } + + if (duration.TotalHours < 1) + { + return $"{duration.TotalMinutes:0.00}m"; + } + + return $"{duration.TotalHours:0.00}h"; + } + + private static void OutputDebug(string message) + { + Debug.WriteLine(message); + } + + /// + /// Get results for this timer and all sub timers recursively + /// + private string GetResult() + { + var builder = new StringBuilder(); + + builder.AppendLine($"{name}: took {FormatTime(stopwatch.Elapsed)}"); + + foreach (var timer in SubTimers) + { + // For each sub timer layer, add a `|-` prefix + builder.AppendLine($"|- {timer.GetResult()}"); + } + + return builder.ToString(); + } + + public void Dispose() + { + stopwatch.Stop(); + + // Remove ourselves from the stack + if (RunningTimers.TryPop(out var timer)) + { + if (timer != this) + { + throw new InvalidOperationException("Timer stack is corrupted"); + } + } + else + { + throw new InvalidOperationException("Timer stack is empty"); + } + + // If we're a root timer, output all results + if (ParentTimer is null) + { + OutputDebug(GetResult()); + SubTimers.Clear(); + } + + GC.SuppressFinalize(this); + } +} From af99250445a3e60e51a21c8ed244020b2bc9f0a5 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 18 Aug 2023 20:29:09 -0400 Subject: [PATCH 133/474] Add new completion sorting, timers --- .../Controls/CodeCompletion/CompletionList.cs | 86 ++++++++----------- 1 file changed, 37 insertions(+), 49 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs index 58284fc2e..03f52ec52 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs @@ -33,6 +33,7 @@ using AvaloniaEdit.Utils; using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Core.Extensions; +using StabilityMatrix.Core.Helper; namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; @@ -276,35 +277,46 @@ public void SelectItem(string text, bool fullUpdate = false) return; } + using var _ = new CodeTimer(); + if (_listBox == null) { ApplyTemplate(); } - var timer = Stopwatch.StartNew(); - if (IsFiltering) { SelectItemFilteringLive(text, fullUpdate); - // SelectItemFiltering(text, fullUpdate); } else { SelectItemWithStart(text); } - Debug.WriteLine($"SelectItem for '{text}' took {timer.Elapsed.TotalMilliseconds:F2} ms"); - _currentText = text; } + private IReadOnlyList FilterItems(IEnumerable items, string query) + { + using var _ = new CodeTimer(); + + // Order first by quality, then by priority + var matchingItems = items + .Select(item => new { Item = item, Quality = GetMatchQuality(item.Text, query) }) + .Where(x => x.Quality > 0) + .OrderByDescending(x => x.Quality) + .ThenByDescending(x => x.Item.Priority) + .Select(x => x.Item) + .ToList(); + + return matchingItems; + } + /// /// Filters CompletionList items to show only those matching given query, and selects the best match. /// private void SelectItemFilteringLive(string query, bool fullUpdate = false) { - if (_listBox is null) throw new NullReferenceException("ListBox not set"); - var listToFilter = _completionData; // if the user just typed one more character, don't filter all data but just filter what we are already displaying @@ -316,57 +328,33 @@ private void SelectItemFilteringLive(string query, bool fullUpdate = false) { listToFilter = FilteredCompletionData; } - - // Order first by quality, then by priority - var matchingItems = listToFilter - .Select(item => new { Item = item, Quality = GetMatchQuality(item.Text, query) }) - .Where(x => x.Quality > 0) - .OrderByDescending(x => x.Quality) - .ThenByDescending(x => x.Item.Priority) - .ToList(); - var suggestedItem = - _listBox.SelectedIndex != -1 ? (ICompletionData)_listBox.SelectedItem! : null; + var matchingItems = FilterItems(listToFilter, query); - // Fast path if both only 1 item - if (FilteredCompletionData.Count == 1 && matchingItems.Count == 1) + // Fast path if both only 1 item, and item is the same + if (FilteredCompletionData.Count == 1 + && matchingItems.Count == 1 + && FilteredCompletionData[0] == matchingItems[0]) { // Just update the character highlighting - matchingItems[0].Item.UpdateCharHighlighting(query); + matchingItems[0].UpdateCharHighlighting(query); } else { - // Clear current items + // Clear current items and set new ones FilteredCompletionData.Clear(); - - var bestIndex = -1; - var bestQuality = -1; - double bestPriority = 0; - var i = 0; - foreach (var matchingItem in matchingItems) + + foreach (var item in matchingItems) { - var priority = - matchingItem.Item == suggestedItem - ? double.PositiveInfinity - : matchingItem.Item.Priority; - var quality = matchingItem.Quality; - if (quality > bestQuality || quality == bestQuality && priority > bestPriority) - { - bestIndex = i; - bestPriority = priority; - bestQuality = quality; - } - - // Add to filtered list - FilteredCompletionData.Add(matchingItem.Item); - - // Update the character highlighting - matchingItem.Item.UpdateCharHighlighting(query); + item.UpdateCharHighlighting(query); + FilteredCompletionData.Add(item); + } - i++; + // Set index to 0 if not already + if (_listBox != null && _listBox.SelectedIndex != 0) + { + _listBox.SelectedIndex = 0; } - - SelectIndex(bestIndex); } } @@ -574,9 +562,9 @@ private int GetMatchQuality(string itemText, string query) // search by substring, if filtering (i.e. new behavior) turned on if (IsFiltering) { - if (itemText.IndexOf(query, StringComparison.CurrentCulture) >= 0) + if (itemText.Contains(query, StringComparison.CurrentCulture)) return 3; - if (itemText.IndexOf(query, StringComparison.CurrentCultureIgnoreCase) >= 0) + if (itemText.Contains(query, StringComparison.CurrentCultureIgnoreCase)) return 2; } From 7d4a068e7e412cb651b95d9ac37e89e10658393e Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 19 Aug 2023 02:37:01 -0400 Subject: [PATCH 134/474] Insertion request event with append --- .../Behaviors/TextEditorCompletionBehavior.cs | 111 +++++++++++++----- .../Controls/CodeCompletion/CompletionList.cs | 72 ++++++++++-- .../CodeCompletion/CompletionWindow.axaml.cs | 22 +++- .../InsertionRequestEventArgs.cs | 12 ++ 4 files changed, 175 insertions(+), 42 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Controls/CodeCompletion/InsertionRequestEventArgs.cs diff --git a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs index c8cc5bd1a..b4e2483b8 100644 --- a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs +++ b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs @@ -12,6 +12,8 @@ using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.TagCompletion; +using StabilityMatrix.Core.Extensions; +using TextMateSharp.Grammars; using CompletionWindow = StabilityMatrix.Avalonia.Controls.CodeCompletion.CompletionWindow; namespace StabilityMatrix.Avalonia.Behaviors; @@ -94,38 +96,49 @@ private void TextArea_TextEntered(object? sender, TextInputEventArgs e) { if (!IsEnabled || e.Text is not { } triggerText) return; - if (triggerText.All(char.IsLetterOrDigit)) + if (triggerText.All(IsCompletionChar)) { // Create completion window if its not already created if (completionWindow == null) { - Dispatcher.UIThread.Post(() => + // Get the segment of the token the caret is currently in + if (GetCaretCompletionToken() is not { } tokenSegment) { - // Get the segment of the token the caret is currently in - if (GetCaretToken(textEditor) is not { } tokenSegment) - { - Logger.Trace("Token segment not found"); - return; - } - - var token = textEditor.Document.GetText(tokenSegment); - Logger.Trace("Using token {Token} ({@Segment})", token, tokenSegment); + Logger.Trace("Token segment not found"); + return; + } + + var token = textEditor.Document.GetText(tokenSegment); + Logger.Trace("Using token {Token} ({@Segment})", token, tokenSegment); - completionWindow = CreateCompletionWindow(textEditor.TextArea); - completionWindow.StartOffset = tokenSegment.Offset; - completionWindow.EndOffset = tokenSegment.EndOffset; + completionWindow = CreateCompletionWindow(textEditor.TextArea); + completionWindow.StartOffset = tokenSegment.Offset; + completionWindow.EndOffset = tokenSegment.EndOffset; - completionWindow.UpdateQuery(token); + completionWindow.UpdateQuery(token); - completionWindow.Closed += delegate - { - completionWindow = null; - }; + completionWindow.Closed += delegate + { + completionWindow = null; + }; - completionWindow.Show(); - }); + completionWindow.Show(); } } + else + { + // Disallowed chars, close completion window if its open + Logger.Trace($"Closing completion window: '{triggerText}' not a valid completion char"); + completionWindow?.Close(); + } + } + + /// + /// Highlights the text segment in the text editor + /// + private void HighlightTextSegment(ISegment segment) + { + textEditor.TextArea.Selection = Selection.Create(textEditor.TextArea, segment); } private void TextArea_TextEntering(object? sender, TextInputEventArgs e) @@ -162,26 +175,70 @@ private static bool IsCompletionChar(char c) } /// - /// Gets a segment of the token the caret is currently in. + /// Gets a segment of the token the caret is currently in for completions. + /// Returns null if caret is not on a valid completion token (i.e. comments) /// - private static ISegment? GetCaretToken(TextEditor textEditor) + private ISegment? GetCaretCompletionToken() { var caret = textEditor.CaretOffset; + + // Get the line the caret is on + var line = textEditor.Document.GetLineByOffset(caret); + var lineText = textEditor.Document.GetText(line.Offset, line.Length); + + // Tokenize + var result = TokenizerProvider.TokenizeLine(lineText); + + var currentTokenIndex = -1; + IToken? currentToken = null; + // Get the token the caret is after + foreach (var (i, token) in result.Tokens.Enumerate()) + { + // If we see a line comment token anywhere, return null + var isComment = token.Scopes.Any(s => s.Contains("comment.line")); + if (isComment) + { + Logger.Trace("Caret is in a comment"); + return null; + } + + // Find match + if (caret >= token.StartIndex && caret < token.EndIndex) + { + currentTokenIndex = i; + currentToken = token; + break; + } + } + + // Still not found + if (currentToken is null || currentTokenIndex == -1) + { + Logger.Info($"Could not find token at caret offset {caret} for line {lineText.ToRepr}"); + return null; + } + + // Cap the offsets by the line offsets + return new TextSegment + { + StartOffset = Math.Max(currentToken.StartIndex, line.Offset), + EndOffset = Math.Min(currentToken.EndIndex, line.EndOffset) + }; // Search for the start and end of a token // A token is defined as either alphanumeric chars or a space - var start = caret; + /*var start = caret; while (start > 0 && IsCompletionChar(textEditor.Document.GetCharAt(start - 1))) { start--; } - + var end = caret; while (end < textEditor.Document.TextLength && IsCompletionChar(textEditor.Document.GetCharAt(end))) { end++; } - - return start < end ? new TextSegment { StartOffset = start, EndOffset = end } : null; + + return start < end ? new TextSegment { StartOffset = start, EndOffset = end } : null;*/ } } diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs index 03f52ec52..f2c39a7c4 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs @@ -43,11 +43,11 @@ namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class CompletionList : TemplatedControl { + private CompletionListBox? _listBox; + public CompletionList() { DoubleTapped += OnDoubleTapped; - - CompletionAcceptKeys = new[] { Key.Enter, Key.Tab, }; } /// @@ -91,17 +91,33 @@ public string? FooterText /// Is raised when the completion list indicates that the user has chosen /// an entry to be completed. /// - public event EventHandler? InsertionRequested; + public event EventHandler? InsertionRequested; + + /// + /// Raised when the completion list indicates that it should be closed. + /// + public event EventHandler? CloseRequested; /// /// Raises the InsertionRequested event. /// - public void RequestInsertion(EventArgs e) + public void RequestInsertion(ICompletionData item, RoutedEventArgs triggeringEvent, string? appendText = null) { - InsertionRequested?.Invoke(this, e); + InsertionRequested?.Invoke(this, new InsertionRequestEventArgs + { + Item = item, + TriggeringEvent = triggeringEvent, + AppendText = appendText + }); + } + + /// + /// Raises the CloseRequested event. + /// + public void RequestClose() + { + CloseRequested?.Invoke(this, EventArgs.Empty); } - - private CompletionListBox? _listBox; protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -129,9 +145,16 @@ public CompletionListBox? ListBox } /// - /// Gets or sets the array of keys that request insertion of the completion + /// Dictionary of keys that request insertion of the completion + /// mapped to strings that will be appended to the completion when selected. + /// The string may be empty. /// - public Key[] CompletionAcceptKeys { get; set; } + public Dictionary CompletionAcceptKeys { get; init; } = new() + { + [Key.Enter] = "", + [Key.Tab] = "", + [Key.OemComma] = ",", + }; /// /// Gets the scroll viewer used in this list box. @@ -161,6 +184,7 @@ protected override void OnKeyDown(KeyEventArgs e) /// Handles a key press. Used to let the completion list handle key presses while the /// focus is still on the text editor. /// + [SuppressMessage("ReSharper", "SwitchStatementHandlesSomeKnownEnumValuesWithDefault")] public void HandleKey(KeyEventArgs e) { if (_listBox == null) @@ -197,10 +221,19 @@ public void HandleKey(KeyEventArgs e) break; default: // Check insertion keys - if (CompletionAcceptKeys.Contains(e.Key) && CurrentList?.Count > 0) + if (CompletionAcceptKeys.TryGetValue(e.Key, out var appendText) + && CurrentList?.Count > 0) { e.Handled = true; - RequestInsertion(e); + + if (SelectedItem is { } item) + { + RequestInsertion(item, e, appendText); + } + else + { + RequestClose(); + } } break; @@ -218,7 +251,15 @@ protected void OnDoubleTapped(object? sender, RoutedEventArgs e) ) { e.Handled = true; - RequestInsertion(e); + + if (SelectedItem is { } item) + { + RequestInsertion(item, e); + } + else + { + RequestClose(); + } } } @@ -331,6 +372,13 @@ private void SelectItemFilteringLive(string query, bool fullUpdate = false) var matchingItems = FilterItems(listToFilter, query); + // Close if no items match + if (matchingItems.Count == 0) + { + RequestClose(); + return; + } + // Fast path if both only 1 item, and item is the same if (FilteredCompletionData.Count == 1 && matchingItems.Count == 1 diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs index 5128238e4..f94da630c 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs @@ -155,17 +155,32 @@ private void CompletionList_SelectionChanged(object? sender, SelectionChangedEve #endregion - private void CompletionList_InsertionRequested(object? sender, EventArgs e) + private void CompletionList_InsertionRequested(object? sender, InsertionRequestEventArgs e) { Hide(); + // The window must close before Complete() is called. // If the Complete callback pushes stacked input handlers, we don't want to pop those when the CC window closes. - var item = CompletionList.SelectedItem; - item?.Complete(TextArea, new AnchorSegment(TextArea.Document, StartOffset, EndOffset - StartOffset), e); + var length = EndOffset - StartOffset; + e.Item.Complete(TextArea, new AnchorSegment(TextArea.Document, StartOffset, length), e); + + // Append text if requested + if (e.AppendText is { } appendText) + { + var end = StartOffset + e.Item.Text.Length; + TextArea.Document.Insert(end, appendText); + TextArea.Caret.Offset = end + appendText.Length; + } + } + + private void CompletionList_CloseRequested(object? sender, EventArgs e) + { + Hide(); } private void AttachEvents() { + CompletionList.CloseRequested += CompletionList_CloseRequested; CompletionList.InsertionRequested += CompletionList_InsertionRequested; CompletionList.SelectionChanged += CompletionList_SelectionChanged; TextArea.Caret.PositionChanged += CaretPositionChanged; @@ -176,6 +191,7 @@ private void AttachEvents() /// protected override void DetachEvents() { + CompletionList.CloseRequested -= CompletionList_CloseRequested; CompletionList.InsertionRequested -= CompletionList_InsertionRequested; CompletionList.SelectionChanged -= CompletionList_SelectionChanged; TextArea.Caret.PositionChanged -= CaretPositionChanged; diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/InsertionRequestEventArgs.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/InsertionRequestEventArgs.cs new file mode 100644 index 000000000..52b721cc3 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/InsertionRequestEventArgs.cs @@ -0,0 +1,12 @@ +using System; +using Avalonia.Interactivity; + +namespace StabilityMatrix.Avalonia.Controls.CodeCompletion; + +public class InsertionRequestEventArgs : EventArgs +{ + public required ICompletionData Item { get; init; } + public required RoutedEventArgs TriggeringEvent { get; init; } + + public string? AppendText { get; init; } +} From c320cdbe1510912eb4f2024f2904470d93fd0955 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 19 Aug 2023 03:16:25 -0400 Subject: [PATCH 135/474] Separate whitespace tokenization --- .../Assets/ImagePrompt.tmLanguage.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json b/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json index 49760be6e..087d46098 100644 --- a/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json +++ b/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json @@ -116,8 +116,12 @@ "match": "\\b(?:BREAK|AND)\\b", "name": "keyword.control" }, + "whitespace": { + "match": "\\s+", + "name": "meta.embedded.whitespace" + }, "text": { - "match": "[^,:\\[\\]\\(\\)]+", + "match": "[^,:\\[\\]\\(\\) ]+", "name": "meta.embedded" }, "value": { @@ -140,6 +144,9 @@ { "include": "#keyword" }, + { + "include": "#whitespace" + }, { "include": "#text" } From e6604a8f6f3296f2698513aa6bb324c5c85706a0 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 19 Aug 2023 04:00:59 -0400 Subject: [PATCH 136/474] Add prepare text function support, replace underscores setting --- .../Controls/CodeCompletion/CompletionData.cs | 20 +++++++++++++++++-- .../CodeCompletion/CompletionWindow.axaml.cs | 14 +++++-------- .../CodeCompletion/ICompletionData.cs | 5 +++-- .../DesignData/MockCompletionProvider.cs | 3 +++ .../TagCompletion/CompletionProvider.cs | 11 ++++++++++ .../TagCompletion/ICompletionProvider.cs | 8 +++++++- .../ViewModels/SettingsViewModel.cs | 8 +++++++- .../Views/SettingsPage.axaml | 11 ++++++++++ .../Models/Settings/Settings.cs | 5 +++++ 9 files changed, 70 insertions(+), 15 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs index 1796f4e82..f202d2c1f 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionData.cs @@ -62,9 +62,25 @@ private InlineCollection CreateInlines() } /// - public void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs) + public void Complete(TextArea textArea, ISegment completionSegment, InsertionRequestEventArgs eventArgs, Func? prepareText = null) { - textArea.Document.Replace(completionSegment, Text); + var text = Text; + + if (prepareText is not null) + { + text = prepareText(text); + } + + // Replace text + textArea.Document.Replace(completionSegment, text); + + // Append text if requested + if (eventArgs.AppendText is { } appendText) + { + var end = completionSegment.Offset + text.Length; + textArea.Document.Insert(end, appendText); + textArea.Caret.Offset = end + appendText.Length; + } } /// diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs index f94da630c..3ce97fd24 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs @@ -162,15 +162,11 @@ private void CompletionList_InsertionRequested(object? sender, InsertionRequestE // The window must close before Complete() is called. // If the Complete callback pushes stacked input handlers, we don't want to pop those when the CC window closes. var length = EndOffset - StartOffset; - e.Item.Complete(TextArea, new AnchorSegment(TextArea.Document, StartOffset, length), e); - - // Append text if requested - if (e.AppendText is { } appendText) - { - var end = StartOffset + e.Item.Text.Length; - TextArea.Document.Insert(end, appendText); - TextArea.Caret.Offset = end + appendText.Length; - } + e.Item.Complete( + TextArea, + new AnchorSegment(TextArea.Document, StartOffset, length), + e, + completionProvider.PrepareInsertionText); } private void CompletionList_CloseRequested(object? sender, EventArgs e) diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs index 55c43620a..3de0c8707 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/ICompletionData.cs @@ -72,10 +72,11 @@ public interface ICompletionData /// The text area on which completion is performed. /// The text segment that was used by the completion window if /// the user types (segment between CompletionWindow.StartOffset and CompletionWindow.EndOffset). - /// The EventArgs used for the insertion request. + /// The EventArgs used for the insertion request. /// These can be TextCompositionEventArgs, KeyEventArgs, MouseEventArgs, depending on how /// the insertion was triggered. - void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs); + /// Optional function to transform the text to be inserted + void Complete(TextArea textArea, ISegment completionSegment, InsertionRequestEventArgs eventArgs, Func? prepareText = null); /// /// Update the text character highlighting diff --git a/StabilityMatrix.Avalonia/DesignData/MockCompletionProvider.cs b/StabilityMatrix.Avalonia/DesignData/MockCompletionProvider.cs index 8dd435a21..cd6dfcdb1 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockCompletionProvider.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockCompletionProvider.cs @@ -12,6 +12,9 @@ public class MockCompletionProvider : ICompletionProvider /// public bool IsLoaded => false; + /// + public Func? PrepareInsertionText => null; + /// public Task LoadFromFile(FilePath path, bool recreate = false) { diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs index ec356d10d..9b7022f39 100644 --- a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs @@ -27,6 +27,7 @@ public class CompletionProvider : ICompletionProvider { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private readonly ISettingsManager settingsManager; private readonly INotificationService notificationService; private readonly AsyncLock loadLock = new(); @@ -36,8 +37,13 @@ public class CompletionProvider : ICompletionProvider public bool IsLoaded => searcher is not null; + public Func? PrepareInsertionText + => settingsManager.Settings.IsCompletionRemoveUnderscoresEnabled + ? PrepareInsertionText_RemoveUnderscores : null; + public CompletionProvider(ISettingsManager settingsManager, INotificationService notificationService) { + this.settingsManager = settingsManager; this.notificationService = notificationService; // Attach to load from set file on initial settings load @@ -69,6 +75,11 @@ void UpdateTagCompletionCsv() BackgroundLoadFromFile(fullPath); } } + + private static string PrepareInsertionText_RemoveUnderscores(string text) + { + return text.Replace("_", " "); + } /// public void BackgroundLoadFromFile(FilePath path, bool recreate = false) diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/ICompletionProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/ICompletionProvider.cs index 7d6d51875..3f6007381 100644 --- a/StabilityMatrix.Avalonia/Models/TagCompletion/ICompletionProvider.cs +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/ICompletionProvider.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Core.Models.FileInterfaces; @@ -11,6 +12,11 @@ public interface ICompletionProvider /// Whether the completion provider is loaded. /// bool IsLoaded { get; } + + /// + /// Optional function to transform the text to be inserted + /// + Func? PrepareInsertionText => null; /// /// Load the completion provider from a file. diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index 854392c4c..0d6d0ff99 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -98,9 +98,10 @@ public partial class SettingsViewModel : PageViewModelBase [ObservableProperty] private bool isPromptCompletionEnabled; [ObservableProperty] private IReadOnlyList availableTagCompletionCsvs = Array.Empty(); - [ObservableProperty] private string? selectedTagCompletionCsv; + [ObservableProperty] + private bool isCompletionRemoveUnderscoresEnabled; // Integrations section [ObservableProperty] private bool isDiscordRichPresenceEnabled; @@ -160,6 +161,11 @@ public SettingsViewModel( settings => settings.IsPromptCompletionEnabled, true); + settingsManager.RelayPropertyFor(this, + vm => vm.IsCompletionRemoveUnderscoresEnabled, + settings => settings.IsCompletionRemoveUnderscoresEnabled, + true); + DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn); ImportTagCsvCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn); } diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml index b3a1ad276..2d9c743ea 100644 --- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml @@ -149,6 +149,17 @@ Content="Import"/> + + + + + + + diff --git a/StabilityMatrix.Core/Models/Settings/Settings.cs b/StabilityMatrix.Core/Models/Settings/Settings.cs index 1fbf5c932..45bb195a1 100644 --- a/StabilityMatrix.Core/Models/Settings/Settings.cs +++ b/StabilityMatrix.Core/Models/Settings/Settings.cs @@ -54,6 +54,11 @@ public InstalledPackage? ActiveInstalledPackage /// public string? TagCompletionCsv { get; set; } + /// + /// Whether to remove underscores from completions + /// + public bool IsCompletionRemoveUnderscoresEnabled { get; set; } + public bool RemoveFolderLinksOnShutdown { get; set; } public bool IsDiscordRichPresenceEnabled { get; set; } From 6bf353c3ba5e479fedf05d5dcd14f25e92e2469f Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 19 Aug 2023 04:38:58 -0400 Subject: [PATCH 137/474] Fix token match index --- .../Behaviors/TextEditorCompletionBehavior.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs index b4e2483b8..c37d38c08 100644 --- a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs +++ b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs @@ -203,7 +203,7 @@ private static bool IsCompletionChar(char c) } // Find match - if (caret >= token.StartIndex && caret < token.EndIndex) + if (caret >= token.StartIndex && caret <= token.EndIndex) { currentTokenIndex = i; currentToken = token; From 5378f8de2c8a2aa964e0bdcfbcd6c4ea8f7d6813 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 19 Aug 2023 04:39:10 -0400 Subject: [PATCH 138/474] Escape character syntax support --- .../Assets/ImagePrompt.tmLanguage.json | 26 ++++++++++++++++++- .../Assets/ThemeMatrixDark.json | 16 ++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json b/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json index 087d46098..00437c4a5 100644 --- a/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json +++ b/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json @@ -16,6 +16,27 @@ "match": "(#).*$\\n?", "name": "comment.line.number-sign.prompt" }, + "escape": { + "begin": "\\\\", + "beginCaptures": { + "0": { + "name": "constant.character.escape.prompt" + } + }, + "end": "[-+.!(){}\\[\\]<\\>]", + "endCaptures": { + "0": { + "name": "constant.character.escape.target.prompt" + } + }, + "name": "meta.structure.escape.prompt", + "patterns": [ + { + "match": "[^-+.!(){}\\[\\]<\\>]", + "name": "invalid.illegal.escape.prompt" + } + ] + }, "parenthesized": { "begin": "\\(", "beginCaptures": { @@ -121,7 +142,7 @@ "name": "meta.embedded.whitespace" }, "text": { - "match": "[^,:\\[\\]\\(\\) ]+", + "match": "[^,:\\[\\]\\(\\) \\\\]+", "name": "meta.embedded" }, "value": { @@ -129,6 +150,9 @@ { "include": "#comment" }, + { + "include": "#escape" + }, { "include": "#parenthesized" }, diff --git a/StabilityMatrix.Avalonia/Assets/ThemeMatrixDark.json b/StabilityMatrix.Avalonia/Assets/ThemeMatrixDark.json index 79fbb6004..590ccbf89 100644 --- a/StabilityMatrix.Avalonia/Assets/ThemeMatrixDark.json +++ b/StabilityMatrix.Avalonia/Assets/ThemeMatrixDark.json @@ -116,6 +116,22 @@ "foreground": "#408080" } }, + { + "name": "Escape character", + "scope": "constant.character.escape", + "settings": { + "fontStyle": "", + "foreground": "#408080" + } + }, + { + "name": "Escape sequence target", + "scope": "constant.character.escape.target", + "settings": { + "fontStyle": "", + "foreground": "#C5C8C6" + } + }, { "name": "User-defined constant", "scope": "constant.character, constant.other", From 27e2251472347d8c0460481ba264740ee1526d2e Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 19 Aug 2023 04:58:19 -0400 Subject: [PATCH 139/474] Fix token calculations for non first lines --- .../Behaviors/TextEditorCompletionBehavior.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs index c37d38c08..ff71c229e 100644 --- a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs +++ b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs @@ -186,6 +186,8 @@ private static bool IsCompletionChar(char c) var line = textEditor.Document.GetLineByOffset(caret); var lineText = textEditor.Document.GetText(line.Offset, line.Length); + var caretAbsoluteOffset = caret - line.Offset; + // Tokenize var result = TokenizerProvider.TokenizeLine(lineText); @@ -203,7 +205,7 @@ private static bool IsCompletionChar(char c) } // Find match - if (caret >= token.StartIndex && caret <= token.EndIndex) + if (caretAbsoluteOffset >= token.StartIndex && caretAbsoluteOffset <= token.EndIndex) { currentTokenIndex = i; currentToken = token; @@ -214,15 +216,19 @@ private static bool IsCompletionChar(char c) // Still not found if (currentToken is null || currentTokenIndex == -1) { - Logger.Info($"Could not find token at caret offset {caret} for line {lineText.ToRepr}"); + Logger.Info($"Could not find token at caret offset {caret} for line {lineText.ToRepr()}"); return null; } + + var startOffset = currentToken.StartIndex + line.Offset; + var endOffset = currentToken.EndIndex + line.EndOffset; + // Cap the offsets by the line offsets return new TextSegment { - StartOffset = Math.Max(currentToken.StartIndex, line.Offset), - EndOffset = Math.Min(currentToken.EndIndex, line.EndOffset) + StartOffset = Math.Max(startOffset, line.Offset), + EndOffset = Math.Min(endOffset, line.EndOffset) }; // Search for the start and end of a token From a8b62f31f8ea16ef8dd2e04eff92e0c8e7b7742e Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 20 Aug 2023 17:09:01 -0400 Subject: [PATCH 140/474] Add Yoh.Text.Json.NamingPolicies nuget --- StabilityMatrix.Core/StabilityMatrix.Core.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/StabilityMatrix.Core/StabilityMatrix.Core.csproj b/StabilityMatrix.Core/StabilityMatrix.Core.csproj index 28c37f221..88f3fed12 100644 --- a/StabilityMatrix.Core/StabilityMatrix.Core.csproj +++ b/StabilityMatrix.Core/StabilityMatrix.Core.csproj @@ -40,6 +40,10 @@ + + + + From f0b4654c736e30e52e40ba94139eaabcbb61509b Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 20 Aug 2023 17:09:45 -0400 Subject: [PATCH 141/474] Region formatting --- .../Controls/AdvancedImageBox.axaml.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs b/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs index d70471b6d..e6390ed96 100644 --- a/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs +++ b/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs @@ -40,13 +40,6 @@ namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class AdvancedImageBox : TemplatedControl { - private static readonly Rect EmptyRect = new(); - - private static bool IsRectEmpty(Rect rect) - { - return rect == EmptyRect; - } - #region Bindable Base /// /// Multicast event for property change notifications. @@ -1893,6 +1886,19 @@ public void PerformActualSize() #endregion #region Utility methods + /// + /// Determines whether the specified rectangle is empty + /// + private static bool IsRectEmpty(Rect rect) + { + return rect == EmptyRect; + } + + /// + /// Static empty rectangle + /// + private static readonly Rect EmptyRect = new(); + /// /// Determines whether the specified point is located within the image view port /// From 6cc9a6d7061030c8a8e58f2951b4f68fb241a446 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 20 Aug 2023 17:09:59 -0400 Subject: [PATCH 142/474] Fix completion line end offset --- .../Behaviors/TextEditorCompletionBehavior.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs index ff71c229e..c9c4fd49f 100644 --- a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs +++ b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs @@ -222,7 +222,7 @@ private static bool IsCompletionChar(char c) var startOffset = currentToken.StartIndex + line.Offset; - var endOffset = currentToken.EndIndex + line.EndOffset; + var endOffset = currentToken.EndIndex + line.Offset; // Cap the offsets by the line offsets return new TextSegment From 942ee35fbbd72de20b62e0a4d3a4e7c4874470b5 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 20 Aug 2023 17:12:11 -0400 Subject: [PATCH 143/474] Add ImageViewerDialog --- StabilityMatrix.Avalonia/App.axaml.cs | 3 ++ .../DesignData/DesignData.cs | 4 +++ .../Dialogs/ImageViewerViewModel.cs | 14 +++++++++ .../Views/Dialogs/ImageViewerDialog.axaml | 30 +++++++++++++++++++ .../Views/Dialogs/ImageViewerDialog.axaml.cs | 18 +++++++++++ 5 files changed, 69 insertions(+) create mode 100644 StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs create mode 100644 StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml create mode 100644 StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml.cs diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 66edfa1b9..49002298e 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -248,6 +248,7 @@ internal static void ConfigureDialogViewModels(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddSingleton(); @@ -303,6 +304,7 @@ internal static void ConfigureDialogViewModels(IServiceCollection services) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) .Register(provider.GetRequiredService)); } @@ -338,6 +340,7 @@ internal static void ConfigureViews(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Controls services.AddTransient(); diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 714e23583..6181a2214 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -480,11 +480,15 @@ public static CompletionList SampleCompletionList IsFiltering = true }; list.CompletionData.AddRange(SampleCompletionData); + list.FilteredCompletionData.AddRange(list.CompletionData); list.SelectItem("te", true); return list; } } + public static ImageViewerViewModel ImageViewerViewModel + => DialogFactory.Get(); + public static Indexer Types => new(); public class Indexer diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs new file mode 100644 index 000000000..1e3a6f1ed --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs @@ -0,0 +1,14 @@ +using Avalonia.Media.Imaging; +using CommunityToolkit.Mvvm.ComponentModel; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Avalonia.Views.Dialogs; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; + +[View(typeof(ImageViewerDialog))] +public partial class ImageViewerViewModel : ViewModelBase +{ + [ObservableProperty] + private Bitmap? image; +} diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml new file mode 100644 index 000000000..dc05faf0c --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml.cs new file mode 100644 index 000000000..0f7325e3c --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace StabilityMatrix.Avalonia.Views.Dialogs; + +public partial class ImageViewerDialog : UserControl +{ + public ImageViewerDialog() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file From 6a5297a3a5a09e5fcbb9347b22841bd0d8ee08c3 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 20 Aug 2023 17:13:38 -0400 Subject: [PATCH 144/474] Add new typed Comfy connections, model upscale --- .../Controls/UpscalerCard.axaml | 4 +- .../DesignData/MockInferenceClientManager.cs | 3 +- .../Services/InferenceClientManager.cs | 17 +- .../InferenceTextToImageViewModel.cs | 232 ++++++++++-------- StabilityMatrix.Core/Inference/ComfyClient.cs | 1 + .../Models/Api/Comfy/ComfyNode.cs | 12 - .../Models/Api/Comfy/ComfyPromptRequest.cs | 1 + .../Models/Api/Comfy/ComfyUpscalerType.cs | 3 +- .../Api/Comfy/NodeTypes/NodeConnectionBase.cs | 16 ++ .../Api/Comfy/NodeTypes/NodeConnections.cs | 13 + .../Models/Api/Comfy/Nodes/ComfyNode.cs | 26 ++ .../Api/Comfy/Nodes/ComfyNodeBuilder.cs | 131 ++++++++++ .../Models/Api/Comfy/Nodes/IOutputNode.cs | 9 + .../Models/Api/Comfy/Nodes/NamedComfyNode.cs | 36 +++ .../Models/Api/Comfy/Nodes/NodeDictionary.cs | 33 +++ .../Models/Api/Comfy/Nodes/RerouteNode.cs | 17 ++ 16 files changed, 435 insertions(+), 119 deletions(-) delete mode 100644 StabilityMatrix.Core/Models/Api/Comfy/ComfyNode.cs create mode 100644 StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnectionBase.cs create mode 100644 StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs create mode 100644 StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNode.cs create mode 100644 StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs create mode 100644 StabilityMatrix.Core/Models/Api/Comfy/Nodes/IOutputNode.cs create mode 100644 StabilityMatrix.Core/Models/Api/Comfy/Nodes/NamedComfyNode.cs create mode 100644 StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs create mode 100644 StabilityMatrix.Core/Models/Api/Comfy/Nodes/RerouteNode.cs diff --git a/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml b/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml index 5fb7c4154..c6605128c 100644 --- a/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml @@ -4,6 +4,8 @@ xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:vmInference="using:StabilityMatrix.Avalonia.ViewModels.Inference" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:data="clr-namespace:FluentAvalonia.UI.Data;assembly=FluentAvalonia" + xmlns:comfy="clr-namespace:StabilityMatrix.Core.Models.Api.Comfy;assembly=StabilityMatrix.Core" x:DataType="vmInference:UpscalerCardViewModel"> @@ -20,7 +22,7 @@ - + ? Upscalers { get; set; } = new ComfyUpscaler[] { new("nearest-exact", ComfyUpscalerType.Latent), - new("bicubic", ComfyUpscalerType.Latent) + new("bicubic", ComfyUpscalerType.Latent), + new("ESRGAN-4x", ComfyUpscalerType.ESRGAN) }; public bool IsConnected { get; set; } diff --git a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs index 774f38a95..61d0ceef2 100644 --- a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Inference; using StabilityMatrix.Core.Models; @@ -20,6 +21,7 @@ namespace StabilityMatrix.Avalonia.Services; /// public partial class InferenceClientManager : ObservableObject, IInferenceClientManager { + private readonly ILogger logger; private readonly IApiFactory apiFactory; [ObservableProperty, NotifyPropertyChangedFor(nameof(IsConnected))] @@ -37,8 +39,9 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient [ObservableProperty] private IReadOnlyCollection? upscalers; - public InferenceClientManager(IApiFactory apiFactory) + public InferenceClientManager(ILogger logger, IApiFactory apiFactory) { + this.logger = logger; this.apiFactory = apiFactory; } @@ -65,6 +68,18 @@ private async Task LoadSharedPropertiesAsync() upscalerBuilder.AddRange(latentUpscalerNames.Select( s => new ComfyUpscaler(s, ComfyUpscalerType.Latent))); } + logger.LogTrace("Loaded latent upscale methods: {@Upscalers}", latentUpscalerNames); + + // Add Model upscale methods + var modelUpscalerNames = await Client.GetNodeOptionNamesAsync( + "UpscaleModelLoader", + "model_name"); + if (modelUpscalerNames is not null) + { + upscalerBuilder.AddRange(modelUpscalerNames.Select( + s => new ComfyUpscaler(s, ComfyUpscalerType.ESRGAN))); + } + logger.LogTrace("Loaded model upscale methods: {@Upscalers}", modelUpscalerNames); Upscalers = upscalerBuilder.ToImmutable(); } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 538c2325f..0338333a0 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -24,8 +24,11 @@ using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Inference; using StabilityMatrix.Core.Models.Api.Comfy; +using StabilityMatrix.Core.Models.Api.Comfy.Nodes; +using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; #pragma warning disable CS0657 // Not a valid attribute location for this declaration @@ -117,87 +120,92 @@ ServiceManager vmFactory GenerateImageCommand.WithNotificationErrorHandler(notificationService); } - private Dictionary GetCurrentPrompt() + private (NodeDictionary prompt, string[] outputs) BuildPrompt() { - var sampler = StackCardViewModel.GetCard(); + using var _ = new CodeTimer(); + + var samplerCard = StackCardViewModel.GetCard(); var batchCard = StackCardViewModel.GetCard(); var modelCard = StackCardViewModel.GetCard(); var seedCard = StackCardViewModel.GetCard(); + + var prompt = new NodeDictionary(); + var builder = new ComfyNodeBuilder(prompt); - var prompt = new Dictionary + var checkpointLoader = prompt.AddNamedNode(new NamedComfyNode("CheckpointLoader") { - ["CheckpointLoader"] = new() - { - ClassType = "CheckpointLoaderSimple", - Inputs = new Dictionary - { - ["ckpt_name"] = modelCard.SelectedModelName - } - }, - ["EmptyLatentImage"] = new() + ClassType = "CheckpointLoaderSimple", + Inputs = new Dictionary { - ClassType = "EmptyLatentImage", - Inputs = new Dictionary - { - ["batch_size"] = batchCard.BatchSize, - ["height"] = sampler.Height, - ["width"] = sampler.Width, - } - }, - ["Sampler"] = new() + ["ckpt_name"] = modelCard.SelectedModelName + } + }); + + var checkpointVae = checkpointLoader.GetOutput(2); + + var emptyLatentImage = prompt.AddNamedNode(new NamedComfyNode("EmptyLatentImage") + { + ClassType = "EmptyLatentImage", + Inputs = new Dictionary { - ClassType = "KSampler", - Inputs = new Dictionary - { - ["cfg"] = sampler.CfgScale, - ["denoise"] = 1, - ["latent_image"] = new object[] { "EmptyLatentImage", 0 }, - ["model"] = new object[] { "CheckpointLoader", 0 }, - ["negative"] = new object[] { "NegativeCLIP", 0 }, - ["positive"] = new object[] { "PositiveCLIP", 0 }, - ["sampler_name"] = sampler.SelectedSampler?.Name, - ["scheduler"] = "normal", - ["seed"] = seedCard.Seed, - ["steps"] = sampler.Steps - } - }, - ["PositiveCLIP"] = new() + ["batch_size"] = batchCard.BatchSize, + ["height"] = samplerCard.Height, + ["width"] = samplerCard.Width, + } + }); + + var positiveClip = prompt.AddNamedNode(new NamedComfyNode("PositiveCLIP") + { + ClassType = "CLIPTextEncode", + Inputs = new Dictionary { - ClassType = "CLIPTextEncode", - Inputs = new Dictionary - { - ["clip"] = new object[] { "CheckpointLoader", 1 }, - ["text"] = PromptCardViewModel.PromptDocument.Text, - } - }, - ["NegativeCLIP"] = new() + ["clip"] = checkpointLoader.GetOutput(1), + ["text"] = PromptCardViewModel.PromptDocument.Text, + } + }); + + var negativeClip = prompt.AddNamedNode(new NamedComfyNode("NegativeCLIP") + { + ClassType = "CLIPTextEncode", + Inputs = new Dictionary { - ClassType = "CLIPTextEncode", - Inputs = new Dictionary - { - ["clip"] = new object[] { "CheckpointLoader", 1 }, - ["text"] = PromptCardViewModel.NegativePromptDocument.Text, - } - }, - ["VAEDecoder"] = new() + ["clip"] = checkpointLoader.GetOutput(1), + ["text"] = PromptCardViewModel.NegativePromptDocument.Text, + } + }); + + var sampler = prompt.AddNamedNode(ComfyNodeBuilder.KSampler( + "Sampler", + checkpointLoader.GetOutput(0), + Convert.ToUInt64(seedCard.Seed), + samplerCard.Steps, + samplerCard.CfgScale, + samplerCard.SelectedSampler?.Name ?? throw new InvalidOperationException("Sampler not selected"), + "normal", + positiveClip.GetOutput(0), + negativeClip.GetOutput(0), + emptyLatentImage.GetOutput(0), + samplerCard.DenoiseStrength)); + + var vaeDecoder = prompt.AddNamedNode(new NamedComfyNode("VAEDecoder") + { + ClassType = "VAEDecode", + Inputs = new Dictionary { - ClassType = "VAEDecode", - Inputs = new Dictionary - { - ["samples"] = new object[] { "Sampler", 0 }, - ["vae"] = new object[] { "CheckpointLoader", 2 } - } - }, - ["SaveImage"] = new() + ["samples"] = sampler.GetOutput(0), + ["vae"] = checkpointLoader.GetOutput(2) + } + }); + + var saveImage = prompt.AddNamedNode(new NamedComfyNode("SaveImage") + { + ClassType = "SaveImage", + Inputs = new Dictionary { - ClassType = "SaveImage", - Inputs = new Dictionary - { - ["filename_prefix"] = "SM-Inference", - ["images"] = new object[] { "VAEDecoder", 0 } - } + ["filename_prefix"] = "SM-Inference", + ["images"] = vaeDecoder.GetOutput(0) } - }; + }); // If hi-res fix is enabled, add the LatentUpscale node and another KSampler node if (IsHiresFixEnabled) @@ -205,43 +213,63 @@ private Dictionary GetCurrentPrompt() var hiresUpscalerCard = UpscalerCardViewModel; var hiresSamplerCard = HiresSamplerCardViewModel; - prompt["LatentUpscale"] = new ComfyNode + // Select between latent upscale and normal upscale based on the upscale method + var selectedUpscaler = hiresUpscalerCard.SelectedUpscaler; + + LatentNodeConnection hiresOutput; + + if (selectedUpscaler?.Type == ComfyUpscalerType.Latent) { - ClassType = "LatentUpscale", - Inputs = new Dictionary + hiresOutput = prompt.AddNamedNode(new NamedComfyNode("LatentUpscale") { - ["upscale_method"] = hiresUpscalerCard.SelectedUpscaler?.Name, - ["width"] = sampler.Width * hiresUpscalerCard.Scale, - ["height"] = sampler.Height * hiresUpscalerCard.Scale, - ["crop"] = "disabled", - ["samples"] = new object[] { "Sampler", 0 } - } - }; - - prompt["Sampler2"] = new ComfyNode + ClassType = "LatentUpscale", + Inputs = new Dictionary + { + ["upscale_method"] = hiresUpscalerCard.SelectedUpscaler?.Name, + ["width"] = samplerCard.Width * hiresUpscalerCard.Scale, + ["height"] = samplerCard.Height * hiresUpscalerCard.Scale, + ["crop"] = "disabled", + ["samples"] = sampler.Output + } + }).GetOutput(0); + } + else if (selectedUpscaler?.Type == ComfyUpscalerType.ESRGAN) { - ClassType = "KSampler", - Inputs = new Dictionary - { - ["cfg"] = hiresSamplerCard.CfgScale, - ["denoise"] = hiresSamplerCard.DenoiseStrength, - ["latent_image"] = new object[] { "LatentUpscale", 0 }, - ["model"] = new object[] { "CheckpointLoader", 0 }, - ["negative"] = new object[] { "NegativeCLIP", 0 }, - ["positive"] = new object[] { "PositiveCLIP", 0 }, - // Use hires sampler name if not null, otherwise use the normal sampler name - ["sampler_name"] = hiresSamplerCard.SelectedSampler?.Name ?? sampler.SelectedSampler?.Name, - ["scheduler"] = "normal", - ["seed"] = seedCard.Seed, - ["steps"] = hiresSamplerCard.Steps - } - }; + // Convert to image space + var samplerImage = builder.Lambda_LatentToImage(sampler.Output, checkpointVae); + // Do group upscale + var modelUpscaler = builder.Group_UpscaleWithModel("Upscaler", + selectedUpscaler.Value.Name, samplerImage); + // Convert back to latent space + hiresOutput = builder.Lambda_ImageToLatent(modelUpscaler.Output, checkpointVae); + } + else + { + // If no upscaler selected or none, just reroute the latent image + hiresOutput = sampler.Output; + } + + var hiresSampler = prompt.AddNamedNode(ComfyNodeBuilder.KSampler( + "HiresSampler", + checkpointLoader.GetOutput(0), + Convert.ToUInt64(seedCard.Seed), + hiresSamplerCard.Steps, + hiresSamplerCard.CfgScale, + // Use hires sampler name if not null, otherwise use the normal sampler name + hiresSamplerCard.SelectedSampler?.Name ?? samplerCard.SelectedSampler?.Name ?? throw new InvalidOperationException("Sampler not selected"), + "normal", + positiveClip.GetOutput(0), + negativeClip.GetOutput(0), + hiresOutput, + hiresSamplerCard.DenoiseStrength)); - // Reroute the VAEDecoder's input to be from Sampler2 - prompt["VAEDecoder"].Inputs["samples"] = new object[] { "Sampler2", 0 }; + // Reroute the VAEDecoder's input to be from the hires sampler + vaeDecoder.Inputs["samples"] = hiresSampler.Output; } + + prompt.NormalizeConnectionTypes(); - return prompt; + return (prompt, new[] { saveImage.Name }); } private void OnProgressUpdateReceived(object? sender, ComfyProgressUpdateEventArgs args) @@ -282,7 +310,7 @@ private async Task GenerateImageImpl(CancellationToken cancellationToken = defau var client = ClientManager.Client; - var nodes = GetCurrentPrompt(); + var (nodes, outputNodeNames) = BuildPrompt(); // Connect progress handler // client.ProgressUpdateReceived += OnProgressUpdateReceived; @@ -317,14 +345,14 @@ private async Task GenerateImageImpl(CancellationToken cancellationToken = defau Logger.Trace($"Prompt task {promptTask.Id} finished"); // Get output images - var outputs = await client.GetImagesForExecutedPromptAsync( + var imageOutputs = await client.GetImagesForExecutedPromptAsync( promptTask.Id, cancellationToken ); ImageGalleryCardViewModel.ImageSources.Clear(); - var images = outputs["SaveImage"]; + var images = imageOutputs[outputNodeNames[0]]; if (images is null) return; List outputImages; diff --git a/StabilityMatrix.Core/Inference/ComfyClient.cs b/StabilityMatrix.Core/Inference/ComfyClient.cs index 48090cb85..f695b0e77 100644 --- a/StabilityMatrix.Core/Inference/ComfyClient.cs +++ b/StabilityMatrix.Core/Inference/ComfyClient.cs @@ -4,6 +4,7 @@ using NLog; using StabilityMatrix.Core.Api; 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 Websocket.Client; diff --git a/StabilityMatrix.Core/Models/Api/Comfy/ComfyNode.cs b/StabilityMatrix.Core/Models/Api/Comfy/ComfyNode.cs deleted file mode 100644 index e8507c559..000000000 --- a/StabilityMatrix.Core/Models/Api/Comfy/ComfyNode.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace StabilityMatrix.Core.Models.Api.Comfy; - -public class ComfyNode -{ - [JsonPropertyName("class_type")] - public required string ClassType { get; set; } - - [JsonPropertyName("inputs")] - public required Dictionary Inputs { get; set; } -} diff --git a/StabilityMatrix.Core/Models/Api/Comfy/ComfyPromptRequest.cs b/StabilityMatrix.Core/Models/Api/Comfy/ComfyPromptRequest.cs index 1006cca89..f5b875dfb 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/ComfyPromptRequest.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/ComfyPromptRequest.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using StabilityMatrix.Core.Models.Api.Comfy.Nodes; namespace StabilityMatrix.Core.Models.Api.Comfy; diff --git a/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscalerType.cs b/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscalerType.cs index 8986c7987..e1a16497a 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscalerType.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscalerType.cs @@ -3,10 +3,9 @@ namespace StabilityMatrix.Core.Models.Api.Comfy; -[JsonConverter(typeof(DefaultUnknownEnumConverter))] public enum ComfyUpscalerType { - Unknown, + None, Latent, ESRGAN } diff --git a/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnectionBase.cs b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnectionBase.cs new file mode 100644 index 000000000..9ca232d3c --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnectionBase.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; +using StabilityMatrix.Core.Converters.Json; + +namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; + +[JsonConverter(typeof(NodeConnectionBaseJsonConverter))] +public abstract class NodeConnectionBase +{ + public object[]? Data { get; set; } + + // Implicit conversion to object[] + public static implicit operator object[](NodeConnectionBase nodeConnection) + { + return nodeConnection.Data ?? Array.Empty(); + } +} diff --git a/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs new file mode 100644 index 000000000..0c7f829c0 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs @@ -0,0 +1,13 @@ +namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; + +public class LatentNodeConnection : NodeConnectionBase { } + +public class VAENodeConnection : NodeConnectionBase { } + +public class ImageNodeConnection : NodeConnectionBase { } + +public class UpscaleModelNodeConnection : NodeConnectionBase { } + +public class ModelNodeConnection : NodeConnectionBase { } + +public class ConditioningNodeConnection : NodeConnectionBase { } diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNode.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNode.cs new file mode 100644 index 000000000..d190327ef --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNode.cs @@ -0,0 +1,26 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; + +namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; + +[JsonSerializable(typeof(ComfyNode))] +[SuppressMessage("ReSharper", "CollectionNeverQueried.Global")] +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] +public record ComfyNode +{ + [JsonPropertyName("class_type")] + public required string ClassType { get; init; } + + [JsonPropertyName("inputs")] + public required Dictionary Inputs { get; init; } + + public NamedComfyNode ToNamedNode(string name) + { + return new NamedComfyNode(name) + { + ClassType = ClassType, + Inputs = Inputs + }; + } +} diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs new file mode 100644 index 000000000..b06986228 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs @@ -0,0 +1,131 @@ +using System.Diagnostics.CodeAnalysis; +using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; + +namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; + +/// +/// Builder functions for comfy nodes +/// +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public class ComfyNodeBuilder +{ + private readonly NodeDictionary nodes; + + public ComfyNodeBuilder(NodeDictionary nodes) + { + this.nodes = nodes; + } + + private static string GetRandomPrefix() => Guid.NewGuid().ToString()[..8]; + + public static NamedComfyNode VAEEncode( + string name, + ImageNodeConnection pixels, + VAENodeConnection vae + ) + { + return new NamedComfyNode(name) + { + ClassType = "VAEEncode", + Inputs = new Dictionary { ["pixels"] = pixels.Data, ["vae"] = vae.Data } + }; + } + + public static NamedComfyNode VAEDecode( + string name, + LatentNodeConnection samples, + VAENodeConnection vae + ) + { + return new NamedComfyNode(name) + { + ClassType = "VAEDecode", + Inputs = new Dictionary { ["latent"] = samples.Data, ["vae"] = vae.Data } + }; + } + + public static NamedComfyNode KSampler( + string name, + ModelNodeConnection model, + ulong seed, + int steps, + double cfg, + string samplerName, + string scheduler, + ConditioningNodeConnection positive, + ConditioningNodeConnection negative, + LatentNodeConnection latentImage, + double denoise + ) + { + return new NamedComfyNode(name) + { + ClassType = "KSampler", + Inputs = new Dictionary + { + ["model"] = model.Data, + ["seed"] = seed, + ["steps"] = steps, + ["cfg"] = cfg, + ["sampler_name"] = samplerName, + ["scheduler"] = scheduler, + ["positive"] = positive.Data, + ["negative"] = negative.Data, + ["latent_image"] = latentImage.Data, + ["denoise"] = denoise + } + }; + } + + public static NamedComfyNode ImageUpscaleWithModel( + string name, + UpscaleModelNodeConnection upscaleModel, + ImageNodeConnection image + ) + { + return new NamedComfyNode(name) + { + ClassType = "ImageUpscaleWithModel", + Inputs = new Dictionary + { + ["upscale_model"] = upscaleModel.Data, + ["image"] = image.Data + } + }; + } + + public static NamedComfyNode UpscaleModelLoader( + string name, + string modelName) + { + return new NamedComfyNode(name) + { + ClassType = "UpscaleModelLoader", + Inputs = new Dictionary { ["model_name"] = modelName } + }; + } + + public ImageNodeConnection Lambda_LatentToImage(LatentNodeConnection latent, VAENodeConnection vae) + { + return nodes.AddNamedNode(VAEDecode($"{GetRandomPrefix()}_VAEDecode", latent, vae)).Output; + } + + public LatentNodeConnection Lambda_ImageToLatent(ImageNodeConnection pixels, VAENodeConnection vae) + { + return nodes.AddNamedNode(VAEEncode($"{GetRandomPrefix()}_VAEEncode", pixels, vae)).Output; + } + + /// + /// Create a upscaling node based on a + /// + public NamedComfyNode Group_UpscaleWithModel(string name, string modelName, ImageNodeConnection image) + { + var modelLoader = nodes.AddNamedNode( + UpscaleModelLoader($"{name}_UpscaleModelLoader", modelName)); + + var upscaler = nodes.AddNamedNode( + ImageUpscaleWithModel($"{name}_ImageUpscaleWithModel", modelLoader.Output, image)); + + return upscaler; + } +} diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/IOutputNode.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/IOutputNode.cs new file mode 100644 index 000000000..915a6f169 --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/IOutputNode.cs @@ -0,0 +1,9 @@ +namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; + +public interface IOutputNode +{ + /// + /// Returns { Name, index } for use as a node connection + /// + public object[] GetOutput(int index); +} diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/NamedComfyNode.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/NamedComfyNode.cs new file mode 100644 index 000000000..23ba8878c --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/NamedComfyNode.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; +using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; + +namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; + +[JsonSerializable(typeof(NamedComfyNode))] +public record NamedComfyNode([property: JsonIgnore] string Name) : ComfyNode, IOutputNode +{ + /// + /// Returns { Name, index } for use as a node connection + /// + public object[] GetOutput(int index) + { + return new object[] { Name, index }; + } + + /// + /// Returns typed { Name, index } for use as a node connection + /// + public TOutput GetOutput(int index) where TOutput : NodeConnectionBase, new() + { + return new TOutput + { + Data = GetOutput(index) + }; + } +} + +[JsonSerializable(typeof(NamedComfyNode<>))] +public record NamedComfyNode(string Name) : NamedComfyNode(Name) where TOutput : NodeConnectionBase, new() +{ + public TOutput Output => new TOutput + { + Data = GetOutput(0) + }; +} diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs new file mode 100644 index 000000000..12a6e415b --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs @@ -0,0 +1,33 @@ +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; + +namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; + +public class NodeDictionary : Dictionary +{ + public TNamedNode AddNamedNode(TNamedNode node) where TNamedNode : NamedComfyNode + { + Add(node.Name, node); + return node; + } + + public void NormalizeConnectionTypes() + { + using var _ = new CodeTimer(); + + // Convert all node inputs containing NodeConnectionBase objects to their Data property + foreach (var node in Values) + { + lock (node.Inputs) + { + foreach (var (key, input) in node.Inputs) + { + if (input is NodeConnectionBase connection) + { + node.Inputs[key] = connection.Data; + } + } + } + } + } +} diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/RerouteNode.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/RerouteNode.cs new file mode 100644 index 000000000..0cbf7caac --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/RerouteNode.cs @@ -0,0 +1,17 @@ +namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; + +/// +/// Skeleton node that relays the output of another node +/// +public record RerouteNode(object[] Connection) : IOutputNode +{ + /// + public object[] GetOutput(int index) + { + if (index != 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + return Connection; + } +} From 1f2372b23a4e88b8e85631c729c45fd6aae381d8 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 20 Aug 2023 17:17:21 -0400 Subject: [PATCH 145/474] Use ImageViewerDialog for settings grid test --- .../ViewModels/SettingsViewModel.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index 0d6d0ff99..96e90146a 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -14,6 +14,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Notifications; +using Avalonia.Controls.PanAndZoom; using Avalonia.Controls.Primitives; using Avalonia.Media.Imaging; using Avalonia.Platform.Storage; @@ -542,19 +543,33 @@ private async Task DebugMakeImageGrid() using var data = peekPixels.Encode(SKEncodedImageFormat.Jpeg, 100); await using var stream = data.AsStream(); - var image = new AdvancedImageBox + var bitmap = WriteableBitmap.Decode(stream); + + /*var imageBox = new AdvancedImageBox { - Image = WriteableBitmap.Decode(stream), + Image = bitmap, Width = 600, Height = 800, PixelGridZoomThreshold = 10, ConstrainZoomOutToFitLevel = true, MaxZoom = 6400 * 2, + };*/ + + var imageBox = new ImageViewerDialog() + { + MinWidth = 1500, + MinHeight = 900, + DataContext = new ImageViewerViewModel() + { + Image = bitmap + } }; var dialog = new BetterContentDialog { - Content = image, + MaxDialogWidth = 1000, + FullSizeDesired = true, + Content = imageBox, CloseButtonText = "Close", ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled, }; From 765d94e0103df8639371fe6da40b5fa75edb427e Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 20 Aug 2023 17:19:06 -0400 Subject: [PATCH 146/474] Create TypeExtensions.cs --- StabilityMatrix.Core/Extensions/TypeExtensions.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 StabilityMatrix.Core/Extensions/TypeExtensions.cs diff --git a/StabilityMatrix.Core/Extensions/TypeExtensions.cs b/StabilityMatrix.Core/Extensions/TypeExtensions.cs new file mode 100644 index 000000000..8806868be --- /dev/null +++ b/StabilityMatrix.Core/Extensions/TypeExtensions.cs @@ -0,0 +1,15 @@ +using System.Reflection; + +namespace StabilityMatrix.Core.Extensions; + +public static class TypeExtensions +{ + /// + /// Get all properties marked with an attribute of type + /// + public static IEnumerable GetPropertiesWithAttribute(this Type type) + where TAttribute : Attribute + { + return type.GetProperties().Where(p => Attribute.IsDefined(p, typeof(TAttribute))); + } +} From 324e4578a6066298d8bebc69ecc8995bedd08af0 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 20 Aug 2023 17:19:24 -0400 Subject: [PATCH 147/474] Create StringJsonConverter.cs --- .../Converters/Json/StringJsonConverter.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 StabilityMatrix.Core/Converters/Json/StringJsonConverter.cs diff --git a/StabilityMatrix.Core/Converters/Json/StringJsonConverter.cs b/StabilityMatrix.Core/Converters/Json/StringJsonConverter.cs new file mode 100644 index 000000000..efd5304fd --- /dev/null +++ b/StabilityMatrix.Core/Converters/Json/StringJsonConverter.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace StabilityMatrix.Core.Converters.Json; + +/// +/// Json converter for types that serialize to string by `ToString()` and +/// can be created by `Activator.CreateInstance(Type, string)` +/// +public class StringJsonConverter : JsonConverter +{ + /// + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException(); + } + + var value = reader.GetString(); + if (value is null) + { + throw new JsonException(); + } + + return (T) Activator.CreateInstance(typeToConvert, value); + } + + /// + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) + { + writer.WriteStringValue(value?.ToString()); + } +} From 42a572ec26dcf4bfe3d9821645bb0689e8f0158b Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 22 Aug 2023 03:46:37 -0400 Subject: [PATCH 148/474] Fix merge stuff --- .../Models/Api/Comfy/NodeTypes/NodeConnectionBase.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnectionBase.cs b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnectionBase.cs index 9ca232d3c..a1da0c661 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnectionBase.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnectionBase.cs @@ -1,9 +1,5 @@ -using System.Text.Json.Serialization; -using StabilityMatrix.Core.Converters.Json; +namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; -namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; - -[JsonConverter(typeof(NodeConnectionBaseJsonConverter))] public abstract class NodeConnectionBase { public object[]? Data { get; set; } From e974b798aeea715784d8bb71dfbe63092e2160ee Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 22 Aug 2023 13:51:58 -0400 Subject: [PATCH 149/474] fix settings merge --- StabilityMatrix.Avalonia/Views/SettingsPage.axaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml index 4b0100d8a..e75503338 100644 --- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml @@ -16,7 +16,7 @@ - @@ -184,8 +184,7 @@ - - + Date: Tue, 22 Aug 2023 20:16:27 -0400 Subject: [PATCH 150/474] Use null tokenizerprovider to fix tests --- StabilityMatrix.Avalonia/DesignData/DesignData.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index f48ce6a16..e1668d6b6 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -88,7 +88,6 @@ public static void Initialize() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton(); @@ -110,6 +109,7 @@ public static void Initialize() .AddSingleton(_ => null!) .AddSingleton(_ => null!) .AddSingleton(_ => null!) + .AddSingleton(_ => null!) .AddSingleton(_ => null!); // Using some default service implementations from App From f6238f24809121e85cb2a9316cae4a5c17f47adf Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 22 Aug 2023 20:16:31 -0400 Subject: [PATCH 151/474] Fix merge --- .../Views/SettingsPage.axaml | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml index cb745ba9d..255c684ff 100644 --- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml @@ -80,7 +80,65 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + SelectionMode="AlwaysSelected"> ImageSources.Count > 1; + public bool CanNavigateBack => SelectedImageIndex > 0; public bool CanNavigateForward => SelectedImageIndex < ImageSources.Count - 1; @@ -73,7 +75,7 @@ public void SetPreviewImage(byte[] imageBytes) { using var stream = new MemoryStream(imageBytes); - using var bitmap = new Bitmap(stream); + var bitmap = new Bitmap(stream); var currentImage = PreviewImage; @@ -110,6 +112,7 @@ or NotifyCollectionChangedAction.Reset } OnPropertyChanged(nameof(CanNavigateBack)); OnPropertyChanged(nameof(CanNavigateForward)); + OnPropertyChanged(nameof(HasMultipleImages)); } } } From f2509ceff3615fc5e27106f8c12ed4edac7c4d3d Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 1 Sep 2023 18:33:24 -0400 Subject: [PATCH 205/474] Add refiner steps to sampler card, switch to localized strings --- .../Controls/SamplerCard.axaml | 60 +++++++++++++----- .../DesignData/DesignData.cs | 18 +++--- .../Languages/Resources.Designer.cs | 63 +++++++++++++++++++ .../Languages/Resources.resx | 21 +++++++ .../Inference/SamplerCardViewModel.cs | 16 +++-- .../Models/Api/Comfy/ComfySampler.cs | 3 + .../Models/Api/Comfy/ComfyUpscaler.cs | 22 ++++--- 7 files changed, 167 insertions(+), 36 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/SamplerCard.axaml b/StabilityMatrix.Avalonia/Controls/SamplerCard.axaml index 129e3dd38..dab112782 100644 --- a/StabilityMatrix.Avalonia/Controls/SamplerCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/SamplerCard.axaml @@ -4,11 +4,13 @@ xmlns:controls="using:StabilityMatrix.Avalonia.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" x:DataType="vmInference:SamplerCardViewModel" - xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"> + xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" + xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"> + @@ -21,7 +23,7 @@ - + - + + + Text="{x:Static lang:Resources.Label_Steps}" /> + - + + + IsVisible="{Binding IsRefinerStepsEnabled}" + Text="{x:Static lang:Resources.Label_StepsRefiner}" /> + + + + + Text="{x:Static lang:Resources.Label_DenoisingStrength}"/> + Text="{x:Static lang:Resources.Label_Width}"/> + Text="{x:Static lang:Resources.Label_Height}"/> DialogFactory.Get(vm => { - vm.Steps = 20; - vm.CfgScale = 7; - vm.SelectedSampler = new ComfySampler("euler"); - vm.IsDimensionsEnabled = false; - vm.IsCfgScaleEnabled = false; - vm.IsSamplerSelectionEnabled = false; vm.IsDenoiseStrengthEnabled = true; }); + public static SamplerCardViewModel SamplerCardViewModelRefinerMode => + DialogFactory.Get(vm => + { + vm.IsCfgScaleEnabled = true; + vm.IsSamplerSelectionEnabled = true; + vm.IsDimensionsEnabled = true; + vm.IsRefinerStepsEnabled = true; + }); + public static ModelCardViewModel ModelCardViewModel => DialogFactory.Get(); public static ImageGalleryCardViewModel ImageGalleryCardViewModel => diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index fe2a553e2..0c2b94d6f 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -131,6 +131,15 @@ public static string Label_Branches { } } + /// + /// Looks up a localized string similar to CFG Scale. + /// + public static string Label_CFGScale { + get { + return ResourceManager.GetString("Label_CFGScale", resourceCulture); + } + } + /// /// Looks up a localized string similar to Comments. /// @@ -149,6 +158,15 @@ public static string Label_Deemphasis { } } + /// + /// Looks up a localized string similar to Denoising Strength. + /// + public static string Label_DenoisingStrength { + get { + return ResourceManager.GetString("Label_DenoisingStrength", resourceCulture); + } + } + /// /// Looks up a localized string similar to Drag & Drop checkpoints here to import. /// @@ -176,6 +194,15 @@ public static string Label_Emphasis { } } + /// + /// Looks up a localized string similar to Height. + /// + public static string Label_Height { + get { + return ResourceManager.GetString("Label_Height", resourceCulture); + } + } + /// /// Looks up a localized string similar to Language. /// @@ -230,6 +257,33 @@ public static string Label_ShowPixelGridAtHighZoomLevels { } } + /// + /// Looks up a localized string similar to Steps. + /// + public static string Label_Steps { + get { + return ResourceManager.GetString("Label_Steps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Steps - Base. + /// + public static string Label_StepsBase { + get { + return ResourceManager.GetString("Label_StepsBase", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Steps - Refiner. + /// + public static string Label_StepsRefiner { + get { + return ResourceManager.GetString("Label_StepsRefiner", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unknown Package. /// @@ -257,6 +311,15 @@ public static string Label_VersionType { } } + /// + /// Looks up a localized string similar to Width. + /// + public static string Label_Width { + get { + return ResourceManager.GetString("Label_Width", resourceCulture); + } + } + /// /// Looks up a localized string similar to Relaunch is required for new language option to take effect. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index 0a7978607..9740557ac 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -87,4 +87,25 @@ Show pixel grid at high zoom levels + + Steps + + + Steps - Base + + + Steps - Refiner + + + CFG Scale + + + Denoising Strength + + + Width + + + Height + diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs index fbe850b16..17448c131 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs @@ -1,9 +1,6 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using CommunityToolkit.Mvvm.ComponentModel; using StabilityMatrix.Avalonia.Controls; -using StabilityMatrix.Avalonia.Models; -using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; @@ -14,9 +11,15 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(SamplerCard))] public partial class SamplerCardViewModel : LoadableViewModelBase { + [ObservableProperty] + private bool isRefinerStepsEnabled; + [ObservableProperty] private int steps = 20; + [ObservableProperty] + private int refinerSteps = 10; + [ObservableProperty] private bool isDenoiseStrengthEnabled; @@ -50,6 +53,7 @@ public partial class SamplerCardViewModel : LoadableViewModelBase [ObservableProperty] private ComfyScheduler? selectedScheduler = new ComfyScheduler("normal"); + [JsonIgnore] public IInferenceClientManager ClientManager { get; } public SamplerCardViewModel(IInferenceClientManager clientManager) @@ -57,7 +61,7 @@ public SamplerCardViewModel(IInferenceClientManager clientManager) ClientManager = clientManager; } - /// + /*/// public override void LoadStateFromJsonObject(JsonObject state) { var model = DeserializeModel(state); @@ -94,5 +98,5 @@ public override JsonObject SaveStateToJsonObject() SelectedSampler = SelectedSampler?.Name } ); - } + }*/ } diff --git a/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs b/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs index d244e3241..2bec42541 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs @@ -1,7 +1,10 @@ using System.Collections.Immutable; +using System.Text.Json.Serialization; +using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.Api.Comfy; +[JsonConverter(typeof(StringJsonConverter))] public readonly record struct ComfySampler(string Name) { private static Dictionary ConvertDict { get; } = diff --git a/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscaler.cs b/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscaler.cs index a975c7338..ed42addfe 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscaler.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscaler.cs @@ -1,7 +1,10 @@ using System.Collections.Immutable; +using System.Text.Json.Serialization; +using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.Api.Comfy; +[JsonConverter(typeof(StringJsonConverter))] public readonly record struct ComfyUpscaler(string Name, ComfyUpscalerType Type) { private static Dictionary ConvertDict { get; } = @@ -13,9 +16,11 @@ public readonly record struct ComfyUpscaler(string Name, ComfyUpscalerType Type) ["bicubic"] = "Bicubic", ["bislerp"] = "Bislerp", }; - + public static IReadOnlyList Defaults { get; } = - ConvertDict.Keys.Select(k => new ComfyUpscaler(k, ComfyUpscalerType.Latent)).ToImmutableArray(); + ConvertDict.Keys + .Select(k => new ComfyUpscaler(k, ComfyUpscalerType.Latent)) + .ToImmutableArray(); public string DisplayType { @@ -30,7 +35,7 @@ public string DisplayType }; } } - + public string DisplayName { get @@ -45,11 +50,11 @@ public string DisplayName // Remove file extensions return Path.GetFileNameWithoutExtension(Name); } - + return Name; } } - + public string ShortDisplayName { get @@ -59,7 +64,7 @@ public string ShortDisplayName // Remove file extensions return Path.GetFileNameWithoutExtension(Name); } - + return DisplayName; } } @@ -73,9 +78,10 @@ public bool Equals(ComfyUpscaler x, ComfyUpscaler y) public int GetHashCode(ComfyUpscaler obj) { - return HashCode.Combine(obj.Name, (int) obj.Type); + return HashCode.Combine(obj.Name, (int)obj.Type); } } - public static IEqualityComparer Comparer { get; } = new NameTypeEqualityComparer(); + public static IEqualityComparer Comparer { get; } = + new NameTypeEqualityComparer(); } From 40ea8f8b817b48ff55d7e963cb0728d633390621 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 2 Sep 2023 16:06:51 -0400 Subject: [PATCH 206/474] Update CodeTimer with optional postfix --- StabilityMatrix.Core/Helper/CodeTimer.cs | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/StabilityMatrix.Core/Helper/CodeTimer.cs b/StabilityMatrix.Core/Helper/CodeTimer.cs index 8f67da65e..348e57c5e 100644 --- a/StabilityMatrix.Core/Helper/CodeTimer.cs +++ b/StabilityMatrix.Core/Helper/CodeTimer.cs @@ -7,29 +7,29 @@ namespace StabilityMatrix.Core.Helper; public class CodeTimer : IDisposable { private static readonly Stack RunningTimers = new(); - + private readonly string name; private readonly Stopwatch stopwatch; private CodeTimer? ParentTimer { get; } private List SubTimers { get; } = new(); - - public CodeTimer([CallerMemberName] string? name = null) + + public CodeTimer(string postFix = "", [CallerMemberName] string callerName = "") { - this.name = name ?? ""; + name = $"{callerName}" + (string.IsNullOrEmpty(postFix) ? "" : $" ({postFix})"); stopwatch = Stopwatch.StartNew(); - + // Set parent as the top of the stack if (RunningTimers.TryPeek(out var timer)) { ParentTimer = timer; timer.SubTimers.Add(this); } - + // Add ourselves to the stack RunningTimers.Push(this); } - + /// /// Formats a TimeSpan into a string. Chooses the most appropriate unit of time. /// @@ -64,22 +64,22 @@ private static void OutputDebug(string message) private string GetResult() { var builder = new StringBuilder(); - + builder.AppendLine($"{name}: took {FormatTime(stopwatch.Elapsed)}"); - + foreach (var timer in SubTimers) { // For each sub timer layer, add a `|-` prefix builder.AppendLine($"|- {timer.GetResult()}"); } - + return builder.ToString(); } - + public void Dispose() { stopwatch.Stop(); - + // Remove ourselves from the stack if (RunningTimers.TryPop(out var timer)) { @@ -92,14 +92,14 @@ public void Dispose() { throw new InvalidOperationException("Timer stack is empty"); } - + // If we're a root timer, output all results if (ParentTimer is null) { OutputDebug(GetResult()); SubTimers.Clear(); } - + GC.SuppressFinalize(this); } } From 5f9e2fbc4187f9897143672969562e9a9bd75416 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 3 Sep 2023 00:38:37 -0400 Subject: [PATCH 207/474] Implement Refiner support --- .../Controls/ModelCard.axaml | 41 +- .../Controls/SamplerCard.axaml | 2 +- .../Controls/StackExpander.axaml.cs | 7 +- .../DesignData/DesignData.cs | 7 +- .../Extensions/ComfyNodeBuilderExtensions.cs | 277 +++++++++++++ .../Extensions/RelayCommandExtensions.cs | 87 +++- .../Languages/Resources.Designer.cs | 27 ++ .../Languages/Resources.resx | 9 + .../Models/Inference/Prompt.cs | 38 ++ .../InferenceImageUpscaleViewModel.cs | 7 +- .../InferenceTextToImageViewModel.cs | 379 +++++++----------- .../Inference/ModelCardViewModel.cs | 46 ++- .../Inference/InferenceTextToImageView.axaml | 10 +- StabilityMatrix.Core/Inference/ComfyClient.cs | 90 +++-- .../Api/Comfy/Nodes/ComfyNodeBuilder.cs | 239 ++++++++++- .../ComfyWebSocketExecutingData.cs | 6 +- .../Models/Database/LocalModelFile.cs | 36 +- 17 files changed, 950 insertions(+), 358 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs diff --git a/StabilityMatrix.Avalonia/Controls/ModelCard.axaml b/StabilityMatrix.Avalonia/Controls/ModelCard.axaml index acc4fd558..a3d19de98 100644 --- a/StabilityMatrix.Avalonia/Controls/ModelCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/ModelCard.axaml @@ -5,6 +5,7 @@ xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" + xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" x:DataType="inference:ModelCardViewModel"> @@ -18,15 +19,15 @@ - - + + + Text="{x:Static lang:Resources.Label_Model}" /> + + Text="{x:Static lang:Resources.Label_VAE}"/> - + + Text="{x:Static lang:Resources.Label_Refiner}" /> + + + + + SpacingProperty = AvaloniaProperty.Register( - "Spacing", 8); + public static readonly StyledProperty SpacingProperty = AvaloniaProperty.Register< + StackCard, + int + >("Spacing", 8); public int Spacing { diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 948d8f0a0..c8abc90c2 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -440,7 +440,12 @@ public static PackageManagerViewModel PackageManagerViewModel }); public static InferenceTextToImageViewModel InferenceTextToImageViewModel => - DialogFactory.Get(); + DialogFactory.Get(vm => + { + vm.OutputProgress.Value = 10; + vm.OutputProgress.Maximum = 30; + vm.OutputProgress.Text = "Sampler 10/30"; + }); public static InferenceImageUpscaleViewModel InferenceImageUpscaleViewModel => DialogFactory.Get(); diff --git a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs new file mode 100644 index 000000000..451cab70e --- /dev/null +++ b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Drawing; +using StabilityMatrix.Avalonia.ViewModels.Inference; +using StabilityMatrix.Core.Models.Api.Comfy.Nodes; +using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.Extensions; + +public static class ComfyNodeBuilderExtensions +{ + public static void SetupLatentSource( + this ComfyNodeBuilder builder, + BatchSizeCardViewModel batchSizeCardViewModel, + SamplerCardViewModel samplerCardViewModel + ) + { + var emptyLatent = builder.Nodes.AddNamedNode( + ComfyNodeBuilder.EmptyLatentImage( + "EmptyLatentImage", + batchSizeCardViewModel.BatchSize, + samplerCardViewModel.Width, + samplerCardViewModel.Height + ) + ); + + builder.Connections.Latent = emptyLatent.Output; + builder.Connections.LatentSize = new Size( + samplerCardViewModel.Width, + samplerCardViewModel.Height + ); + } + + public static void SetupBaseSampler( + this ComfyNodeBuilder builder, + SeedCardViewModel seedCardViewModel, + SamplerCardViewModel samplerCardViewModel, + PromptCardViewModel promptCardViewModel, + ModelCardViewModel modelCardViewModel, + IModelIndexService modelIndexService + ) + { + // Load base checkpoint + var checkpointLoader = builder.Nodes.AddNamedNode( + ComfyNodeBuilder.CheckpointLoaderSimple( + "CheckpointLoader", + modelCardViewModel.SelectedModelName + ?? throw new NullReferenceException("Model not selected") + ) + ); + + builder.Connections.BaseVAE = checkpointLoader.GetOutput(2); + + // Define model and clip for connections for chaining + var modelSource = checkpointLoader.GetOutput(0); + var clipSource = checkpointLoader.GetOutput(1); + + // Load prompts + var prompt = promptCardViewModel.GetPrompt(); + prompt.Process(); + var negativePrompt = promptCardViewModel.GetNegativePrompt(); + negativePrompt.Process(); + + // If need to load loras, add a group + if (prompt.ExtraNetworks.Count > 0) + { + // Convert to local file names + var lorasGroup = builder.Group_LoraLoadMany( + "Loras", + modelSource, + clipSource, + prompt.GetExtraNetworksAsLocalModels(modelIndexService) + ); + + // Set as source + modelSource = lorasGroup.Output1; + clipSource = lorasGroup.Output2; + } + builder.Connections.BaseModel = modelSource; + + // Clips + var positiveClip = builder.Nodes.AddNamedNode( + ComfyNodeBuilder.ClipTextEncode("PositiveCLIP", clipSource, prompt.ProcessedText) + ); + var negativeClip = builder.Nodes.AddNamedNode( + ComfyNodeBuilder.ClipTextEncode( + "NegativeCLIP", + clipSource, + negativePrompt.ProcessedText + ) + ); + builder.Connections.BaseConditioning = positiveClip.Output; + builder.Connections.BaseNegativeConditioning = negativeClip.Output; + + // Add base sampler (without refiner) + if ( + modelCardViewModel + is not { IsRefinerSelectionEnabled: true, SelectedRefiner.IsDefault: false } + ) + { + var sampler = builder.Nodes.AddNamedNode( + ComfyNodeBuilder.KSampler( + "Sampler", + modelSource, + Convert.ToUInt64(seedCardViewModel.Seed), + samplerCardViewModel.Steps, + samplerCardViewModel.CfgScale, + samplerCardViewModel.SelectedSampler + ?? throw new ValidationException("Sampler not selected"), + samplerCardViewModel.SelectedScheduler + ?? throw new ValidationException("Sampler not selected"), + positiveClip.Output, + negativeClip.Output, + builder.Connections.Latent + ?? throw new ValidationException("Latent source not set"), + samplerCardViewModel.DenoiseStrength + ) + ); + builder.Connections.Latent = sampler.Output; + } + // Add base sampler (with refiner) + else + { + // Total steps is the sum of the base and refiner steps + var totalSteps = samplerCardViewModel.Steps + samplerCardViewModel.RefinerSteps; + + var sampler = builder.Nodes.AddNamedNode( + ComfyNodeBuilder.KSamplerAdvanced( + "Sampler", + modelSource, + true, + Convert.ToUInt64(seedCardViewModel.Seed), + totalSteps, + samplerCardViewModel.CfgScale, + samplerCardViewModel.SelectedSampler + ?? throw new ValidationException("Sampler not selected"), + samplerCardViewModel.SelectedScheduler + ?? throw new ValidationException("Sampler not selected"), + positiveClip.Output, + negativeClip.Output, + builder.Connections.Latent + ?? throw new ValidationException("Latent source not set"), + 0, + samplerCardViewModel.Steps, + true + ) + ); + builder.Connections.Latent = sampler.Output; + } + } + + public static void SetupRefinerSampler( + this ComfyNodeBuilder builder, + SeedCardViewModel seedCardViewModel, + SamplerCardViewModel samplerCardViewModel, + PromptCardViewModel promptCardViewModel, + ModelCardViewModel modelCardViewModel, + IModelIndexService modelIndexService + ) + { + // Load refiner checkpoint + var checkpointLoader = builder.Nodes.AddNamedNode( + ComfyNodeBuilder.CheckpointLoaderSimple( + "Refiner_CheckpointLoader", + modelCardViewModel.SelectedRefiner?.FileName + ?? throw new NullReferenceException("Model not selected") + ) + ); + + builder.Connections.RefinerVAE = checkpointLoader.GetOutput(2); + + // Define model and clip for connections for chaining + var modelSource = checkpointLoader.GetOutput(0); + var clipSource = checkpointLoader.GetOutput(1); + + // Load prompts + var prompt = promptCardViewModel.GetPrompt(); + prompt.Process(); + var negativePrompt = promptCardViewModel.GetNegativePrompt(); + negativePrompt.Process(); + + // If need to load loras, add a group + if (prompt.ExtraNetworks.Count > 0) + { + // Convert to local file names + var lorasGroup = builder.Group_LoraLoadMany( + "Refiner_Loras", + modelSource, + clipSource, + prompt.GetExtraNetworksAsLocalModels(modelIndexService) + ); + + // Set as source + modelSource = lorasGroup.Output1; + clipSource = lorasGroup.Output2; + } + builder.Connections.RefinerModel = modelSource; + + // Clips + var positiveClip = builder.Nodes.AddNamedNode( + ComfyNodeBuilder.ClipTextEncode( + "Refiner_PositiveCLIP", + clipSource, + prompt.ProcessedText + ) + ); + var negativeClip = builder.Nodes.AddNamedNode( + ComfyNodeBuilder.ClipTextEncode( + "Refiner_NegativeCLIP", + clipSource, + negativePrompt.ProcessedText + ) + ); + builder.Connections.RefinerConditioning = positiveClip.Output; + builder.Connections.RefinerNegativeConditioning = negativeClip.Output; + + // Add refiner sampler + + // Total steps is the sum of the base and refiner steps + var totalSteps = samplerCardViewModel.Steps + samplerCardViewModel.RefinerSteps; + + var sampler = builder.Nodes.AddNamedNode( + ComfyNodeBuilder.KSamplerAdvanced( + "Refiner_Sampler", + modelSource, + false, + Convert.ToUInt64(seedCardViewModel.Seed), + totalSteps, + samplerCardViewModel.CfgScale, + samplerCardViewModel.SelectedSampler + ?? throw new ValidationException("Sampler not selected"), + samplerCardViewModel.SelectedScheduler + ?? throw new ValidationException("Sampler not selected"), + positiveClip.Output, + negativeClip.Output, + builder.Connections.Latent + ?? throw new ValidationException("Latent source not set"), + samplerCardViewModel.Steps, + totalSteps, + false + ) + ); + builder.Connections.Latent = sampler.Output; + } + + public static string SetupOutputImage(this ComfyNodeBuilder builder) + { + // Do VAE decoding if not done already + if (builder.Connections.Image is null) + { + var vaeDecoder = builder.Nodes.AddNamedNode( + ComfyNodeBuilder.VAEDecode( + "VAEDecode", + builder.Connections.Latent!, + builder.Connections.GetRefinerOrBaseVAE() + ) + ); + builder.Connections.Image = vaeDecoder.Output; + } + + var saveImage = builder.Nodes.AddNamedNode( + new NamedComfyNode("SaveImage") + { + ClassType = "SaveImage", + Inputs = new Dictionary + { + ["filename_prefix"] = "SM-Inference", + ["images"] = builder.Connections.Image + } + } + ); + + return saveImage.Name; + } +} diff --git a/StabilityMatrix.Avalonia/Extensions/RelayCommandExtensions.cs b/StabilityMatrix.Avalonia/Extensions/RelayCommandExtensions.cs index ab8d3c6bc..bbbdf8c47 100644 --- a/StabilityMatrix.Avalonia/Extensions/RelayCommandExtensions.cs +++ b/StabilityMatrix.Avalonia/Extensions/RelayCommandExtensions.cs @@ -12,27 +12,62 @@ public static class RelayCommandExtensions { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private static void VerifyFlowExceptionsToTaskSchedulerEnabled(IAsyncRelayCommand command) + { + // Check that the FlowExceptionsToTaskScheduler flag is set + var options = command.GetPrivateField("options"); + + if (!options.HasFlag(AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler)) + { + throw new ArgumentException( + "The command must be created with the FlowExceptionsToTaskScheduler option enabled" + ); + } + } + /// /// Attach an error handler to the command that will invoke the given action when an exception occurs. /// /// The command to attach the error handler to. /// The action to invoke when an exception occurs. /// Thrown if the command was not created with the FlowExceptionsToTaskScheduler option enabled. - public static T WithErrorHandler(this T command, Action onError) where T : IAsyncRelayCommand + public static T WithErrorHandler(this T command, Action onError) + where T : IAsyncRelayCommand { - if (command is AsyncRelayCommand relayCommand) - { - // Check that the FlowExceptionsToTaskScheduler flag is set - var options = relayCommand.GetPrivateField("options"); + VerifyFlowExceptionsToTaskSchedulerEnabled(command); - if (!options.HasFlag(AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler)) + command.PropertyChanged += (sender, e) => + { + if (sender is not IAsyncRelayCommand senderCommand) { - throw new ArgumentException( - "The command must be created with the FlowExceptionsToTaskScheduler option enabled" - ); + return; } - } + // On ExecutionTask updates, check if there is an exception + if ( + e.PropertyName == nameof(AsyncRelayCommand.ExecutionTask) + && senderCommand.ExecutionTask is { Exception: { } exception } + ) + { + onError(exception); + } + }; + + return command; + } + /// + /// Conditionally attach an error handler to the command that will invoke the given action when an exception occurs. + /// The error is propagated if not in DEBUG mode. + /// + /// The command to attach the error handler to. + /// The action to invoke when an exception occurs. + /// Thrown if the command was not created with the FlowExceptionsToTaskScheduler option enabled. + public static T WithConditionalErrorHandler(this T command, Action onError) + where T : IAsyncRelayCommand + { + VerifyFlowExceptionsToTaskSchedulerEnabled(command); + +#if DEBUG command.PropertyChanged += (sender, e) => { if (sender is not IAsyncRelayCommand senderCommand) @@ -45,11 +80,14 @@ public static T WithErrorHandler(this T command, Action onError) w && senderCommand.ExecutionTask is { Exception: { } exception } ) { - onError(exception); + throw exception; } }; return command; +#else + return WithErrorHandler(command, onError); +#endif } /// @@ -63,7 +101,8 @@ public static T WithNotificationErrorHandler( this T command, INotificationService notificationService, LogLevel? logLevel = default - ) where T : IAsyncRelayCommand + ) + where T : IAsyncRelayCommand { logLevel ??= LogLevel.Error; @@ -73,4 +112,28 @@ public static T WithNotificationErrorHandler( notificationService.ShowPersistent("Error", $"[{e.GetType().Name}] {e.Message}"); }); } + + /// + /// Attach an error handler to the command that will log the error and show a notification. + /// The error is propagated if not in DEBUG mode. + /// + /// The command to attach the error handler to. + /// The notification service to use to show the notification. + /// The log level to use when logging the error. Defaults to LogLevel.Error + /// Thrown if the command was not created with the FlowExceptionsToTaskScheduler option enabled. + public static T WithConditionalNotificationErrorHandler( + this T command, + INotificationService notificationService, + LogLevel? logLevel = default + ) + where T : IAsyncRelayCommand + { + logLevel ??= LogLevel.Error; + + return command.WithConditionalErrorHandler(e => + { + Logger.Log(logLevel, e, "Error executing command"); + notificationService.ShowPersistent("Error", $"[{e.GetType().Name}] {e.Message}"); + }); + } } diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index 0c2b94d6f..15657c730 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -212,6 +212,15 @@ public static string Label_Language { } } + /// + /// Looks up a localized string similar to Model. + /// + public static string Label_Model { + get { + return ResourceManager.GetString("Label_Model", resourceCulture); + } + } + /// /// Looks up a localized string similar to Networks (Lora / LyCORIS). /// @@ -230,6 +239,15 @@ public static string Label_PackageType { } } + /// + /// Looks up a localized string similar to Refiner. + /// + public static string Label_Refiner { + get { + return ResourceManager.GetString("Label_Refiner", resourceCulture); + } + } + /// /// Looks up a localized string similar to Relaunch Required. /// @@ -293,6 +311,15 @@ public static string Label_UnknownPackage { } } + /// + /// Looks up a localized string similar to VAE. + /// + public static string Label_VAE { + get { + return ResourceManager.GetString("Label_VAE", resourceCulture); + } + } + /// /// Looks up a localized string similar to Version. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index 9740557ac..60baaaaf9 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -108,4 +108,13 @@ Height + + Refiner + + + VAE + + + Model + diff --git a/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs b/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs index 03faf9129..0389f4e51 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs @@ -10,6 +10,7 @@ using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Tokens; using StabilityMatrix.Core.Services; using TextMateSharp.Grammars; @@ -56,6 +57,43 @@ public void ValidateExtraNetworks(IModelIndexService indexService) GetExtraNetworks(indexService); } + /// + /// Get ExtraNetworks as local model files and weights. + /// + public IEnumerable<( + LocalModelFile ModelFile, + double? ModelWeight, + double? ClipWeight + )> GetExtraNetworksAsLocalModels(IModelIndexService indexService) + { + if (ExtraNetworks is null) + { + throw new InvalidOperationException( + "Prompt must be processed before calling GetExtraNetworksAsLocalModels" + ); + } + + foreach (var network in ExtraNetworks) + { + var sharedFolderType = network.Type.ConvertTo(); + + if (!indexService.ModelIndex.TryGetValue(sharedFolderType, out var modelList)) + { + throw new ApplicationException($"Model {network.Name} does not exist in index"); + } + + var localModel = modelList.FirstOrDefault( + m => m.FileNameWithoutExtension == network.Name + ); + if (localModel == null) + { + throw new ApplicationException($"Model {network.Name} does not exist in index"); + } + + yield return (localModel, network.ModelWeight, network.ClipWeight); + } + } + private int GetSafeEndIndex(int index) { return Math.Min(index, RawText.Length); diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs index 054420ec6..ba077b42f 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs @@ -99,12 +99,9 @@ IModelIndexService modelIndexService { using var _ = new CodeTimer(); - var nodes = new NodeDictionary(); - var builder = new ComfyNodeBuilder(nodes); + var builder = new ComfyNodeBuilder(); - nodes.NormalizeConnectionTypes(); - - return (nodes, new[] { "?" }); + return (builder.ToNodeDictionary(), new[] { "?" }); } private void OnProgressUpdateReceived(object? sender, ComfyProgressUpdateEventArgs args) diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index bebb56323..ddbf655e5 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Drawing; using System.IO; using System.Linq; +using System.Reactive.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -12,9 +15,11 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DynamicData.Binding; using NLog; using Refit; using SkiaSharp; +using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; @@ -31,6 +36,7 @@ using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; using StabilityMatrix.Core.Services; using InferenceTextToImageView = StabilityMatrix.Avalonia.Views.Inference.InferenceTextToImageView; +using Size = System.Drawing.Size; #pragma warning disable CS0657 // Not a valid attribute location for this declaration @@ -45,21 +51,50 @@ public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase private readonly ServiceManager vmFactory; private readonly IModelIndexService modelIndexService; + [JsonIgnore] public IInferenceClientManager ClientManager { get; } + [JsonIgnore] + public StackCardViewModel StackCardViewModel { get; } + + [JsonPropertyName("Model")] + public ModelCardViewModel ModelCardViewModel { get; } + + [JsonPropertyName("Sampler")] + public SamplerCardViewModel SamplerCardViewModel { get; } + + [JsonPropertyName("ImageGallery")] public ImageGalleryCardViewModel ImageGalleryCardViewModel { get; } + + [JsonPropertyName("Prompt")] public PromptCardViewModel PromptCardViewModel { get; } - public StackCardViewModel StackCardViewModel { get; } - public UpscalerCardViewModel UpscalerCardViewModel => - StackCardViewModel.GetCard().GetCard(); + [JsonPropertyName("Upscaler")] + public UpscalerCardViewModel UpscalerCardViewModel { get; } + + [JsonPropertyName("HiresSampler")] + public SamplerCardViewModel HiresSamplerCardViewModel { get; } + + [JsonPropertyName("HiresUpscaler")] + public UpscalerCardViewModel HiresUpscalerCardViewModel { get; } + + [JsonPropertyName("BatchSize")] + public BatchSizeCardViewModel BatchSizeCardViewModel { get; } - public SamplerCardViewModel HiresSamplerCardViewModel => - StackCardViewModel.GetCard().GetCard(); + [JsonPropertyName("Seed")] + public SeedCardViewModel SeedCardViewModel { get; } - public bool IsHiresFixEnabled => StackCardViewModel.GetCard().IsEnabled; + public bool IsHiresFixEnabled + { + get => StackCardViewModel.GetCard().IsEnabled; + set => StackCardViewModel.GetCard().IsEnabled = value; + } - public bool IsUpscaleEnabled => StackCardViewModel.GetCard(1).IsEnabled; + public bool IsUpscaleEnabled + { + get => StackCardViewModel.GetCard(1).IsEnabled; + set => StackCardViewModel.GetCard(1).IsEnabled = value; + } [JsonIgnore] public ProgressViewModel OutputProgress { get; } = new(); @@ -82,27 +117,36 @@ IModelIndexService modelIndexService // Get sub view models from service manager - var seedCard = vmFactory.Get(); - seedCard.GenerateNewSeed(); + SeedCardViewModel = vmFactory.Get(); + SeedCardViewModel.GenerateNewSeed(); + + ModelCardViewModel = vmFactory.Get(); + + SamplerCardViewModel = vmFactory.Get(samplerCard => + { + samplerCard.IsDimensionsEnabled = true; + samplerCard.IsCfgScaleEnabled = true; + samplerCard.IsSamplerSelectionEnabled = true; + samplerCard.IsSchedulerSelectionEnabled = true; + }); ImageGalleryCardViewModel = vmFactory.Get(); PromptCardViewModel = vmFactory.Get(); + HiresSamplerCardViewModel = vmFactory.Get(samplerCard => + { + samplerCard.IsDenoiseStrengthEnabled = true; + }); + HiresUpscalerCardViewModel = vmFactory.Get(); + UpscalerCardViewModel = vmFactory.Get(); + BatchSizeCardViewModel = vmFactory.Get(); StackCardViewModel = vmFactory.Get(); StackCardViewModel.AddCards( new LoadableViewModelBase[] { - // Model Card - vmFactory.Get(), - // Sampler - vmFactory.Get(samplerCard => - { - samplerCard.IsDimensionsEnabled = true; - samplerCard.IsCfgScaleEnabled = true; - samplerCard.IsSamplerSelectionEnabled = true; - samplerCard.IsSchedulerSelectionEnabled = true; - }), + ModelCardViewModel, + SamplerCardViewModel, // Hires Fix vmFactory.Get(stackExpander => { @@ -110,35 +154,31 @@ IModelIndexService modelIndexService stackExpander.AddCards( new LoadableViewModelBase[] { - // Hires Fix Upscaler - vmFactory.Get(), - // Hires Fix Sampler - vmFactory.Get(samplerCard => - { - samplerCard.IsDenoiseStrengthEnabled = true; - }) + HiresUpscalerCardViewModel, + HiresSamplerCardViewModel } ); }), vmFactory.Get(stackExpander => { stackExpander.Title = "Upscale"; - stackExpander.AddCards( - new LoadableViewModelBase[] - { - // Post processing upscaler - vmFactory.Get(), - } - ); + stackExpander.AddCards(new LoadableViewModelBase[] { UpscalerCardViewModel }); }), - // Seed - seedCard, - // Batch Size - vmFactory.Get(), + SeedCardViewModel, + BatchSizeCardViewModel, } ); - // GenerateImageCommand.WithNotificationErrorHandler(notificationService); + // When refiner is provided in model card, enable for sampler + ModelCardViewModel + .WhenPropertyChanged(x => x.IsRefinerSelectionEnabled) + .Subscribe(e => + { + SamplerCardViewModel.IsRefinerStepsEnabled = + e.Sender is { IsRefinerSelectionEnabled: true, SelectedRefiner: not null }; + }); + + GenerateImageCommand.WithConditionalNotificationErrorHandler(notificationService); } private (NodeDictionary prompt, string[] outputs) BuildPrompt( @@ -147,178 +187,63 @@ IModelIndexService modelIndexService { using var _ = new CodeTimer(); - var samplerCard = StackCardViewModel.GetCard(); - var batchCard = StackCardViewModel.GetCard(); - var modelCard = StackCardViewModel.GetCard(); - var seedCard = StackCardViewModel.GetCard(); + var builder = new ComfyNodeBuilder(); + var nodes = builder.Nodes; - var nodes = new NodeDictionary(); - var builder = new ComfyNodeBuilder(nodes); + // Setup empty latent + builder.SetupLatentSource(BatchSizeCardViewModel, SamplerCardViewModel); - var emptyLatentImage = nodes.AddNamedNode( - new NamedComfyNode("EmptyLatentImage") - { - ClassType = "EmptyLatentImage", - Inputs = new Dictionary - { - ["batch_size"] = batchCard.BatchSize, - ["height"] = samplerCard.Height, - ["width"] = samplerCard.Width, - } - } - ); - - var checkpointLoader = nodes.AddNamedNode( - new NamedComfyNode("CheckpointLoader") - { - ClassType = "CheckpointLoaderSimple", - Inputs = new Dictionary - { - ["ckpt_name"] = modelCard.SelectedModelName - } - } + // Setup base stage + builder.SetupBaseSampler( + SeedCardViewModel, + SamplerCardViewModel, + PromptCardViewModel, + ModelCardViewModel, + modelIndexService ); - // Global connections for chaining - var modelSource = checkpointLoader.GetOutput(0); - var clipSource = checkpointLoader.GetOutput(1); - var vaeSource = checkpointLoader.GetOutput(2); - - // Use custom VAE if enabled - if (modelCard is { IsVaeSelectionEnabled: true, SelectedVae.IsDefault: false }) + // Setup refiner stage if enabled + if ( + ModelCardViewModel is + { IsRefinerSelectionEnabled: true, SelectedRefiner.IsDefault: false } + ) { - // Add a loader - var vaeLoader = nodes.AddNamedNode( - ComfyNodeBuilder.VAELoader("VAELoader", modelCard.SelectedVae.FileName) + builder.SetupRefinerSampler( + SeedCardViewModel, + SamplerCardViewModel, + PromptCardViewModel, + ModelCardViewModel, + modelIndexService ); - - // Set as source - vaeSource = vaeLoader.Output; } - // See if we need to load loras - var prompt = PromptCardViewModel.GetPrompt(); - prompt.Process(); - var negativePrompt = PromptCardViewModel.GetNegativePrompt(); - negativePrompt.Process(); - - // If need to load loras, add a group - if (prompt.ExtraNetworks.Count > 0) + // Override with custom VAE if enabled + if (ModelCardViewModel is { IsVaeSelectionEnabled: true, SelectedVae.IsDefault: false }) { - // Convert to local file names - var loras = prompt.ExtraNetworks.Select(n => - { - var localLoras = modelIndexService.ModelIndex.GetOrAdd(SharedFolderType.Lora); - var localLora = localLoras.FirstOrDefault( - m => - m.FileName == n.Name - || Path.GetFileNameWithoutExtension(m.FileName) == n.Name - ); - - if (localLora is null) - { - throw new ApplicationException($"Lora model {n.Name} was not found locally"); - } - - return (localLora.FileName, n.ModelWeight, n.ClipWeight); - }); - - var lorasGroup = builder.Group_LoraLoadMany("Loras", modelSource, clipSource, loras); - - // Set as source - modelSource = lorasGroup.Output1; - clipSource = lorasGroup.Output2; + builder.Connections.BaseVAE = nodes + .AddNamedNode( + ComfyNodeBuilder.VAELoader("VAELoader", ModelCardViewModel.SelectedVae.FileName) + ) + .Output; } - var positiveClip = nodes.AddNamedNode( - new NamedComfyNode("PositiveCLIP") - { - ClassType = "CLIPTextEncode", - Inputs = new Dictionary - { - ["clip"] = clipSource, - ["text"] = prompt.ProcessedText, - } - } - ); - - var negativeClip = nodes.AddNamedNode( - new NamedComfyNode("NegativeCLIP") - { - ClassType = "CLIPTextEncode", - Inputs = new Dictionary - { - ["clip"] = clipSource, - ["text"] = negativePrompt.ProcessedText, - } - } - ); - - var sampler = nodes.AddNamedNode( - ComfyNodeBuilder.KSampler( - "Sampler", - modelSource, - Convert.ToUInt64(seedCard.Seed), - samplerCard.Steps, - samplerCard.CfgScale, - samplerCard.SelectedSampler - ?? throw new ValidationException("Sampler not selected"), - samplerCard.SelectedScheduler - ?? throw new ValidationException("Sampler not selected"), - positiveClip.GetOutput(0), - negativeClip.GetOutput(0), - emptyLatentImage.GetOutput(0), - samplerCard.DenoiseStrength - ) - ); - - var lastLatent = sampler.Output; - var lastLatentWidth = samplerCard.Width; - var lastLatentHeight = samplerCard.Height; - - var vaeDecoder = nodes.AddNamedNode( - new NamedComfyNode("VAEDecoder") - { - ClassType = "VAEDecode", - Inputs = new Dictionary - { - ["samples"] = lastLatent, - ["vae"] = vaeSource - } - } - ); - - var saveImage = nodes.AddNamedNode( - new NamedComfyNode("SaveImage") - { - ClassType = "SaveImage", - Inputs = new Dictionary - { - ["filename_prefix"] = "SM-Inference", - ["images"] = vaeDecoder.GetOutput(0) - } - } - ); - // If hi-res fix is enabled, add the LatentUpscale node and another KSampler node if (overrides?.IsHiresFixEnabled ?? IsHiresFixEnabled) { - var hiresUpscalerCard = UpscalerCardViewModel; - var hiresSamplerCard = HiresSamplerCardViewModel; - // Requested upscale to this size - var hiresWidth = (int)Math.Floor(lastLatentWidth * hiresUpscalerCard.Scale); - var hiresHeight = (int)Math.Floor(lastLatentHeight * hiresUpscalerCard.Scale); + var hiresSize = builder.Connections.GetScaledLatentSize( + HiresUpscalerCardViewModel.Scale + ); LatentNodeConnection hiresLatent; // Select between latent upscale and normal upscale based on the upscale method - var selectedUpscaler = hiresUpscalerCard.SelectedUpscaler!.Value; + var selectedUpscaler = UpscalerCardViewModel.SelectedUpscaler!.Value; if (selectedUpscaler.Type == ComfyUpscalerType.None) { - // If no upscaler selected or none, just reroute the latent image - hiresLatent = sampler.Output; + // If no upscaler selected or none, just use the latent image + hiresLatent = builder.Connections.Latent!; } else { @@ -326,74 +251,65 @@ IModelIndexService modelIndexService hiresLatent = builder .Group_UpscaleToLatent( "HiresFix", - lastLatent, - vaeSource, + builder.Connections.Latent!, + builder.Connections.GetRefinerOrBaseVAE(), selectedUpscaler, - hiresWidth, - hiresHeight + hiresSize.Width, + hiresSize.Height ) .Output; } + // Use refiner model if set, or base if not var hiresSampler = nodes.AddNamedNode( ComfyNodeBuilder.KSampler( "HiresSampler", - modelSource, - Convert.ToUInt64(seedCard.Seed), - hiresSamplerCard.Steps, - hiresSamplerCard.CfgScale, + builder.Connections.GetRefinerOrBaseModel(), + Convert.ToUInt64(SeedCardViewModel.Seed), + HiresSamplerCardViewModel.Steps, + HiresSamplerCardViewModel.CfgScale, // Use hires sampler name if not null, otherwise use the normal sampler - hiresSamplerCard.SelectedSampler - ?? samplerCard.SelectedSampler + HiresSamplerCardViewModel.SelectedSampler + ?? SamplerCardViewModel.SelectedSampler ?? throw new ValidationException("Sampler not selected"), - hiresSamplerCard.SelectedScheduler - ?? samplerCard.SelectedScheduler + HiresSamplerCardViewModel.SelectedScheduler + ?? SamplerCardViewModel.SelectedScheduler ?? throw new ValidationException("Scheduler not selected"), - positiveClip.GetOutput(0), - negativeClip.GetOutput(0), + builder.Connections.GetRefinerOrBaseConditioning(), + builder.Connections.GetRefinerOrBaseNegativeConditioning(), hiresLatent, - hiresSamplerCard.DenoiseStrength + HiresSamplerCardViewModel.DenoiseStrength ) ); - // Set as last latent - lastLatent = hiresSampler.Output; - lastLatentWidth = hiresWidth; - lastLatentHeight = hiresHeight; - // Reroute the VAEDecoder's input to be from the hires sampler - vaeDecoder.Inputs["samples"] = lastLatent; + // Set as latest latent + builder.Connections.Latent = hiresSampler.Output; + builder.Connections.LatentSize = hiresSize; } // If upscale is enabled, add another upscale group if (IsUpscaleEnabled) { - var postUpscalerCard = StackCardViewModel - .GetCard(1) - .GetCard(); - - var upscaleWidth = (int)Math.Floor(lastLatentWidth * postUpscalerCard.Scale); - var upscaleHeight = (int)Math.Floor(lastLatentHeight * postUpscalerCard.Scale); + var upscaleSize = builder.Connections.GetScaledLatentSize(UpscalerCardViewModel.Scale); // Build group var postUpscaleGroup = builder.Group_UpscaleToImage( "PostUpscale", - lastLatent, - vaeSource, - postUpscalerCard.SelectedUpscaler!.Value, - upscaleWidth, - upscaleHeight + builder.Connections.Latent!, + builder.Connections.GetRefinerOrBaseVAE(), + UpscalerCardViewModel.SelectedUpscaler!.Value, + upscaleSize.Width, + upscaleSize.Height ); - // Remove the original vae decoder - nodes.Remove(vaeDecoder.Name); - - // Set as the input for save image - saveImage.Inputs["images"] = postUpscaleGroup.Output; + // Set as the image output + builder.Connections.Image = postUpscaleGroup.Output; } - nodes.NormalizeConnectionTypes(); + // Output + var outputName = builder.SetupOutputImage(); - return (nodes, new[] { saveImage.Name }); + return (builder.ToNodeDictionary(), new[] { outputName }); } private void OnProgressUpdateReceived(object? sender, ComfyProgressUpdateEventArgs args) @@ -484,9 +400,12 @@ private async Task GenerateImageImpl( ImageGalleryCardViewModel.ImageSources.Clear(); - var images = imageOutputs[outputNodeNames[0]]; - if (images is null) + if (!imageOutputs.TryGetValue(outputNodeNames[0], out var images)) + { + // No images match + notificationService.Show("No output", "Did not receive any output images"); return; + } List outputImages; // Use local file path if available, otherwise use remote URL @@ -550,7 +469,7 @@ private async Task GenerateImageImpl( } } - [RelayCommand(IncludeCancelCommand = true)] + [RelayCommand(IncludeCancelCommand = true, FlowExceptionsToTaskScheduler = true)] private async Task GenerateImage( string? options = null, CancellationToken cancellationToken = default diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs index dc7fb9851..d46c999b7 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs @@ -12,35 +12,43 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(ModelCard))] public partial class ModelCardViewModel : LoadableViewModelBase { - [ObservableProperty] + [ObservableProperty] private HybridModelFile? selectedModel; - + + [ObservableProperty] + private bool isRefinerSelectionEnabled; + + [ObservableProperty] + private HybridModelFile? selectedRefiner = HybridModelFile.None; + [ObservableProperty] private HybridModelFile? selectedVae = HybridModelFile.Default; - + [ObservableProperty] private bool isVaeSelectionEnabled; - + public string? SelectedModelName => SelectedModel?.FileName; - + public string? SelectedVaeName => SelectedVae?.FileName; - + public IInferenceClientManager ClientManager { get; } - + public ModelCardViewModel(IInferenceClientManager clientManager) { ClientManager = clientManager; } - + /// public override JsonObject SaveStateToJsonObject() { - return SerializeModel(new ModelCardModel - { - SelectedModelName = SelectedModelName, - SelectedVaeName = SelectedVaeName, - IsVaeSelectionEnabled = IsVaeSelectionEnabled - }); + return SerializeModel( + new ModelCardModel + { + SelectedModelName = SelectedModelName, + SelectedVaeName = SelectedVaeName, + IsVaeSelectionEnabled = IsVaeSelectionEnabled + } + ); } /// @@ -48,13 +56,15 @@ public override void LoadStateFromJsonObject(JsonObject state) { var model = DeserializeModel(state); - SelectedModel = model.SelectedModelName is null ? null + SelectedModel = model.SelectedModelName is null + ? null : ClientManager.Models.FirstOrDefault(x => x.FileName == model.SelectedModelName); - - SelectedVae = model.SelectedVaeName is null ? HybridModelFile.Default + + SelectedVae = model.SelectedVaeName is null + ? HybridModelFile.Default : ClientManager.VaeModels.FirstOrDefault(x => x.FileName == model.SelectedVaeName); } - + internal class ModelCardModel { public string? SelectedModelName { get; init; } diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml b/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml index 67e84b472..318fdce01 100644 --- a/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml +++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml @@ -185,6 +185,10 @@ + + + - - diff --git a/StabilityMatrix.Core/Inference/ComfyClient.cs b/StabilityMatrix.Core/Inference/ComfyClient.cs index d33513fef..6986f14b7 100644 --- a/StabilityMatrix.Core/Inference/ComfyClient.cs +++ b/StabilityMatrix.Core/Inference/ComfyClient.cs @@ -21,9 +21,9 @@ public class ComfyClient : InferenceClientBase // ReSharper disable once MemberCanBePrivate.Global public string ClientId { get; } = Guid.NewGuid().ToString(); - + public Uri BaseAddress { get; } - + /// /// Optional local path to output images. /// @@ -33,22 +33,22 @@ public class ComfyClient : InferenceClientBase /// Dictionary of ongoing prompt execution tasks /// public ConcurrentDictionary PromptTasks { get; } = new(); - + /// /// Current running prompt task /// private ComfyTask? currentPromptTask; - + /// /// Event raised when a progress update is received from the server /// public event EventHandler? ProgressUpdateReceived; - + /// /// Event raised when a status update is received from the server /// public event EventHandler? StatusUpdateReceived; - + /// /// Event raised when a executing update is received from the server /// @@ -58,7 +58,7 @@ public class ComfyClient : InferenceClientBase /// Event raised when a preview image is received from the server /// public event EventHandler? PreviewImageReceived; - + public ComfyClient(IApiFactory apiFactory, Uri baseAddress) { comfyApi = apiFactory.CreateRefitClient(baseAddress); @@ -75,7 +75,7 @@ public ComfyClient(IApiFactory apiFactory, Uri baseAddress) { ReconnectTimeout = TimeSpan.FromSeconds(30), }; - + webSocketClient.DisconnectionHappened.Subscribe( info => Logger.Info("Websocket Disconnected, ({Type})", info.Type) ); @@ -123,21 +123,17 @@ private void HandleTextMessage(string text) return; } - Logger.Trace( - "Received json message: (Type = {Type}, Data = {Data})", - json.Type, - json.Data - ); - + Logger.Trace("Received json message: (Type = {Type}, Data = {Data})", json.Type, json.Data); + if (json.Type == ComfyWebSocketResponseType.Executing) { var executingData = json.GetDataAsType(); - if (executingData is null) + if (executingData?.PromptId is null) { Logger.Warn($"Could not parse executing data {json.Data}, skipping"); return; } - + // When Node property is null, it means the prompt has finished executing // remove the task from the dictionary and set the result if (executingData.Node is null) @@ -150,7 +146,9 @@ private void HandleTextMessage(string text) } else { - Logger.Warn($"Could not find task for prompt {executingData.PromptId}, skipping"); + Logger.Warn( + $"Could not find task for prompt {executingData.PromptId}, skipping" + ); } } // Otherwise set the task's active node to the one received @@ -161,7 +159,7 @@ private void HandleTextMessage(string text) task.RunningNode = executingData.Node; } } - + ExecutingUpdateReceived?.Invoke(this, executingData); } else if (json.Type == ComfyWebSocketResponseType.Status) @@ -172,7 +170,7 @@ private void HandleTextMessage(string text) Logger.Warn($"Could not parse status data {json.Data}, skipping"); return; } - + StatusUpdateReceived?.Invoke(this, statusData); } else if (json.Type == ComfyWebSocketResponseType.Progress) @@ -183,10 +181,10 @@ private void HandleTextMessage(string text) Logger.Warn($"Could not parse progress data {json.Data}, skipping"); return; } - + // Set for the current prompt task currentPromptTask?.OnProgressUpdate(progressData); - + ProgressUpdateReceived?.Invoke(this, progressData); } else @@ -201,16 +199,16 @@ private void HandleTextMessage(string text) /// private void HandleBinaryMessage(byte[] data) { - if (data is not {Length: > 4}) + if (data is not { Length: > 4 }) { Logger.Warn("The input data is null or not long enough."); return; } - + // The first 4 bytes is int32 of the message type // Subsequent 4 bytes following is int32 of the image format // The rest is the image data - + // Read the image type from the first 4 bytes of the data. // Python's struct.pack(">I", type_num) will pack the data as a big-endian unsigned int /*var typeBytes = new byte[4]; @@ -221,11 +219,8 @@ private void HandleBinaryMessage(byte[] data) { Array.Reverse(typeBytes); }*/ - - PreviewImageReceived?.Invoke(this, new ComfyWebSocketImageData - { - ImageBytes = data[8..], - }); + + PreviewImageReceived?.Invoke(this, new ComfyWebSocketImageData { ImageBytes = data[8..], }); } public override async Task ConnectAsync(CancellationToken cancellationToken = default) @@ -247,7 +242,7 @@ public async Task QueuePromptAsync( { var request = new ComfyPromptRequest { ClientId = ClientId, Prompt = nodes }; var result = await comfyApi.PostPrompt(request, cancellationToken).ConfigureAwait(false); - + // Add task to dictionary and set it as the current task var task = new ComfyTask(result.PromptId); PromptTasks[result.PromptId] = task; @@ -255,11 +250,11 @@ public async Task QueuePromptAsync( return task; } - + public async Task InterruptPromptAsync(CancellationToken cancellationToken = default) { await comfyApi.PostInterrupt(cancellationToken).ConfigureAwait(false); - + // Set the current task to null, and remove it from the dictionary if (currentPromptTask is { } task) { @@ -271,11 +266,13 @@ public async Task InterruptPromptAsync(CancellationToken cancellationToken = def } public async Task?>> GetImagesForExecutedPromptAsync( - string promptId, CancellationToken cancellationToken = default) + string promptId, + CancellationToken cancellationToken = default + ) { // Get history for images var history = await comfyApi.GetHistory(promptId, cancellationToken).ConfigureAwait(false); - + // Get the current prompt history var current = history[promptId]; @@ -286,13 +283,18 @@ public async Task InterruptPromptAsync(CancellationToken cancellationToken = def } return dict; } - - public async Task GetImageStreamAsync(ComfyImage comfyImage, CancellationToken cancellationToken = default) + + public async Task GetImageStreamAsync( + ComfyImage comfyImage, + CancellationToken cancellationToken = default + ) { - var response = await comfyApi.GetImage(comfyImage.FileName, comfyImage.SubFolder, comfyImage.Type, cancellationToken).ConfigureAwait(false); + var response = await comfyApi + .GetImage(comfyImage.FileName, comfyImage.SubFolder, comfyImage.Type, cancellationToken) + .ConfigureAwait(false); return response; } - + /// /// Get a list of strings representing available model names /// @@ -300,7 +302,7 @@ public async Task GetImageStreamAsync(ComfyImage comfyImage, Cancellatio { return GetNodeOptionNamesAsync("CheckpointLoaderSimple", "ckpt_name", cancellationToken); } - + /// /// Get a list of strings representing available sampler names /// @@ -315,9 +317,12 @@ public async Task GetImageStreamAsync(ComfyImage comfyImage, Cancellatio public async Task?> GetNodeOptionNamesAsync( string nodeName, string optionName, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { - var response = await comfyApi.GetObjectInfo(nodeName, cancellationToken).ConfigureAwait(false); + var response = await comfyApi + .GetObjectInfo(nodeName, cancellationToken) + .ConfigureAwait(false); var info = response[nodeName]; return info.Input.GetRequiredValueAsNestedList(optionName); @@ -325,7 +330,8 @@ public async Task GetImageStreamAsync(ComfyImage comfyImage, Cancellatio protected override void Dispose(bool disposing) { - if (isDisposed) return; + if (isDisposed) + return; webSocketClient.Dispose(); isDisposed = true; } diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs index ee717c10a..7fd23e9fc 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs @@ -1,6 +1,8 @@ using System.Diagnostics.CodeAnalysis; +using System.Drawing; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; +using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Tokens; namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; @@ -11,12 +13,9 @@ namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class ComfyNodeBuilder { - private readonly NodeDictionary nodes; + public NodeDictionary Nodes { get; } = new(); - public ComfyNodeBuilder(NodeDictionary nodes) - { - this.nodes = nodes; - } + public Dictionary GlobalConnections { get; } = new(); private static string GetRandomPrefix() => Guid.NewGuid().ToString()[..8]; @@ -87,6 +86,64 @@ double denoise }; } + public static NamedComfyNode KSamplerAdvanced( + string name, + ModelNodeConnection model, + bool addNoise, + ulong noiseSeed, + int steps, + double cfg, + ComfySampler sampler, + ComfyScheduler scheduler, + ConditioningNodeConnection positive, + ConditioningNodeConnection negative, + LatentNodeConnection latentImage, + int startAtStep, + int endAtStep, + bool returnWithLeftoverNoise + ) + { + return new NamedComfyNode(name) + { + ClassType = "KSamplerAdvanced", + Inputs = new Dictionary + { + ["model"] = model.Data, + ["add_noise"] = addNoise ? "enable" : "disable", + ["noise_seed"] = noiseSeed, + ["steps"] = steps, + ["cfg"] = cfg, + ["sampler_name"] = sampler.Name, + ["scheduler"] = scheduler.Name, + ["positive"] = positive.Data, + ["negative"] = negative.Data, + ["latent_image"] = latentImage.Data, + ["start_at_step"] = startAtStep, + ["end_at_step"] = endAtStep, + ["return_with_leftover_noise"] = returnWithLeftoverNoise ? "enable" : "disable" + } + }; + } + + public static NamedComfyNode EmptyLatentImage( + string name, + int batchSize, + int height, + int width + ) + { + return new NamedComfyNode(name) + { + ClassType = "EmptyLatentImage", + Inputs = new Dictionary + { + ["batch_size"] = batchSize, + ["height"] = height, + ["width"] = width, + } + }; + } + public static NamedComfyNode ImageUpscaleWithModel( string name, UpscaleModelNodeConnection upscaleModel, @@ -171,12 +228,37 @@ double strengthClip }; } + public static NamedComfyNode CheckpointLoaderSimple( + string name, + string modelName + ) + { + return new NamedComfyNode(name) + { + ClassType = "CheckpointLoaderSimple", + Inputs = new Dictionary { ["ckpt_name"] = modelName } + }; + } + + public static NamedComfyNode ClipTextEncode( + string name, + ClipNodeConnection clip, + string text + ) + { + return new NamedComfyNode(name) + { + ClassType = "CLIPTextEncode", + Inputs = new Dictionary { ["clip"] = clip.Data, ["text"] = text } + }; + } + public ImageNodeConnection Lambda_LatentToImage( LatentNodeConnection latent, VAENodeConnection vae ) { - return nodes.AddNamedNode(VAEDecode($"{GetRandomPrefix()}_VAEDecode", latent, vae)).Output; + return Nodes.AddNamedNode(VAEDecode($"{GetRandomPrefix()}_VAEDecode", latent, vae)).Output; } public LatentNodeConnection Lambda_ImageToLatent( @@ -184,7 +266,30 @@ public LatentNodeConnection Lambda_ImageToLatent( VAENodeConnection vae ) { - return nodes.AddNamedNode(VAEEncode($"{GetRandomPrefix()}_VAEEncode", pixels, vae)).Output; + return Nodes.AddNamedNode(VAEEncode($"{GetRandomPrefix()}_VAEEncode", pixels, vae)).Output; + } + + /// + /// Get a global connection for a given type + /// + public TConnection GetConnection() + where TConnection : NodeConnectionBase + { + if (GlobalConnections.TryGetValue(typeof(TConnection), out var connection)) + { + return (TConnection)connection; + } + + throw new InvalidOperationException($"No global connection of type {typeof(TConnection)}"); + } + + /// + /// Set a global connection for a given type + /// + public void SetConnection(TConnection connection) + where TConnection : NodeConnectionBase + { + GlobalConnections[typeof(TConnection)] = connection; } /// @@ -196,11 +301,11 @@ public NamedComfyNode Group_UpscaleWithModel( ImageNodeConnection image ) { - var modelLoader = nodes.AddNamedNode( + var modelLoader = Nodes.AddNamedNode( UpscaleModelLoader($"{name}_UpscaleModelLoader", modelName) ); - var upscaler = nodes.AddNamedNode( + var upscaler = Nodes.AddNamedNode( ImageUpscaleWithModel($"{name}_ImageUpscaleWithModel", modelLoader.Output, image) ); @@ -221,7 +326,7 @@ int height { if (upscaleInfo.Type == ComfyUpscalerType.Latent) { - return nodes.AddNamedNode( + return Nodes.AddNamedNode( new NamedComfyNode($"{name}_LatentUpscale") { ClassType = "LatentUpscale", @@ -240,7 +345,7 @@ int height if (upscaleInfo.Type == ComfyUpscalerType.ESRGAN) { // Convert to image space - var samplerImage = nodes.AddNamedNode(VAEDecode($"{name}_VAEDecode", latent, vae)); + var samplerImage = Nodes.AddNamedNode(VAEDecode($"{name}_VAEDecode", latent, vae)); // Do group upscale var modelUpscaler = Group_UpscaleWithModel( @@ -250,7 +355,7 @@ int height ); // Since the model upscale is fixed to model (2x/4x), scale it again to the requested size - var resizedScaled = nodes.AddNamedNode( + var resizedScaled = Nodes.AddNamedNode( ImageScale( $"{name}_ImageScale", modelUpscaler.Output, @@ -262,7 +367,7 @@ int height ); // Convert back to latent space - return nodes.AddNamedNode(VAEEncode($"{name}_VAEEncode", resizedScaled.Output, vae)); + return Nodes.AddNamedNode(VAEEncode($"{name}_VAEEncode", resizedScaled.Output, vae)); } throw new InvalidOperationException($"Unknown upscaler type: {upscaleInfo.Type}"); @@ -282,7 +387,7 @@ int height { if (upscaleInfo.Type == ComfyUpscalerType.Latent) { - var latentUpscale = nodes.AddNamedNode( + var latentUpscale = Nodes.AddNamedNode( new NamedComfyNode($"{name}_LatentUpscale") { ClassType = "LatentUpscale", @@ -298,13 +403,13 @@ int height ); // Convert to image space - return nodes.AddNamedNode(VAEDecode($"{name}_VAEDecode", latentUpscale.Output, vae)); + return Nodes.AddNamedNode(VAEDecode($"{name}_VAEDecode", latentUpscale.Output, vae)); } if (upscaleInfo.Type == ComfyUpscalerType.ESRGAN) { // Convert to image space - var samplerImage = nodes.AddNamedNode(VAEDecode($"{name}_VAEDecode", latent, vae)); + var samplerImage = Nodes.AddNamedNode(VAEDecode($"{name}_VAEDecode", latent, vae)); // Do group upscale var modelUpscaler = Group_UpscaleWithModel( @@ -314,7 +419,7 @@ int height ); // Since the model upscale is fixed to model (2x/4x), scale it again to the requested size - var resizedScaled = nodes.AddNamedNode( + var resizedScaled = Nodes.AddNamedNode( ImageScale( $"{name}_ImageScale", modelUpscaler.Output, @@ -346,7 +451,7 @@ public NamedComfyNode Group_LoraLoadMan foreach (var (i, loraNetwork) in loras.Enumerate()) { - currentNode = nodes.AddNamedNode( + currentNode = Nodes.AddNamedNode( LoraLoader( $"{name}_LoraLoader_{i + 1}", model, @@ -364,4 +469,102 @@ public NamedComfyNode Group_LoraLoadMan return currentNode ?? throw new InvalidOperationException("No lora networks given"); } + + /// + /// Create a group node that loads multiple Lora's in series + /// + public NamedComfyNode Group_LoraLoadMany( + string name, + ModelNodeConnection model, + ClipNodeConnection clip, + IEnumerable<(LocalModelFile ModelFile, double? ModelWeight, double? ClipWeight)> loras + ) + { + NamedComfyNode? currentNode = null; + + foreach (var (i, loraNetwork) in loras.Enumerate()) + { + currentNode = Nodes.AddNamedNode( + LoraLoader( + $"{name}_LoraLoader_{i + 1}", + model, + clip, + loraNetwork.ModelFile.FileName, + loraNetwork.ModelWeight ?? 1, + loraNetwork.ClipWeight ?? 1 + ) + ); + + // Connect to previous node + model = currentNode.Output1; + clip = currentNode.Output2; + } + + return currentNode ?? throw new InvalidOperationException("No lora networks given"); + } + + /// + /// Convert to a NodeDictionary + /// + public NodeDictionary ToNodeDictionary() + { + Nodes.NormalizeConnectionTypes(); + return Nodes; + } + + public class NodeBuilderConnections + { + public ModelNodeConnection? BaseModel { get; set; } + public VAENodeConnection? BaseVAE { get; set; } + + public ConditioningNodeConnection? BaseConditioning { get; set; } + public ConditioningNodeConnection? BaseNegativeConditioning { get; set; } + + public ModelNodeConnection? RefinerModel { get; set; } + public VAENodeConnection? RefinerVAE { get; set; } + public ConditioningNodeConnection? RefinerConditioning { get; set; } + public ConditioningNodeConnection? RefinerNegativeConditioning { get; set; } + + public LatentNodeConnection? Latent { get; set; } + public Size LatentSize { get; set; } + + public ImageNodeConnection? Image { get; set; } + + /// + /// Gets the latent size scaled by a given factor + /// + public Size GetScaledLatentSize(double scale) + { + return new Size( + (int)Math.Floor(LatentSize.Width * scale), + (int)Math.Floor(LatentSize.Height * scale) + ); + } + + public VAENodeConnection GetRefinerOrBaseVAE() + { + return RefinerVAE ?? BaseVAE ?? throw new NullReferenceException("No VAE"); + } + + public ModelNodeConnection GetRefinerOrBaseModel() + { + return RefinerModel ?? BaseModel ?? throw new NullReferenceException("No Model"); + } + + public ConditioningNodeConnection GetRefinerOrBaseConditioning() + { + return RefinerConditioning + ?? BaseConditioning + ?? throw new NullReferenceException("No Conditioning"); + } + + public ConditioningNodeConnection GetRefinerOrBaseNegativeConditioning() + { + return RefinerNegativeConditioning + ?? BaseNegativeConditioning + ?? throw new NullReferenceException("No Negative Conditioning"); + } + } + + public NodeBuilderConnections Connections { get; } = new(); } diff --git a/StabilityMatrix.Core/Models/Api/Comfy/WebSocketData/ComfyWebSocketExecutingData.cs b/StabilityMatrix.Core/Models/Api/Comfy/WebSocketData/ComfyWebSocketExecutingData.cs index 98e91353a..c2e90e2e2 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/WebSocketData/ComfyWebSocketExecutingData.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/WebSocketData/ComfyWebSocketExecutingData.cs @@ -5,11 +5,11 @@ namespace StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; public class ComfyWebSocketExecutingData { [JsonPropertyName("prompt_id")] - public required string PromptId { get; set; } - + public string? PromptId { get; set; } + /// /// When this is null it indicates completed /// [JsonPropertyName("node")] - public required string? Node { get; set; } + public string? Node { get; set; } } diff --git a/StabilityMatrix.Core/Models/Database/LocalModelFile.cs b/StabilityMatrix.Core/Models/Database/LocalModelFile.cs index a97922094..dde9326f0 100644 --- a/StabilityMatrix.Core/Models/Database/LocalModelFile.cs +++ b/StabilityMatrix.Core/Models/Database/LocalModelFile.cs @@ -22,7 +22,7 @@ public class LocalModelFile /// Optional connected model information. /// public ConnectedModelInfo? ConnectedModelInfo { get; set; } - + /// /// Optional preview image relative path. /// @@ -32,23 +32,28 @@ public class LocalModelFile /// File name of the relative path. /// public string FileName => Path.GetFileName(RelativePath); - + + /// + /// File name of the relative path without extension. + /// + public string FileNameWithoutExtension => Path.GetFileNameWithoutExtension(RelativePath); + public string GetFullPath(string rootModelDirectory) { return Path.Combine(rootModelDirectory, RelativePath); } - + public string? GetPreviewImageFullPath(string rootModelDirectory) { - return PreviewImageRelativePath == null ? null + return PreviewImageRelativePath == null + ? null : Path.Combine(rootModelDirectory, PreviewImageRelativePath); } - - public string FullPathGlobal - => GetFullPath(GlobalConfig.LibraryDir.JoinDir("Models")); - - public string? PreviewImageFullPathGlobal - => GetPreviewImageFullPath(GlobalConfig.LibraryDir.JoinDir("Models")); + + public string FullPathGlobal => GetFullPath(GlobalConfig.LibraryDir.JoinDir("Models")); + + public string? PreviewImageFullPathGlobal => + GetPreviewImageFullPath(GlobalConfig.LibraryDir.JoinDir("Models")); protected bool Equals(LocalModelFile other) { @@ -58,10 +63,13 @@ protected bool Equals(LocalModelFile other) /// public override bool Equals(object? obj) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((LocalModelFile) obj); + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != this.GetType()) + return false; + return Equals((LocalModelFile)obj); } /// From bd6f5064e7068b3afdab1471036d504c50eba25e Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 3 Sep 2023 11:05:49 -0400 Subject: [PATCH 208/474] Fixed empty latent size order --- .../Extensions/ComfyNodeBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs index 451cab70e..9813c531b 100644 --- a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs +++ b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs @@ -21,8 +21,8 @@ SamplerCardViewModel samplerCardViewModel ComfyNodeBuilder.EmptyLatentImage( "EmptyLatentImage", batchSizeCardViewModel.BatchSize, - samplerCardViewModel.Width, - samplerCardViewModel.Height + samplerCardViewModel.Height, + samplerCardViewModel.Width ) ); From 3cb14cc7cce359ac8d73baa4e8bf365ba7047572 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 3 Sep 2023 18:45:03 -0400 Subject: [PATCH 209/474] Add DialogHelper.CreateJsonDialog --- StabilityMatrix.Avalonia/DialogHelper.cs | 80 ++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/StabilityMatrix.Avalonia/DialogHelper.cs b/StabilityMatrix.Avalonia/DialogHelper.cs index 589af79ac..ec6dccf5d 100644 --- a/StabilityMatrix.Avalonia/DialogHelper.cs +++ b/StabilityMatrix.Avalonia/DialogHelper.cs @@ -218,6 +218,86 @@ public static BetterContentDialog CreateApiExceptionDialog( return dialog; } + /// + /// Create a dialog for displaying json + /// + public static BetterContentDialog CreateJsonDialog( + string json, + string? title = null, + string? subTitle = null + ) + { + Dispatcher.UIThread.VerifyAccess(); + + // Setup text editor + var textEditor = new TextEditor + { + IsReadOnly = true, + WordWrap = true, + Options = { ShowColumnRulers = false, AllowScrollBelowDocument = false } + }; + var registryOptions = new RegistryOptions(ThemeName.DarkPlus); + textEditor + .InstallTextMate(registryOptions) + .SetGrammar(registryOptions.GetScopeByLanguageId("json")); + + var mainGrid = new StackPanel + { + Spacing = 8, + Margin = new Thickness(16), + Children = { textEditor } + }; + + if (subTitle is not null) + { + mainGrid.Children.Insert( + 0, + new TextBlock + { + Text = subTitle, + FontSize = 18, + FontWeight = FontWeight.Medium, + Margin = new Thickness(0, 8), + } + ); + } + + var dialog = new BetterContentDialog + { + Title = title, + Content = mainGrid, + CloseButtonText = "Close", + IsPrimaryButtonEnabled = false, + }; + + // Try to deserialize to json element + try + { + // Deserialize to json element then re-serialize to ensure indentation + var jsonElement = JsonSerializer.Deserialize( + json, + new JsonSerializerOptions + { + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip + } + ); + var formatted = JsonSerializer.Serialize( + jsonElement, + new JsonSerializerOptions { WriteIndented = true } + ); + + textEditor.Document.Text = formatted; + } + catch (JsonException) + { + // Otherwise just add the content as a code block + textEditor.Document.Text = json; + } + + return dialog; + } + public static BetterContentDialog CreatePromptErrorDialog( PromptError exception, string sourceText From 8b0b4480f534ba9528c88fb4d8ab5dcecfb2501b Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 3 Sep 2023 18:45:36 -0400 Subject: [PATCH 210/474] Fix dock panel context menu and transparency --- .../Styles/ContextMenuStyles.axaml | 74 +++++++++++++++++++ .../Styles/DockStyles.axaml | 5 +- .../Styles/ThemeColors.axaml | 9 +++ .../Inference/InferenceImageUpscaleView.axaml | 3 - 4 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Styles/ContextMenuStyles.axaml diff --git a/StabilityMatrix.Avalonia/Styles/ContextMenuStyles.axaml b/StabilityMatrix.Avalonia/Styles/ContextMenuStyles.axaml new file mode 100644 index 000000000..43bf628a0 --- /dev/null +++ b/StabilityMatrix.Avalonia/Styles/ContextMenuStyles.axaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 0,4,0,4 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Styles/DockStyles.axaml b/StabilityMatrix.Avalonia/Styles/DockStyles.axaml index ebfd869b7..6a20348d9 100644 --- a/StabilityMatrix.Avalonia/Styles/DockStyles.axaml +++ b/StabilityMatrix.Avalonia/Styles/DockStyles.axaml @@ -16,7 +16,10 @@ + diff --git a/StabilityMatrix.Avalonia/Styles/ThemeColors.axaml b/StabilityMatrix.Avalonia/Styles/ThemeColors.axaml index 87ba83726..77f074e9e 100644 --- a/StabilityMatrix.Avalonia/Styles/ThemeColors.axaml +++ b/StabilityMatrix.Avalonia/Styles/ThemeColors.axaml @@ -35,6 +35,9 @@ + + + @@ -45,6 +48,9 @@ + + + @@ -55,6 +61,9 @@ + + + diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml index dddc54e2c..c48e05a56 100644 --- a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml +++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml @@ -7,9 +7,6 @@ xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" - xmlns:system="clr-namespace:System;assembly=System.Runtime" - xmlns:ui="using:FluentAvalonia.UI.Controls" - xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" d:DataContext="{x:Static mocks:DesignData.InferenceImageUpscaleViewModel}" d:DesignHeight="450" From e76ef3508e0b29dd39db8cf45c9a314a9dabe9c3 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 3 Sep 2023 18:45:56 -0400 Subject: [PATCH 211/474] Add StripStart string extension method --- .../Extensions/StringExtensions.cs | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/StabilityMatrix.Core/Extensions/StringExtensions.cs b/StabilityMatrix.Core/Extensions/StringExtensions.cs index 9f8435d4e..986f7b214 100644 --- a/StabilityMatrix.Core/Extensions/StringExtensions.cs +++ b/StabilityMatrix.Core/Extensions/StringExtensions.cs @@ -4,24 +4,26 @@ namespace StabilityMatrix.Core.Extensions; public static class StringExtensions { - private static string EncodeNonAsciiCharacters(string value) { + private static string EncodeNonAsciiCharacters(string value) + { var sb = new StringBuilder(); - foreach (var c in value) + foreach (var c in value) { // If not ascii or not printable if (c > 127 || c < 32) { // This character is too big for ASCII - var encodedValue = "\\u" + ((int) c).ToString("x4"); + var encodedValue = "\\u" + ((int)c).ToString("x4"); sb.Append(encodedValue); } - else { + else + { sb.Append(c); } } return sb.ToString(); } - + /// /// Converts string to repr /// @@ -35,22 +37,24 @@ public static string ToRepr(this string? str) writer.Write("'"); foreach (var ch in str) { - writer.Write(ch switch - { - '\0' => "\\0", - '\n' => "\\n", - '\r' => "\\r", - '\t' => "\\t", - // Non ascii - _ when ch > 127 || ch < 32 => $"\\u{(int) ch:x4}", - _ => ch.ToString() - }); + writer.Write( + ch switch + { + '\0' => "\\0", + '\n' => "\\n", + '\r' => "\\r", + '\t' => "\\t", + // Non ascii + _ when ch > 127 || ch < 32 => $"\\u{(int)ch:x4}", + _ => ch.ToString() + } + ); } writer.Write("'"); - + return writer.ToString(); } - + /// /// Counts continuous sequence of a character /// from the start of the string @@ -71,4 +75,13 @@ public static int CountStart(this string str, char c) } return count; } + + /// + /// Strips the substring from the start of the string + /// + public static string StripStart(this string str, string subString) + { + var index = str.IndexOf(subString, StringComparison.Ordinal); + return index < 0 ? str : str.Remove(index, subString.Length); + } } From 352163422bfa5db688cd20360772967bc6f0bd30 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 3 Sep 2023 21:39:37 -0400 Subject: [PATCH 212/474] Fix serialization support for JsonPropertyName --- .../ViewModels/Base/LoadableViewModelBase.cs | 160 +++++++++++++----- 1 file changed, 120 insertions(+), 40 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs index 857846bd3..6aabb15c7 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs @@ -21,20 +21,18 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState typeof(IRelayCommand) }; - private static readonly string[] SerializerIgnoredNames = - { - nameof(HasErrors), - }; + private static readonly string[] SerializerIgnoredNames = { nameof(HasErrors), }; - private static readonly JsonSerializerOptions SerializerOptions = new() - { - IgnoreReadOnlyProperties = true, - }; + private static readonly JsonSerializerOptions SerializerOptions = + new() { IgnoreReadOnlyProperties = true, }; private static bool ShouldIgnoreProperty(PropertyInfo property) { // Skip if read-only and not IJsonLoadableState - if (property.SetMethod is null && !typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) + if ( + property.SetMethod is null + && !typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType) + ) { Logger.Trace("Skipping {Property} - read-only", property.Name); return true; @@ -48,7 +46,11 @@ private static bool ShouldIgnoreProperty(PropertyInfo property) // Check not excluded type if (SerializerIgnoredTypes.Contains(property.PropertyType)) { - Logger.Trace("Skipping {Property} - serializer ignored type {Type}", property.Name, property.PropertyType); + Logger.Trace( + "Skipping {Property} - serializer ignored type {Type}", + property.Name, + property.PropertyType + ); return true; } // Check not ignored name @@ -60,7 +62,22 @@ private static bool ShouldIgnoreProperty(PropertyInfo property) return false; } - + + /// + /// True if we should include property without checking exclusions + /// + private static bool ShouldIncludeProperty(PropertyInfo property) + { + // Has JsonIncludeAttribute + if (property.GetCustomAttributes(typeof(JsonIncludeAttribute), true).Length > 0) + { + Logger.Trace("Including {Property} - has [JsonInclude]", property.Name); + return true; + } + + return false; + } + /// /// Load the state of this view model from a JSON object. /// The default implementation is a mirror of . @@ -81,51 +98,84 @@ public virtual void LoadStateFromJsonObject(JsonObject state) foreach (var property in properties) { + var name = property.Name; + + // If JsonPropertyName provided, use that as the key + if ( + property + .GetCustomAttributes(typeof(JsonPropertyNameAttribute), true) + .FirstOrDefault() + is JsonPropertyNameAttribute jsonPropertyName + ) + { + Logger.Trace( + "Deserializing {Property} ({Type}) with JsonPropertyName {JsonPropertyName}", + property.Name, + property.PropertyType, + jsonPropertyName.Name + ); + if (property.GetValue(this) is string jsonPropertyNameValue) + { + name = jsonPropertyNameValue; + } + } + // Check if property is in the JSON object - if (!state.TryGetPropertyValue(property.Name, out var value)) + if (!state.TryGetPropertyValue(name, out var value)) { Logger.Trace("Skipping {Property} - not in JSON object", property.Name); continue; } - + // Check if we should ignore this property - if (ShouldIgnoreProperty(property)) + if (!ShouldIncludeProperty(property) && ShouldIgnoreProperty(property)) { continue; } - + // For types that also implement IJsonLoadableState, defer to their load implementation if (typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) { - Logger.Trace("Loading {Property} ({Type}) with IJsonLoadableState", property.Name, property.PropertyType); - + Logger.Trace( + "Loading {Property} ({Type}) with IJsonLoadableState", + property.Name, + property.PropertyType + ); + // Value must be non-null if (value is null) { - throw new InvalidOperationException($"Property {property.Name} is IJsonLoadableState but value to be loaded is null"); + throw new InvalidOperationException( + $"Property {property.Name} is IJsonLoadableState but value to be loaded is null" + ); } - + // Check if the current object at this property is null if (property.GetValue(this) is not IJsonLoadableState propertyValue) { // If null, it must have a default constructor - if (property.PropertyType.GetConstructor(Type.EmptyTypes) is not { } constructorInfo) + if ( + property.PropertyType.GetConstructor(Type.EmptyTypes) + is not { } constructorInfo + ) { - throw new InvalidOperationException($"Property {property.Name} is IJsonLoadableState but current object is null and has no default constructor"); + throw new InvalidOperationException( + $"Property {property.Name} is IJsonLoadableState but current object is null and has no default constructor" + ); } - + // Create a new instance and set it - propertyValue = (IJsonLoadableState) constructorInfo.Invoke(null); + propertyValue = (IJsonLoadableState)constructorInfo.Invoke(null); property.SetValue(this, propertyValue); } - + // Load the state from the JSON object propertyValue.LoadStateFromJsonObject(value.AsObject()); } else { Logger.Trace("Loading {Property} ({Type})", property.Name, property.PropertyType); - + var propertyValue = value.Deserialize(property.PropertyType, SerializerOptions); property.SetValue(this, propertyValue); } @@ -149,60 +199,90 @@ public virtual JsonObject SaveStateToJsonObject() // Get all of our properties using reflection. var properties = GetType().GetProperties(); Logger.Trace("Serializing {Type} with {Count} properties", GetType(), properties.Length); - + // Create a JSON object to store the state. var state = new JsonObject(); - + // Serialize each property marked with JsonIncludeAttribute. foreach (var property in properties) { - if (ShouldIgnoreProperty(property)) + if (!ShouldIncludeProperty(property) && ShouldIgnoreProperty(property)) { continue; } + var name = property.Name; + + // If JsonPropertyName provided, use that as the key. + if ( + property + .GetCustomAttributes(typeof(JsonPropertyNameAttribute), true) + .FirstOrDefault() + is JsonPropertyNameAttribute jsonPropertyName + ) + { + Logger.Trace( + "Serializing {Property} ({Type}) with JsonPropertyName {JsonPropertyName}", + property.Name, + property.PropertyType, + jsonPropertyName.Name + ); + if (property.GetValue(this) is string value) + { + name = value; + } + } + // For types that also implement IJsonLoadableState, defer to their implementation. if (typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) { - Logger.Trace("Serializing {Property} ({Type}) with IJsonLoadableState", property.Name, property.PropertyType); + Logger.Trace( + "Serializing {Property} ({Type}) with IJsonLoadableState", + property.Name, + property.PropertyType + ); var value = property.GetValue(this); if (value is not null) { - var model = (IJsonLoadableState) value; + var model = (IJsonLoadableState)value; var modelState = model.SaveStateToJsonObject(); - state.Add(property.Name, modelState); + state.Add(name, modelState); } } else { - Logger.Trace("Serializing {Property} ({Type})", property.Name, property.PropertyType); + Logger.Trace( + "Serializing {Property} ({Type})", + property.Name, + property.PropertyType + ); var value = property.GetValue(this); if (value is not null) { - state.Add(property.Name, JsonSerializer.SerializeToNode(value, SerializerOptions)); + state.Add(name, JsonSerializer.SerializeToNode(value, SerializerOptions)); } } } - + return state; } - + /// /// Serialize a model to a JSON object. /// protected static JsonObject SerializeModel(T model) { var node = JsonSerializer.SerializeToNode(model); - return node?.AsObject() ?? throw new - NullReferenceException("Failed to serialize state to JSON object."); + return node?.AsObject() + ?? throw new NullReferenceException("Failed to serialize state to JSON object."); } - + /// /// Deserialize a model from a JSON object. /// protected static T DeserializeModel(JsonObject state) { - return state.Deserialize() ?? throw new - NullReferenceException("Failed to deserialize state from JSON object."); + return state.Deserialize() + ?? throw new NullReferenceException("Failed to deserialize state from JSON object."); } } From cda51c64fb99f2b747c5c43ef405b7f3e03a0841 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 3 Sep 2023 22:16:43 -0400 Subject: [PATCH 213/474] Add current tab state saving --- StabilityMatrix.Avalonia/App.axaml | 2 + .../DesignData/MockLiteDbContext.cs | 17 +- .../Models/InferenceProjectDocument.cs | 22 +- .../InferenceTextToImageViewModel.cs | 9 - .../ViewModels/InferenceViewModel.cs | 238 ++++++++++++++---- .../Views/MainWindow.axaml | 5 +- .../Database/ILiteDbContext.cs | 4 +- .../Database/LiteDbContext.cs | 117 +++++---- .../Models/Api/Comfy/ComfySampler.cs | 3 - .../Models/Api/Comfy/ComfyUpscaler.cs | 5 +- .../Models/Database/InferenceProjectEntry.cs | 35 +++ 11 files changed, 330 insertions(+), 127 deletions(-) create mode 100644 StabilityMatrix.Core/Models/Database/InferenceProjectEntry.cs diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml index c2f99b9e0..65d450154 100644 --- a/StabilityMatrix.Avalonia/App.axaml +++ b/StabilityMatrix.Avalonia/App.axaml @@ -17,7 +17,9 @@ + + 700 diff --git a/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs b/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs index 3b56d19a7..fe5995d36 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs @@ -12,11 +12,18 @@ public class MockLiteDbContext : ILiteDbContext { public LiteDatabaseAsync Database => throw new NotImplementedException(); public ILiteCollectionAsync CivitModels => throw new NotImplementedException(); - public ILiteCollectionAsync CivitModelVersions => throw new NotImplementedException(); - public ILiteCollectionAsync CivitModelQueryCache => throw new NotImplementedException(); - public ILiteCollectionAsync LocalModelFiles => throw new NotImplementedException(); - - public Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3) + public ILiteCollectionAsync CivitModelVersions => + throw new NotImplementedException(); + public ILiteCollectionAsync CivitModelQueryCache => + throw new NotImplementedException(); + public ILiteCollectionAsync LocalModelFiles => + throw new NotImplementedException(); + public ILiteCollectionAsync InferenceProjects => + throw new NotImplementedException(); + + public Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync( + string hashBlake3 + ) { return Task.FromResult<(CivitModel?, CivitModelVersion?)>((null, null)); } diff --git a/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs b/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs index c06ac7815..fa977e13f 100644 --- a/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs +++ b/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs @@ -13,17 +13,14 @@ namespace StabilityMatrix.Avalonia.Models; public class InferenceProjectDocument { [JsonIgnore] - private static readonly JsonSerializerOptions SerializerOptions = new() - { - IgnoreReadOnlyProperties = true, - WriteIndented = true, - }; - - public int Version { get; set; } = 1; - + private static readonly JsonSerializerOptions SerializerOptions = + new() { IgnoreReadOnlyProperties = true, WriteIndented = true, }; + + public int Version { get; set; } = 2; + [JsonConverter(typeof(JsonStringEnumConverter))] public InferenceProjectType ProjectType { get; set; } - + public JsonObject? State { get; set; } public static InferenceProjectDocument FromLoadable(IJsonLoadableState loadableModel) @@ -33,9 +30,10 @@ public static InferenceProjectDocument FromLoadable(IJsonLoadableState loadableM ProjectType = loadableModel switch { InferenceTextToImageViewModel => InferenceProjectType.TextToImage, - _ => throw new InvalidOperationException( - $"Unknown loadable model type: {loadableModel.GetType()}" - ) + _ + => throw new InvalidOperationException( + $"Unknown loadable model type: {loadableModel.GetType()}" + ) }, State = loadableModel.SaveStateToJsonObject() }; diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index ddbf655e5..0022651a4 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -1,17 +1,12 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Drawing; -using System.IO; using System.Linq; -using System.Reactive.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; -using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -24,19 +19,15 @@ using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; -using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Attributes; -using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Inference; -using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; using StabilityMatrix.Core.Services; using InferenceTextToImageView = StabilityMatrix.Avalonia.Views.Inference.InferenceTextToImageView; -using Size = System.Drawing.Size; #pragma warning disable CS0657 // Not a valid attribute location for this declaration diff --git a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs index b715fcff1..6a3d89b2a 100644 --- a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Immutable; using System.Collections.ObjectModel; +using System.Linq; using System.Text.Json; using System.Threading.Tasks; using AsyncAwaitBestPractices; +using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; @@ -17,8 +20,11 @@ using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Database; +using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; @@ -38,6 +44,7 @@ public partial class InferenceViewModel : PageViewModelBase private readonly ServiceManager vmFactory; private readonly IApiFactory apiFactory; private readonly IModelIndexService modelIndexService; + private readonly ILiteDbContext liteDbContext; public override string Title => "Inference"; public override IconSource IconSource => @@ -71,7 +78,8 @@ public InferenceViewModel( INotificationService notificationService, IInferenceClientManager inferenceClientManager, ISettingsManager settingsManager, - IModelIndexService modelIndexService + IModelIndexService modelIndexService, + ILiteDbContext liteDbContext ) { this.vmFactory = vmFactory; @@ -79,6 +87,7 @@ IModelIndexService modelIndexService this.notificationService = notificationService; this.settingsManager = settingsManager; this.modelIndexService = modelIndexService; + this.liteDbContext = liteDbContext; ClientManager = inferenceClientManager; @@ -91,9 +100,61 @@ IModelIndexService modelIndexService MenuSaveAsCommand.WithNotificationErrorHandler(notificationService); } - public override void OnLoaded() + private bool isFirstLoadComplete; + + public override async Task OnLoadedAsync() { - base.OnLoaded(); + await base.OnLoadedAsync(); + + if (!Design.IsDesignMode && !isFirstLoadComplete) + { + // Load any open projects + var openProjects = await liteDbContext.InferenceProjects.FindAsync(p => p.IsOpen); + + if (openProjects is not null) + { + foreach (var project in openProjects.OrderBy(p => p.CurrentTabIndex)) + { + var file = new FilePath(project.FilePath); + + if (!file.Exists) + { + // Remove from database + await liteDbContext.InferenceProjects.DeleteAsync(project.Id); + } + + try + { + if (file.Exists) + { + await AddTabFromFile(project.FilePath); + } + } + catch (Exception e) + { + Logger.Warn(e, "Failed to open project file {FilePath}", project.FilePath); + + notificationService.Show( + "Failed to open project file", + $"[{e.GetType().Name}] {e.Message}", + NotificationType.Error + ); + + // Set not open + await liteDbContext.InferenceProjects.UpdateAsync( + project with + { + IsOpen = false, + IsSelected = false, + CurrentTabIndex = -1 + } + ); + } + } + } + + isFirstLoadComplete = true; + } if (Tabs.Count == 0) { @@ -101,7 +162,75 @@ public override void OnLoaded() } // Start a model index update - modelIndexService.RefreshIndex().SafeFireAndForget(); + await modelIndexService.RefreshIndex(); + } + + /// + /// Update the database with current tabs + /// + private async Task SyncTabStatesWithDatabase() + { + // Update the database with the current tabs + foreach (var (i, tab) in Tabs.ToImmutableArray().Enumerate()) + { + if (tab.ProjectFile is not { } projectFile) + { + continue; + } + + var entry = await liteDbContext.InferenceProjects.FindOneAsync( + p => p.FilePath == projectFile.ToString() + ); + + // Create if not found + entry ??= new InferenceProjectEntry + { + Id = Guid.NewGuid(), + FilePath = projectFile.ToString() + }; + + entry.IsOpen = tab == SelectedTab; + entry.CurrentTabIndex = i; + + Logger.Trace( + "SyncTabStatesWithDatabase updated entry for tab '{Title}': {@Entry}", + tab.TabTitle, + entry + ); + await liteDbContext.InferenceProjects.UpsertAsync(entry); + } + } + + /// + /// Update the database with given tab + /// + private async Task SyncTabStateWithDatabase(InferenceTabViewModelBase tab) + { + if (tab.ProjectFile is not { } projectFile) + { + return; + } + + var entry = await liteDbContext.InferenceProjects.FindOneAsync( + p => p.FilePath == projectFile.ToString() + ); + + // Create if not found + entry ??= new InferenceProjectEntry + { + Id = Guid.NewGuid(), + FilePath = projectFile.ToString() + }; + + entry.IsOpen = tab == SelectedTab; + entry.CurrentTabIndex = Tabs.IndexOf(tab); + + Logger.Trace( + "SyncTabStatesWithDatabase updated entry for tab '{Title}': {@Entry}", + tab.TabTitle, + entry + ); + await liteDbContext.InferenceProjects.UpsertAsync(entry); } /// @@ -110,10 +239,14 @@ public override void OnLoaded() [RelayCommand] private void AddTab() { - Tabs.Add(vmFactory.Get()); + var tab = vmFactory.Get(); + Tabs.Add(tab); // Set as new selected tab SelectedTabIndex = Tabs.Count - 1; + + // Update the database with the current tab + SyncTabStateWithDatabase(tab).SafeFireAndForget(); } /// @@ -144,10 +277,13 @@ public void OnTabCloseRequested(TabViewTabCloseRequestedEventArgs e) // Remove the tab Tabs.RemoveAt(index); - - // Dispose the view model - vm.Dispose(); } + + // Update the database with the current tab + SyncTabStateWithDatabase(vm).SafeFireAndForget(); + + // Dispose the view model + vm.Dispose(); } /// @@ -261,6 +397,9 @@ await JsonSerializer.SerializeAsync( // Update project file currentTab.ProjectFile = new FilePath(result.TryGetLocalPath()!); + // Update the database with the current tab + await SyncTabStateWithDatabase(currentTab); + notificationService.Show( "Saved", $"Saved project to {result.Name}", @@ -320,6 +459,50 @@ await JsonSerializer.SerializeAsync( ); } + private async Task AddTabFromFile(FilePath file) + { + await using var stream = file.Info.OpenRead(); + + var document = await JsonSerializer.DeserializeAsync(stream); + if (document is null) + { + throw new ApplicationException( + "MenuOpenProject: Deserialize project file returned null" + ); + } + + if (document.State is null) + { + throw new ApplicationException("Project file does not have 'State' key"); + } + + InferenceTabViewModelBase vm; + if (document.ProjectType is InferenceProjectType.TextToImage) + { + // Get view model + var textToImage = vmFactory.Get(); + // Load state + textToImage.LoadStateFromJsonObject(document.State); + // Set the file backing the view model + textToImage.ProjectFile = file; + vm = textToImage; + } + else + { + throw new InvalidOperationException( + $"Unsupported project type: {document.ProjectType}" + ); + } + + Tabs.Add(vm); + + // Set the selected tab to the newly opened tab + SelectedTab = vm; + + // Update the database with the current tab + SyncTabStateWithDatabase(vm).SafeFireAndForget(); + } + /// /// Menu "Open Project" command. /// @@ -357,44 +540,7 @@ private async Task MenuOpenProject() // Load from file var file = results[0]; - await using var stream = await file.OpenReadAsync(); - var document = await JsonSerializer.DeserializeAsync(stream); - if (document is null) - { - throw new ApplicationException( - "MenuOpenProject: Deserialize project file returned null" - ); - } - - if (document.State is null) - { - throw new ApplicationException( - "MenuOpenProject: Deserialize project file returned null state" - ); - } - - InferenceTabViewModelBase vm; - if (document.ProjectType is InferenceProjectType.TextToImage) - { - // Get view model - var textToImage = vmFactory.Get(); - // Load state - textToImage.LoadStateFromJsonObject(document.State); - // Set the file backing the view model - textToImage.ProjectFile = new FilePath(file.TryGetLocalPath()!); - vm = textToImage; - } - else - { - throw new InvalidOperationException( - $"Unsupported project type: {document.ProjectType}" - ); - } - - Tabs.Add(vm); - - // Set the selected tab to the newly opened tab - SelectedTab = vm; + await AddTabFromFile(file.TryGetLocalPath()!); } } diff --git a/StabilityMatrix.Avalonia/Views/MainWindow.axaml b/StabilityMatrix.Avalonia/Views/MainWindow.axaml index 970a489dc..b3cbcbe80 100644 --- a/StabilityMatrix.Avalonia/Views/MainWindow.axaml +++ b/StabilityMatrix.Avalonia/Views/MainWindow.axaml @@ -6,7 +6,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:id="using:Dock.Avalonia" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:base="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Base" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="550" @@ -17,8 +16,8 @@ Width="1100" Height="750" Title="Stability Matrix" - id:DockProperties.IsDragEnabled="True" - id:DockProperties.IsDropEnabled="True" + DockProperties.IsDragEnabled="True" + DockProperties.IsDropEnabled="True" x:Class="StabilityMatrix.Avalonia.Views.MainWindow"> diff --git a/StabilityMatrix.Core/Database/ILiteDbContext.cs b/StabilityMatrix.Core/Database/ILiteDbContext.cs index ac55191c0..c4defc173 100644 --- a/StabilityMatrix.Core/Database/ILiteDbContext.cs +++ b/StabilityMatrix.Core/Database/ILiteDbContext.cs @@ -7,13 +7,13 @@ namespace StabilityMatrix.Core.Database; public interface ILiteDbContext : IDisposable { LiteDatabaseAsync Database { get; } - + ILiteCollectionAsync CivitModels { get; } ILiteCollectionAsync CivitModelVersions { get; } ILiteCollectionAsync CivitModelQueryCache { get; } ILiteCollectionAsync LocalModelFiles { get; } + ILiteCollectionAsync InferenceProjects { get; } - Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3); Task UpsertCivitModelAsync(CivitModel civitModel); Task UpsertCivitModelAsync(IEnumerable civitModels); diff --git a/StabilityMatrix.Core/Database/LiteDbContext.cs b/StabilityMatrix.Core/Database/LiteDbContext.cs index f568222e4..2da50072c 100644 --- a/StabilityMatrix.Core/Database/LiteDbContext.cs +++ b/StabilityMatrix.Core/Database/LiteDbContext.cs @@ -21,18 +21,26 @@ public class LiteDbContext : ILiteDbContext // Notification events public event EventHandler? CivitModelsChanged; - + // Collections (Tables) - public ILiteCollectionAsync CivitModels => Database.GetCollection("CivitModels"); - public ILiteCollectionAsync CivitModelVersions => Database.GetCollection("CivitModelVersions"); - public ILiteCollectionAsync CivitModelQueryCache => Database.GetCollection("CivitModelQueryCache"); - public ILiteCollectionAsync GithubCache => Database.GetCollection("GithubCache"); - public ILiteCollectionAsync LocalModelFiles => Database.GetCollection("LocalModelFiles"); - + public ILiteCollectionAsync CivitModels => + Database.GetCollection("CivitModels"); + public ILiteCollectionAsync CivitModelVersions => + Database.GetCollection("CivitModelVersions"); + public ILiteCollectionAsync CivitModelQueryCache => + Database.GetCollection("CivitModelQueryCache"); + public ILiteCollectionAsync GithubCache => + Database.GetCollection("GithubCache"); + public ILiteCollectionAsync LocalModelFiles => + Database.GetCollection("LocalModelFiles"); + public ILiteCollectionAsync InferenceProjects => + Database.GetCollection("InferenceProjects"); + public LiteDbContext( ILogger logger, - ISettingsManager settingsManager, - IOptions debugOptions) + ISettingsManager settingsManager, + IOptions debugOptions + ) { this.logger = logger; this.settingsManager = settingsManager; @@ -42,7 +50,7 @@ public LiteDbContext( private LiteDatabaseAsync CreateDatabase() { LiteDatabaseAsync? db = null; - + if (debugOptions.TempDatabase) { db = new LiteDatabaseAsync(":temp:"); @@ -53,54 +61,74 @@ private LiteDatabaseAsync CreateDatabase() try { var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db"); - db = new LiteDatabaseAsync(new ConnectionString() - { - Filename = dbPath, - Connection = ConnectionType.Shared, - }); + db = new LiteDatabaseAsync( + new ConnectionString() + { + Filename = dbPath, + Connection = ConnectionType.Shared, + } + ); } catch (IOException e) { - logger.LogWarning("Database in use or not accessible ({Message}), using temporary database", e.Message); + logger.LogWarning( + "Database in use or not accessible ({Message}), using temporary database", + e.Message + ); } } - + // Fallback to temporary database db ??= new LiteDatabaseAsync(":temp:"); // Register reference fields - LiteDBExtensions.Register(m => m.ModelVersions, "CivitModelVersions"); - LiteDBExtensions.Register(e => e.Items, "CivitModels"); + LiteDBExtensions.Register( + m => m.ModelVersions, + "CivitModelVersions" + ); + LiteDBExtensions.Register( + e => e.Items, + "CivitModels" + ); return db; } - - public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3) + + public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync( + string hashBlake3 + ) { - var version = await CivitModelVersions.Query() - .Where(mv => mv.Files! - .Select(f => f.Hashes) - .Select(hashes => hashes.BLAKE3) - .Any(hash => hash == hashBlake3)) + var version = await CivitModelVersions + .Query() + .Where( + mv => + mv.Files! + .Select(f => f.Hashes) + .Select(hashes => hashes.BLAKE3) + .Any(hash => hash == hashBlake3) + ) .FirstOrDefaultAsync() .ConfigureAwait(false); - - if (version is null) return (null, null); - - var model = await CivitModels.Query() + + if (version is null) + return (null, null); + + var model = await CivitModels + .Query() .Include(m => m.ModelVersions) - .Where(m => m.ModelVersions! - .Select(v => v.Id) - .Any(id => id == version.Id)) - .FirstOrDefaultAsync().ConfigureAwait(false); - + .Where(m => m.ModelVersions!.Select(v => v.Id).Any(id => id == version.Id)) + .FirstOrDefaultAsync() + .ConfigureAwait(false); + return (model, version); } - + public async Task UpsertCivitModelAsync(CivitModel civitModel) { // Insert model versions first then model - var versionsUpdated = await CivitModelVersions.UpsertAsync(civitModel.ModelVersions).ConfigureAwait(false); + var versionsUpdated = await CivitModelVersions + .UpsertAsync(civitModel.ModelVersions) + .ConfigureAwait(false); var updated = await CivitModels.UpsertAsync(civitModel).ConfigureAwait(false); // Notify listeners on any change var anyUpdated = versionsUpdated > 0 || updated; @@ -110,7 +138,7 @@ public async Task UpsertCivitModelAsync(CivitModel civitModel) } return anyUpdated; } - + public async Task UpsertCivitModelAsync(IEnumerable civitModels) { var civitModelsArray = civitModels.ToArray(); @@ -126,7 +154,7 @@ public async Task UpsertCivitModelAsync(IEnumerable civitModel } return anyUpdated; } - + // Add to cache public async Task UpsertCivitModelQueryCacheEntryAsync(CivitModelQueryCacheEntry entry) { @@ -141,13 +169,14 @@ public async Task UpsertCivitModelQueryCacheEntryAsync(CivitModelQueryCach public async Task GetGithubCacheEntry(string? cacheKey) { - if (string.IsNullOrEmpty(cacheKey)) return null; - + if (string.IsNullOrEmpty(cacheKey)) + return null; + if (await GithubCache.FindByIdAsync(cacheKey).ConfigureAwait(false) is { } result) { return result; } - + return null; } @@ -162,9 +191,7 @@ public void Dispose() { database.Dispose(); } - catch (ObjectDisposedException) - { - } + catch (ObjectDisposedException) { } database = null; } diff --git a/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs b/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs index 2bec42541..d244e3241 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs @@ -1,10 +1,7 @@ using System.Collections.Immutable; -using System.Text.Json.Serialization; -using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.Api.Comfy; -[JsonConverter(typeof(StringJsonConverter))] public readonly record struct ComfySampler(string Name) { private static Dictionary ConvertDict { get; } = diff --git a/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscaler.cs b/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscaler.cs index ed42addfe..9c8821c1e 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscaler.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscaler.cs @@ -1,10 +1,8 @@ using System.Collections.Immutable; using System.Text.Json.Serialization; -using StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Models.Api.Comfy; -[JsonConverter(typeof(StringJsonConverter))] public readonly record struct ComfyUpscaler(string Name, ComfyUpscalerType Type) { private static Dictionary ConvertDict { get; } = @@ -22,6 +20,7 @@ public readonly record struct ComfyUpscaler(string Name, ComfyUpscalerType Type) .Select(k => new ComfyUpscaler(k, ComfyUpscalerType.Latent)) .ToImmutableArray(); + [JsonIgnore] public string DisplayType { get @@ -36,6 +35,7 @@ public string DisplayType } } + [JsonIgnore] public string DisplayName { get @@ -55,6 +55,7 @@ public string DisplayName } } + [JsonIgnore] public string ShortDisplayName { get diff --git a/StabilityMatrix.Core/Models/Database/InferenceProjectEntry.cs b/StabilityMatrix.Core/Models/Database/InferenceProjectEntry.cs new file mode 100644 index 000000000..042d94f91 --- /dev/null +++ b/StabilityMatrix.Core/Models/Database/InferenceProjectEntry.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Nodes; +using LiteDB; + +namespace StabilityMatrix.Core.Models.Database; + +public record InferenceProjectEntry +{ + [BsonId] + public required Guid Id { get; init; } + + /// + /// Full path to the project file (.smproj) + /// + public required string FilePath { get; init; } + + /// + /// Whether the project is open in the editor + /// + public bool IsOpen { get; set; } + + /// + /// Whether the project is selected in the editor + /// + public bool IsSelected { get; set; } + + /// + /// Current index of the tab + /// + public int CurrentTabIndex { get; set; } = -1; + + /// + /// The current dock layout state + /// + public JsonObject? DockLayout { get; set; } +} From 384884e187bf0bbbb30937e01f6fef64de51731a Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 3 Sep 2023 22:17:01 -0400 Subject: [PATCH 214/474] Add image metadata parsing internals, debug menu --- .../Helpers/ImageMetadata.cs | 44 +++++++++++++++++++ .../StabilityMatrix.Avalonia.csproj | 5 ++- .../ViewModels/SettingsViewModel.cs | 22 ++++++++++ .../Views/SettingsPage.axaml | 12 +++++ .../StabilityMatrix.Core.csproj | 1 + 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Helpers/ImageMetadata.cs diff --git a/StabilityMatrix.Avalonia/Helpers/ImageMetadata.cs b/StabilityMatrix.Avalonia/Helpers/ImageMetadata.cs new file mode 100644 index 000000000..79b66563f --- /dev/null +++ b/StabilityMatrix.Avalonia/Helpers/ImageMetadata.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using MetadataExtractor; +using StabilityMatrix.Core.Extensions; +using StabilityMatrix.Core.Models.FileInterfaces; + +namespace StabilityMatrix.Avalonia.Helpers; + +public class ImageMetadata +{ + private IReadOnlyList? Directories { get; set; } + + public static ImageMetadata ParseFile(FilePath path) + { + return new ImageMetadata() { Directories = ImageMetadataReader.ReadMetadata(path) }; + } + + public string? GetComfyMetadata() + { + if (Directories is null) + { + return null; + } + + // For Comfy, we want the PNG-tEXt directory + if (Directories.FirstOrDefault(d => d.Name == "PNG-tEXt") is not { } pngText) + { + return null; + } + + // Expect the 'Textual Data' tag + if ( + pngText.Tags.FirstOrDefault(tag => tag.Name == "Textual Data") is not { } textTag + || textTag.Description is null + ) + { + return null; + } + + // Strip `prompt: ` and the rest of the description is json + + return textTag.Description.StripStart("prompt:").TrimStart(); + } +} diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index 286dc433f..aeca1c8d0 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -29,14 +29,15 @@ - - + + + diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index e7164507f..90bf44709 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -729,6 +729,28 @@ private async Task DebugLoadCompletionCsv() notificationService.Show("Loaded completion file", ""); } + [RelayCommand] + private async Task DebugImageMetadata() + { + var provider = App.StorageProvider; + var files = await provider.OpenFilePickerAsync(new FilePickerOpenOptions()); + + if (files.Count == 0) + return; + + var metadata = ImageMetadata.ParseFile(files[0].TryGetLocalPath()!); + var comfyJson = metadata.GetComfyMetadata(); + + if (comfyJson is null) + { + notificationService.Show("No Comfy metadata found", ""); + return; + } + + var dialog = DialogHelper.CreateJsonDialog(comfyJson); + await dialog.ShowAsync(); + } + [RelayCommand] private async Task DebugRefreshModelsIndex() { diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml index ed1ca4334..32d35b346 100644 --- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml @@ -406,6 +406,18 @@ Content="Select images" /> + + + + + + + + - - - - - + + + From cbe41b16d5a2ad4b95ef5397975e979a299f7764 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 5 Sep 2023 18:26:38 -0400 Subject: [PATCH 228/474] 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 aeca1c8d0..ad402e900 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -8,7 +8,7 @@ app.manifest true ./Assets/Icon.ico - 2.4.0-dev.1 + 2.4.0-dev.4 $(Version) true true From 2f2b6b428bdf926b4a04b4c4700490af19766e0b Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 5 Sep 2023 19:15:30 -0400 Subject: [PATCH 229/474] Fix hires fix using post processing upscaler --- .../ViewModels/Inference/InferenceTextToImageViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 0022651a4..43dec13b5 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -229,7 +229,7 @@ ModelCardViewModel is LatentNodeConnection hiresLatent; // Select between latent upscale and normal upscale based on the upscale method - var selectedUpscaler = UpscalerCardViewModel.SelectedUpscaler!.Value; + var selectedUpscaler = HiresUpscalerCardViewModel.SelectedUpscaler!.Value; if (selectedUpscaler.Type == ComfyUpscalerType.None) { From 6715c391cbdba8f482447289b1029a97ca6f19a1 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 6 Sep 2023 15:35:50 -0400 Subject: [PATCH 230/474] Add SettingsManager.ImagesDirectory --- .../Services/ISettingsManager.cs | 22 +- .../Services/SettingsManager.cs | 286 +++++++++++------- 2 files changed, 187 insertions(+), 121 deletions(-) diff --git a/StabilityMatrix.Core/Services/ISettingsManager.cs b/StabilityMatrix.Core/Services/ISettingsManager.cs index c8bfd7bae..6d1e1f9d4 100644 --- a/StabilityMatrix.Core/Services/ISettingsManager.cs +++ b/StabilityMatrix.Core/Services/ISettingsManager.cs @@ -15,14 +15,15 @@ public interface ISettingsManager string ModelsDirectory { get; } string DownloadsDirectory { get; } DirectoryPath TagsDirectory { get; } - + DirectoryPath ImagesDirectory { get; } + Settings Settings { get; } - + /// /// Event fired when the library directory is changed /// event EventHandler? LibraryDirChanged; - + /// /// Event fired when a property of Settings is changed /// @@ -33,12 +34,12 @@ public interface ISettingsManager /// Will fire instantly if it is already set. /// void RegisterOnLibraryDirSet(Action handler); - + /// /// Event fired when Settings are loaded from disk /// event EventHandler? Loaded; - + /// /// Return a SettingsTransaction that can be used to modify Settings /// Saves on Dispose. @@ -62,17 +63,20 @@ public interface ISettingsManager /// Register a source observable object and property to be relayed to Settings /// void RelayPropertyFor( - T source, + T source, Expression> sourceProperty, Expression> settingsProperty, - bool setInitial = false) where T : INotifyPropertyChanged; - + bool setInitial = false + ) + where T : INotifyPropertyChanged; + /// /// Register an Action to be called on change of the settings property. /// void RegisterPropertyChangedHandler( Expression> settingsProperty, - Action onPropertyChanged); + Action onPropertyChanged + ); /// /// Attempts to locate and set the library path diff --git a/StabilityMatrix.Core/Services/SettingsManager.cs b/StabilityMatrix.Core/Services/SettingsManager.cs index 308d383a7..a3f2a7dec 100644 --- a/StabilityMatrix.Core/Services/SettingsManager.cs +++ b/StabilityMatrix.Core/Services/SettingsManager.cs @@ -20,10 +20,16 @@ public class SettingsManager : ISettingsManager private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly ReaderWriterLockSlim FileLock = new(); - private static readonly string GlobalSettingsPath = Path.Combine(Compat.AppDataHome, "global.json"); - - private readonly string? originalEnvPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process); - + private static readonly string GlobalSettingsPath = Path.Combine( + Compat.AppDataHome, + "global.json" + ); + + private readonly string? originalEnvPath = Environment.GetEnvironmentVariable( + "PATH", + EnvironmentVariableTarget.Process + ); + // Library properties public bool IsPortableMode { get; private set; } private string? libraryDir; @@ -40,9 +46,9 @@ public string LibraryDir private set { var isChanged = libraryDir != value; - + libraryDir = value; - + // Only invoke if different if (isChanged) { @@ -57,20 +63,22 @@ private set private string SettingsPath => Path.Combine(LibraryDir, "settings.json"); public string ModelsDirectory => Path.Combine(LibraryDir, "Models"); public string DownloadsDirectory => Path.Combine(LibraryDir, ".downloads"); - + public DirectoryPath TagsDirectory => new(LibraryDir, "Tags"); + public DirectoryPath ImagesDirectory => new(LibraryDir, "Images"); + public Settings Settings { get; private set; } = new(); - + /// public event EventHandler? LibraryDirChanged; - + /// public event EventHandler? SettingsPropertyChanged; - + /// public event EventHandler? Loaded; - + /// public void RegisterOnLibraryDirSet(Action handler) { @@ -81,7 +89,7 @@ public void RegisterOnLibraryDirSet(Action handler) } LibraryDirChanged += Handler; - + return; void Handler(object? sender, string dir) @@ -90,17 +98,19 @@ void Handler(object? sender, string dir) handler(dir); } } - + /// public SettingsTransaction BeginTransaction() { if (!IsLibraryDirSet) { - throw new InvalidOperationException("LibraryDir not set when BeginTransaction was called"); + throw new InvalidOperationException( + "LibraryDir not set when BeginTransaction was called" + ); } return new SettingsTransaction(this, SaveSettingsAsync); } - + /// public void Transaction(Action func, bool ignoreMissingLibraryDir = false) { @@ -117,85 +127,101 @@ public void Transaction(Action func, bool ignoreMissingLibraryDir = fa func(transaction.Settings); transaction.Dispose(); } - + /// public void Transaction(Expression> expression, TValue value) { if (expression.Body is not MemberExpression memberExpression) { throw new ArgumentException( - $"Expression must be a member expression, not {expression.Body.NodeType}"); + $"Expression must be a member expression, not {expression.Body.NodeType}" + ); } var propertyInfo = memberExpression.Member as PropertyInfo; if (propertyInfo == null) { throw new ArgumentException( - $"Expression member must be a property, not {memberExpression.Member.MemberType}"); + $"Expression member must be a property, not {memberExpression.Member.MemberType}" + ); } - + var name = propertyInfo.Name; - + // Set value using var transaction = BeginTransaction(); propertyInfo.SetValue(transaction.Settings, value); - + // Invoke property changed event SettingsPropertyChanged?.Invoke(this, new RelayPropertyChangedEventArgs(name)); } /// public void RelayPropertyFor( - T source, + T source, Expression> sourceProperty, Expression> settingsProperty, - bool setInitial = false) where T : INotifyPropertyChanged + bool setInitial = false + ) + where T : INotifyPropertyChanged { var sourceGetter = sourceProperty.Compile(); var (propertyName, assigner) = Expressions.GetAssigner(sourceProperty); var sourceSetter = assigner.Compile(); - + var settingsGetter = settingsProperty.Compile(); var (targetPropertyName, settingsAssigner) = Expressions.GetAssigner(settingsProperty); var settingsSetter = settingsAssigner.Compile(); - + var sourceTypeName = source.GetType().Name; - + // Update source when settings change SettingsPropertyChanged += (sender, args) => { - if (args.PropertyName != targetPropertyName) return; - + if (args.PropertyName != targetPropertyName) + return; + // Skip if event is relay and the sender is the source, to prevent duplicate - if (args.IsRelay && ReferenceEquals(sender, source)) return; - + if (args.IsRelay && ReferenceEquals(sender, source)) + return; + Logger.Trace( - "[RelayPropertyFor] " + - "Settings.{TargetProperty:l} -> {SourceType:l}.{SourceProperty:l}", - targetPropertyName, sourceTypeName, propertyName); - + "[RelayPropertyFor] " + + "Settings.{TargetProperty:l} -> {SourceType:l}.{SourceProperty:l}", + targetPropertyName, + sourceTypeName, + propertyName + ); + sourceSetter(source, settingsGetter(Settings)); }; - + // Set and Save settings when source changes source.PropertyChanged += (sender, args) => { - if (args.PropertyName != propertyName) return; - + if (args.PropertyName != propertyName) + return; + Logger.Trace( - "[RelayPropertyFor] " + - "{SourceType:l}.{SourceProperty:l} -> Settings.{TargetProperty:l}", - sourceTypeName, propertyName, targetPropertyName); - + "[RelayPropertyFor] " + + "{SourceType:l}.{SourceProperty:l} -> Settings.{TargetProperty:l}", + sourceTypeName, + propertyName, + targetPropertyName + ); + settingsSetter(Settings, sourceGetter(source)); - + // Save settings to file SaveSettingsAsync().SafeFireAndForget(); - + // Invoke property changed event, passing along sender - SettingsPropertyChanged?.Invoke(sender, new RelayPropertyChangedEventArgs(targetPropertyName, true)); + SettingsPropertyChanged?.Invoke( + sender, + new RelayPropertyChangedEventArgs(targetPropertyName, true) + ); }; - + // Set initial value if requested if (setInitial) { @@ -206,28 +232,31 @@ public void RelayPropertyFor( /// public void RegisterPropertyChangedHandler( Expression> settingsProperty, - Action onPropertyChanged) + Action onPropertyChanged + ) { var settingsGetter = settingsProperty.Compile(); var (propertyName, _) = Expressions.GetAssigner(settingsProperty); - + // Invoke handler when settings change SettingsPropertyChanged += (_, args) => { - if (args.PropertyName != propertyName) return; - + if (args.PropertyName != propertyName) + return; + onPropertyChanged(settingsGetter(Settings)); }; } - + /// /// Attempts to locate and set the library path /// Return true if found, false otherwise /// public bool TryFindLibrary(bool forceReload = false) { - if (IsLibraryDirSet && !forceReload) return true; - + if (IsLibraryDirSet && !forceReload) + return true; + // 1. Check portable mode var appDir = Compat.AppCurrentDir; IsPortableMode = File.Exists(Path.Combine(appDir, "Data", ".sm-portable")); @@ -238,18 +267,21 @@ public bool TryFindLibrary(bool forceReload = false) LoadSettings(); return true; } - + // 2. Check %APPDATA%/StabilityMatrix/library.json FilePath libraryJsonFile = Compat.AppDataHome + "library.json"; - if (!libraryJsonFile.Exists) return false; - + if (!libraryJsonFile.Exists) + return false; + try { var libraryJson = libraryJsonFile.ReadAllText(); var librarySettings = JsonSerializer.Deserialize(libraryJson); - - if (!string.IsNullOrWhiteSpace(librarySettings?.LibraryPath) - && Directory.Exists(librarySettings?.LibraryPath)) + + if ( + !string.IsNullOrWhiteSpace(librarySettings?.LibraryPath) + && Directory.Exists(librarySettings?.LibraryPath) + ) { LibraryDir = librarySettings.LibraryPath; SetStaticLibraryPaths(); @@ -281,13 +313,16 @@ public void SetLibraryPath(string path) var libraryJsonFile = Compat.AppDataHome.JoinFile("library.json"); var library = new LibrarySettings { LibraryPath = path }; - var libraryJson = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true }); + var libraryJson = JsonSerializer.Serialize( + library, + new JsonSerializerOptions { WriteIndented = true } + ); libraryJsonFile.WriteAllText(libraryJson); - + // actually create the LibraryPath directory Directory.CreateDirectory(path); - } - + } + /// /// Enable and create settings files for portable mode /// Creates the ./Data directory and the `.sm-portable` marker file @@ -309,18 +344,21 @@ public void SetPortableMode() /// public IEnumerable GetOldInstalledPackages() { - var oldSettingsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "StabilityMatrix", "settings.json"); + var oldSettingsPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "StabilityMatrix", + "settings.json" + ); if (!File.Exists(oldSettingsPath)) yield break; - + var oldSettingsJson = File.ReadAllText(oldSettingsPath); - var oldSettings = JsonSerializer.Deserialize(oldSettingsJson, new JsonSerializerOptions - { - Converters = { new JsonStringEnumConverter() } - }); - + var oldSettings = JsonSerializer.Deserialize( + oldSettingsJson, + new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } } + ); + // Absolute paths are old formats requiring migration #pragma warning disable CS0618 var oldPackages = oldSettings?.InstalledPackages.Where(package => package.Path != null); @@ -328,7 +366,7 @@ public IEnumerable GetOldInstalledPackages() if (oldPackages == null) yield break; - + foreach (var package in oldPackages) { yield return package; @@ -337,24 +375,27 @@ public IEnumerable GetOldInstalledPackages() public Guid GetOldActivePackageId() { - var oldSettingsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "StabilityMatrix", "settings.json"); + var oldSettingsPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "StabilityMatrix", + "settings.json" + ); if (!File.Exists(oldSettingsPath)) return default; - + var oldSettingsJson = File.ReadAllText(oldSettingsPath); - var oldSettings = JsonSerializer.Deserialize(oldSettingsJson, new JsonSerializerOptions - { - Converters = { new JsonStringEnumConverter() } - }); + var oldSettings = JsonSerializer.Deserialize( + oldSettingsJson, + new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } } + ); if (oldSettings == null) return default; - + return oldSettings.ActiveInstalledPackageId ?? default; } - + public void AddPathExtension(string pathExtension) { Settings.PathExtensions ??= new List(); @@ -366,13 +407,14 @@ public string GetPathExtensionsAsString() { return string.Join(";", Settings.PathExtensions ?? new List()); } - + /// /// Insert path extensions to the front of the PATH environment variable /// public void InsertPathExtensions() { - if (Settings.PathExtensions == null) return; + if (Settings.PathExtensions == null) + return; var toInsert = GetPathExtensionsAsString(); // Append the original path, if any if (originalEnvPath != null) @@ -397,21 +439,23 @@ public void UpdatePackageVersionNumber(Guid id, string? newVersion) SaveSettings(); } - + public void SetLastUpdateCheck(InstalledPackage package) { - var installedPackage = Settings.InstalledPackages.First(p => p.DisplayName == package.DisplayName); + var installedPackage = Settings.InstalledPackages.First( + p => p.DisplayName == package.DisplayName + ); installedPackage.LastUpdateCheck = package.LastUpdateCheck; installedPackage.UpdateAvailable = package.UpdateAvailable; SaveSettings(); } - + public List GetLaunchArgs(Guid packageId) { var packageData = Settings.InstalledPackages.FirstOrDefault(x => x.Id == packageId); return packageData?.LaunchArgs ?? new(); } - + public void SaveLaunchArgs(Guid packageId, List launchArgs) { var packageData = Settings.InstalledPackages.FirstOrDefault(x => x.Id == packageId); @@ -425,12 +469,17 @@ public void SaveLaunchArgs(Guid packageId, List launchArgs) packageData.LaunchArgs = toSave; SaveSettings(); } - + public string? GetActivePackageHost() { - var package = Settings.InstalledPackages.FirstOrDefault(x => x.Id == Settings.ActiveInstalledPackageId); - if (package == null) return null; - var hostOption = package.LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "host"); + var package = Settings.InstalledPackages.FirstOrDefault( + x => x.Id == Settings.ActiveInstalledPackageId + ); + if (package == null) + return null; + var hostOption = package.LaunchArgs?.FirstOrDefault( + x => x.Name.ToLowerInvariant() == "host" + ); if (hostOption?.OptionValue != null) { return hostOption.OptionValue as string; @@ -440,9 +489,14 @@ public void SaveLaunchArgs(Guid packageId, List launchArgs) public string? GetActivePackagePort() { - var package = Settings.InstalledPackages.FirstOrDefault(x => x.Id == Settings.ActiveInstalledPackageId); - if (package == null) return null; - var portOption = package.LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "port"); + var package = Settings.InstalledPackages.FirstOrDefault( + x => x.Id == Settings.ActiveInstalledPackageId + ); + if (package == null) + return null; + var portOption = package.LaunchArgs?.FirstOrDefault( + x => x.Name.ToLowerInvariant() == "port" + ); if (portOption?.OptionValue != null) { return portOption.OptionValue as string; @@ -463,11 +517,12 @@ public void SetSharedFolderCategoryVisible(SharedFolderType type, bool visible) } SaveSettings(); } - + public bool IsSharedFolderCategoryVisible(SharedFolderType type) { // False for default - if (type == 0) return false; + if (type == 0) + return false; return Settings.SharedFolderVisibleCategories?.HasFlag(type) ?? false; } @@ -488,7 +543,7 @@ public bool IsEulaAccepted() public void SetEulaAccepted() { - var globalSettings = new GlobalSettings {EulaAccepted = true}; + var globalSettings = new GlobalSettings { EulaAccepted = true }; var json = JsonSerializer.Serialize(globalSettings); File.WriteAllText(GlobalSettingsPath, json); } @@ -504,11 +559,15 @@ public void IndexCheckpoints() var modelHashes = new HashSet(); var sharedModelDirectory = Path.Combine(LibraryDir, "Models"); - - if (!Directory.Exists(sharedModelDirectory)) return; - - var connectedModelJsons = Directory.GetFiles(sharedModelDirectory, "*.cm-info.json", - SearchOption.AllDirectories); + + if (!Directory.Exists(sharedModelDirectory)) + return; + + var connectedModelJsons = Directory.GetFiles( + sharedModelDirectory, + "*.cm-info.json", + SearchOption.AllDirectories + ); foreach (var jsonFile in connectedModelJsons) { var json = File.ReadAllText(jsonFile); @@ -521,7 +580,7 @@ public void IndexCheckpoints() } Transaction(s => s.InstalledModelHashes = modelHashes); - + sw.Stop(); Logger.Info($"Indexed {modelHashes.Count} checkpoints in {sw.ElapsedMilliseconds}ms"); } @@ -548,9 +607,10 @@ protected virtual void LoadSettings() var modifiedDefaultSerializerOptions = SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions(); modifiedDefaultSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - Settings = - JsonSerializer.Deserialize(settingsContent, - modifiedDefaultSerializerOptions)!; + Settings = JsonSerializer.Deserialize( + settingsContent, + modifiedDefaultSerializerOptions + )!; Loaded?.Invoke(this, EventArgs.Empty); } @@ -569,12 +629,15 @@ protected virtual void SaveSettings() { File.Create(SettingsPath).Close(); } - - var json = JsonSerializer.Serialize(Settings, new JsonSerializerOptions - { - WriteIndented = true, - Converters = { new JsonStringEnumConverter() } - }); + + var json = JsonSerializer.Serialize( + Settings, + new JsonSerializerOptions + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter() } + } + ); File.WriteAllText(SettingsPath, json); } finally @@ -588,4 +651,3 @@ private Task SaveSettingsAsync() return Task.Run(SaveSettings); } } - From 39553b67010b181d08bd8247c8a108ddffdd2a87 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 6 Sep 2023 15:46:16 -0400 Subject: [PATCH 231/474] Add SettingsManager.ImagesInferenceDirectory --- StabilityMatrix.Core/Services/ISettingsManager.cs | 1 + StabilityMatrix.Core/Services/SettingsManager.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/StabilityMatrix.Core/Services/ISettingsManager.cs b/StabilityMatrix.Core/Services/ISettingsManager.cs index 6d1e1f9d4..0705205da 100644 --- a/StabilityMatrix.Core/Services/ISettingsManager.cs +++ b/StabilityMatrix.Core/Services/ISettingsManager.cs @@ -16,6 +16,7 @@ public interface ISettingsManager string DownloadsDirectory { get; } DirectoryPath TagsDirectory { get; } DirectoryPath ImagesDirectory { get; } + DirectoryPath ImagesInferenceDirectory { get; } Settings Settings { get; } diff --git a/StabilityMatrix.Core/Services/SettingsManager.cs b/StabilityMatrix.Core/Services/SettingsManager.cs index a3f2a7dec..e277a52a8 100644 --- a/StabilityMatrix.Core/Services/SettingsManager.cs +++ b/StabilityMatrix.Core/Services/SettingsManager.cs @@ -67,6 +67,7 @@ private set public DirectoryPath TagsDirectory => new(LibraryDir, "Tags"); public DirectoryPath ImagesDirectory => new(LibraryDir, "Images"); + public DirectoryPath ImagesInferenceDirectory => ImagesDirectory.JoinDir("Inference"); public Settings Settings { get; private set; } = new(); From 97e5aa8ca9f6a715639ef9aa892de83db3fe76c8 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 6 Sep 2023 20:29:04 -0400 Subject: [PATCH 232/474] Add keybindings for inference menu --- StabilityMatrix.Avalonia/Views/InferencePage.axaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml b/StabilityMatrix.Avalonia/Views/InferencePage.axaml index 90cc75b6d..068ea2c29 100644 --- a/StabilityMatrix.Avalonia/Views/InferencePage.axaml +++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml @@ -18,6 +18,12 @@ d:DesignWidth="1000" x:DataType="vm:InferenceViewModel" mc:Ignorable="d"> + + + + + + From 8a3bce388c2343ffaab3aec243a6016640f98a47 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 6 Sep 2023 20:29:57 -0400 Subject: [PATCH 233/474] Set client output images directory on connect --- StabilityMatrix.Avalonia/Services/InferenceClientManager.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs index 8a7440a2f..e35b537bc 100644 --- a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs @@ -271,6 +271,11 @@ public async Task ConnectAsync( var uri = new UriBuilder("http", host, int.Parse(port)).Uri; await ConnectAsyncImpl(uri, cancellationToken); + + // Set output path + Client!.OutputImagesDir = new DirectoryPath(packagePair.InstalledPackage.FullPath).JoinDir( + "output" + ); } public async Task CloseAsync() From e1b6b999c6de9886eab9172aade888ef74f2fba2 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 7 Sep 2023 02:17:23 -0400 Subject: [PATCH 234/474] Add ImageIndexService and Inference Image Viewer --- StabilityMatrix.Avalonia/App.axaml | 1 + StabilityMatrix.Avalonia/App.axaml.cs | 4 + .../Controls/ImageFolderCard.axaml | 62 +++++ .../Controls/ImageFolderCard.axaml.cs | 27 +++ .../DesignData/DesignData.cs | 4 + .../DesignData/MockImageIndexService.cs | 47 ++++ .../DesignData/MockLiteDbContext.cs | 2 + .../Extensions/ComfyNodeBuilderExtensions.cs | 2 +- .../Services/InferenceClientManager.cs | 15 +- .../Inference/ImageFolderCardItemViewModel.cs | 14 ++ .../Inference/ImageFolderCardViewModel.cs | 83 +++++++ .../InferenceTextToImageViewModel.cs | 5 + .../Inference/InferenceTextToImageView.axaml | 33 ++- .../Database/ILiteDbContext.cs | 1 + .../Database/LiteDbContext.cs | 2 + StabilityMatrix.Core/Helper/SharedFolders.cs | 121 +++++++--- .../Models/Database/LocalImageFile.cs | 48 ++++ .../Models/Database/LocalImageFileType.cs | 14 ++ .../Models/Packages/ComfyUI.cs | 212 ++++++++++-------- .../Services/IImageIndexService.cs | 21 ++ .../Services/ImageIndexService.cs | 111 +++++++++ .../StabilityMatrix.Core.csproj | 1 + 22 files changed, 702 insertions(+), 128 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml create mode 100644 StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs create mode 100644 StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs create mode 100644 StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardItemViewModel.cs create mode 100644 StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs create mode 100644 StabilityMatrix.Core/Models/Database/LocalImageFile.cs create mode 100644 StabilityMatrix.Core/Models/Database/LocalImageFileType.cs create mode 100644 StabilityMatrix.Core/Services/IImageIndexService.cs create mode 100644 StabilityMatrix.Core/Services/ImageIndexService.cs diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml index 65d450154..60c950f39 100644 --- a/StabilityMatrix.Avalonia/App.axaml +++ b/StabilityMatrix.Avalonia/App.axaml @@ -50,5 +50,6 @@ + diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index edbd20457..9c0e21f1d 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -307,6 +307,7 @@ internal static void ConfigureDialogViewModels(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -337,6 +338,7 @@ internal static void ConfigureDialogViewModels(IServiceCollection services) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) + .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) @@ -369,6 +371,7 @@ internal static void ConfigureViews(IServiceCollection services) // Inference controls services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -434,6 +437,7 @@ private static IServiceCollection ConfigureServices() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton( diff --git a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml new file mode 100644 index 000000000..0e292e4f0 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs new file mode 100644 index 000000000..1fd322452 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs @@ -0,0 +1,27 @@ +using AsyncAwaitBestPractices; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Threading; +using StabilityMatrix.Avalonia.ViewModels.Base; + +namespace StabilityMatrix.Avalonia.Controls; + +public class ImageFolderCard : TemplatedControl +{ + /// + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + if (DataContext is ViewModelBase vm) + { + vm.OnLoaded(); + Dispatcher.UIThread + .InvokeAsync(async () => + { + await vm.OnLoadedAsync(); + }) + .SafeFireAndForget(); + } + } +} diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 0f8bae403..c9f61a91b 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -104,6 +104,7 @@ public static void Initialize() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton(); // Placeholder services that nobody should need during design time @@ -511,6 +512,9 @@ public static PackageManagerViewModel PackageManagerViewModel ); }); + public static ImageFolderCardViewModel ImageFolderCardViewModel => + DialogFactory.Get(); + public static PromptCardViewModel PromptCardViewModel => DialogFactory.Get(vm => { diff --git a/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs b/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs new file mode 100644 index 000000000..09517086c --- /dev/null +++ b/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using StabilityMatrix.Core.Models.Database; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.DesignData; + +public class MockImageIndexService : IImageIndexService +{ + /// + public Task> GetLocalImagesByPrefix(string pathPrefix) + { + return Task.FromResult( + (IReadOnlyList) + new LocalImageFile[] + { + new() + { + RelativePath = + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/4a7e00a7-6f18-42d4-87c0-10e792df2640/width=1152", + }, + new() + { + RelativePath = + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1024", + }, + new() + { + RelativePath = + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/16588c94-6595-4be9-8806-d7e6e22d198c/width=1152", + } + } + ); + } + + /// + public Task RefreshIndex(string subPath = "") + { + return Task.CompletedTask; + } + + /// + public void BackgroundRefreshIndex() + { + throw new System.NotImplementedException(); + } +} diff --git a/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs b/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs index fe5995d36..08e8efc02 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs @@ -20,6 +20,8 @@ public class MockLiteDbContext : ILiteDbContext throw new NotImplementedException(); public ILiteCollectionAsync InferenceProjects => throw new NotImplementedException(); + public ILiteCollectionAsync LocalImageFiles => + throw new NotImplementedException(); public Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync( string hashBlake3 diff --git a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs index 9813c531b..df81d4827 100644 --- a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs +++ b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs @@ -266,7 +266,7 @@ public static string SetupOutputImage(this ComfyNodeBuilder builder) ClassType = "SaveImage", Inputs = new Dictionary { - ["filename_prefix"] = "SM-Inference", + ["filename_prefix"] = "Inference/TextToImage", ["images"] = builder.Connections.Image } } diff --git a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs index e35b537bc..926fc3a34 100644 --- a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs @@ -1,11 +1,6 @@ using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; -using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; @@ -13,12 +8,10 @@ using DynamicData; using DynamicData.Binding; using Microsoft.Extensions.Logging; -using StabilityMatrix.Avalonia.ViewModels.PackageManager; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Inference; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; -using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Services; @@ -258,11 +251,17 @@ public async Task ConnectAsync( if (IsConnected) return; - if (packagePair.BasePackage is not ComfyUI) + if (packagePair.BasePackage is not ComfyUI comfyPackage) { throw new ArgumentException("Base package is not ComfyUI", nameof(packagePair)); } + // Setup image folder links + await comfyPackage.SetupInferenceOutputFolderLinks( + packagePair.InstalledPackage.FullPath + ?? throw new InvalidOperationException("Package does not have a Path") + ); + // Get user defined host and port var host = packagePair.InstalledPackage.GetLaunchArgsHost() ?? "127.0.0.1"; host = host.Replace("localhost", "127.0.0.1"); diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardItemViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardItemViewModel.cs new file mode 100644 index 000000000..0cd35806a --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardItemViewModel.cs @@ -0,0 +1,14 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Core.Models.Database; + +namespace StabilityMatrix.Avalonia.ViewModels.Inference; + +public partial class ImageFolderCardItemViewModel : ViewModelBase +{ + [ObservableProperty] + private LocalImageFile? localImageFile; + + [ObservableProperty] + private string? imagePath; +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs new file mode 100644 index 000000000..96f38a140 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs @@ -0,0 +1,83 @@ +using System; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls; +using DynamicData; +using DynamicData.Binding; +using Microsoft.Extensions.Logging; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Models.Database; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.ViewModels.Inference; + +[View(typeof(ImageFolderCard))] +public partial class ImageFolderCardViewModel : ViewModelBase +{ + private readonly ILogger logger; + private readonly IImageIndexService imageIndexService; + private readonly ISettingsManager settingsManager; + + /// + /// Source of image files to display + /// + private readonly SourceCache localImagesSource = + new(imageFile => imageFile.RelativePath); + + /// + /// Collection of image files to display + /// + public IObservableCollection LocalImages { get; } = + new ObservableCollectionExtended(); + + /// + /// Collection of image items to display + /// + public IObservableCollection Items { get; } = + new ObservableCollectionExtended(); + + public ImageFolderCardViewModel( + ILogger logger, + IImageIndexService imageIndexService, + ISettingsManager settingsManager + ) + { + this.logger = logger; + this.imageIndexService = imageIndexService; + this.settingsManager = settingsManager; + + localImagesSource + .Connect() + .DeferUntilLoaded() + .Transform( + imageFile => + new ImageFolderCardItemViewModel + { + LocalImageFile = imageFile, + ImagePath = Design.IsDesignMode + ? imageFile.RelativePath + : imageFile.GetFullPath(settingsManager.ImagesDirectory) + } + ) + .Bind(Items) + .Subscribe(); + } + + /// + public override async Task OnLoadedAsync() + { + await base.OnLoadedAsync(); + + await imageIndexService.RefreshIndex("Inference"); + + var imageFiles = await imageIndexService.GetLocalImagesByPrefix("Inference"); + + localImagesSource.Edit(x => + { + x.Load(imageFiles); + }); + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 43dec13b5..25884a829 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -57,6 +57,9 @@ public partial class InferenceTextToImageViewModel : InferenceTabViewModelBase [JsonPropertyName("ImageGallery")] public ImageGalleryCardViewModel ImageGalleryCardViewModel { get; } + [JsonPropertyName("ImageFolder")] + public ImageFolderCardViewModel ImageFolderCardViewModel { get; } + [JsonPropertyName("Prompt")] public PromptCardViewModel PromptCardViewModel { get; } @@ -122,6 +125,7 @@ IModelIndexService modelIndexService }); ImageGalleryCardViewModel = vmFactory.Get(); + ImageFolderCardViewModel = vmFactory.Get(); PromptCardViewModel = vmFactory.Get(); HiresSamplerCardViewModel = vmFactory.Get(samplerCard => { @@ -475,6 +479,7 @@ private async Task GenerateImage( }; await GenerateImageImpl(overrides, cancellationToken); + await ImageFolderCardViewModel.OnLoadedAsync(); } catch (OperationCanceledException e) { diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml b/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml index 318fdce01..e2dd25f99 100644 --- a/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml +++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml @@ -65,7 +65,7 @@ x:Name="ConfigPane" Alignment="Left" Id="ConfigPane" - Proportion="0.25"> + Proportion="0.2"> + Proportion="0.3"> - + - + + Proportion="0.3"> + + + + + + + + + + + + diff --git a/StabilityMatrix.Core/Database/ILiteDbContext.cs b/StabilityMatrix.Core/Database/ILiteDbContext.cs index c4defc173..3e4288157 100644 --- a/StabilityMatrix.Core/Database/ILiteDbContext.cs +++ b/StabilityMatrix.Core/Database/ILiteDbContext.cs @@ -13,6 +13,7 @@ public interface ILiteDbContext : IDisposable ILiteCollectionAsync CivitModelQueryCache { get; } ILiteCollectionAsync LocalModelFiles { get; } ILiteCollectionAsync InferenceProjects { get; } + ILiteCollectionAsync LocalImageFiles { get; } Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3); Task UpsertCivitModelAsync(CivitModel civitModel); diff --git a/StabilityMatrix.Core/Database/LiteDbContext.cs b/StabilityMatrix.Core/Database/LiteDbContext.cs index 2da50072c..fdf946f6f 100644 --- a/StabilityMatrix.Core/Database/LiteDbContext.cs +++ b/StabilityMatrix.Core/Database/LiteDbContext.cs @@ -35,6 +35,8 @@ public class LiteDbContext : ILiteDbContext Database.GetCollection("LocalModelFiles"); public ILiteCollectionAsync InferenceProjects => Database.GetCollection("InferenceProjects"); + public ILiteCollectionAsync LocalImageFiles => + Database.GetCollection("LocalImageFiles"); public LiteDbContext( ILogger logger, diff --git a/StabilityMatrix.Core/Helper/SharedFolders.cs b/StabilityMatrix.Core/Helper/SharedFolders.cs index 1ad33ec75..85122290d 100644 --- a/StabilityMatrix.Core/Helper/SharedFolders.cs +++ b/StabilityMatrix.Core/Helper/SharedFolders.cs @@ -1,4 +1,5 @@ -using NLog; +using System.Diagnostics.CodeAnalysis; +using NLog; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; @@ -20,9 +21,9 @@ public SharedFolders(ISettingsManager settingsManager, IPackageFactory packageFa this.settingsManager = settingsManager; this.packageFactory = packageFactory; } - + // Platform redirect for junctions / symlinks - private static void CreateLinkOrJunction(string junctionDir, string targetDir, bool overwrite) + public static void CreateLinkOrJunction(string junctionDir, string targetDir, bool overwrite) { if (Compat.IsWindows) { @@ -36,8 +37,54 @@ private static void CreateLinkOrJunction(string junctionDir, string targetDir, b } } - public static void SetupLinks(Dictionary> definitions, - DirectoryPath modelsDirectory, DirectoryPath installDirectory) + /// + /// Creates a junction link from the source to the destination. + /// Moves destination files to source if they exist. + /// + /// Shared source (i.e. "Models/") + /// Destination (i.e. "webui/models/lora") + public static void CreateLinkOrJunctionWithMove( + DirectoryPath sourceDir, + DirectoryPath destinationDir + ) + { + // Create source folder if it doesn't exist + if (!sourceDir.Exists) + { + Logger.Info($"Creating junction source {sourceDir}"); + sourceDir.Create(); + } + // Delete the destination folder if it exists + if (destinationDir.Exists) + { + // Copy all files from destination to source + Logger.Info($"Copying files from {destinationDir} to {sourceDir}"); + foreach (var file in destinationDir.Info.EnumerateFiles()) + { + var sourceFile = sourceDir + file; + var destinationFile = destinationDir + file; + // Skip name collisions + if (File.Exists(sourceFile)) + { + Logger.Warn( + $"Skipping file {file.FullName} because it already exists in {sourceDir}" + ); + continue; + } + destinationFile.Info.MoveTo(sourceFile); + } + Logger.Info($"Deleting junction target {destinationDir}"); + destinationDir.Delete(true); + } + Logger.Info($"Creating junction link from {sourceDir} to {destinationDir}"); + CreateLinkOrJunction(destinationDir, sourceDir, true); + } + + public static void SetupLinks( + Dictionary> definitions, + DirectoryPath modelsDirectory, + DirectoryPath installDirectory + ) { foreach (var (folderType, relativePaths) in definitions) { @@ -63,7 +110,9 @@ public static void SetupLinks(Dictionary // Skip name collisions if (File.Exists(sourceFile)) { - Logger.Warn($"Skipping file {file.FullName} because it already exists in {sourceDir}"); + Logger.Warn( + $"Skipping file {file.FullName} because it already exists in {sourceDir}" + ); continue; } destinationFile.Info.MoveTo(sourceFile); @@ -81,19 +130,25 @@ public void SetupLinksForPackage(BasePackage basePackage, DirectoryPath installD { var modelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory); var sharedFolders = basePackage.SharedFolders; - if (sharedFolders == null) return; + if (sharedFolders == null) + return; SetupLinks(sharedFolders, modelsDirectory, installDirectory); } - + /// - /// Deletes junction links and remakes them. Unlike SetupLinksForPackage, + /// Deletes junction links and remakes them. Unlike SetupLinksForPackage, /// this will not copy files from the destination to the source. /// - public static async Task UpdateLinksForPackage(BasePackage basePackage, DirectoryPath modelsDirectory, DirectoryPath installDirectory) + public static async Task UpdateLinksForPackage( + BasePackage basePackage, + DirectoryPath modelsDirectory, + DirectoryPath installDirectory + ) { var sharedFolders = basePackage.SharedFolders; - if (sharedFolders is null) return; - + if (sharedFolders is null) + return; + foreach (var (folderType, relativePaths) in sharedFolders) { foreach (var relativePath in relativePaths) @@ -117,7 +172,8 @@ public static async Task UpdateLinksForPackage(BasePackage basePackage, Director if (destinationDir.Info.LinkTarget == sourceDir) { Logger.Info( - $"Skipped updating matching folder link ({destinationDir} -> ({sourceDir})"); + $"Skipped updating matching folder link ({destinationDir} -> ({sourceDir})" + ); return; } @@ -131,8 +187,12 @@ public static async Task UpdateLinksForPackage(BasePackage basePackage, Director if (destinationDir.Info.EnumerateFileSystemInfos().Any()) { Logger.Info($"Moving files from {destinationDir} to {sourceDir}"); - await FileTransfers.MoveAllFilesAndDirectories( - destinationDir, sourceDir, overwriteIfHashMatches: true) + await FileTransfers + .MoveAllFilesAndDirectories( + destinationDir, + sourceDir, + overwriteIfHashMatches: true + ) .ConfigureAwait(false); } @@ -154,15 +214,16 @@ public static void RemoveLinksForPackage(BasePackage package, DirectoryPath inst { return; } - + foreach (var (_, relativePaths) in sharedFolders) { foreach (var relativePath in relativePaths) { var destination = Path.GetFullPath(Path.Combine(installPath, relativePath)); // Delete the destination folder if it exists - if (!Directory.Exists(destination)) continue; - + if (!Directory.Exists(destination)) + continue; + Logger.Info($"Deleting junction target {destination}"); Directory.Delete(destination, false); } @@ -174,19 +235,26 @@ public void RemoveLinksForAllPackages() var packages = settingsManager.Settings.InstalledPackages; foreach (var package in packages) { - if (package.PackageName == null) continue; + if (package.PackageName == null) + continue; var basePackage = packageFactory[package.PackageName]; - if (basePackage == null) continue; - if (package.LibraryPath == null) continue; - + if (basePackage == null) + continue; + if (package.LibraryPath == null) + continue; + try { basePackage.RemoveModelFolderLinks(package.FullPath).GetAwaiter().GetResult(); } catch (Exception e) { - Logger.Warn("Failed to remove links for package {Package} " + - "({DisplayName}): {Message}", package.PackageName, package.DisplayName, e.Message); + Logger.Warn( + "Failed to remove links for package {Package} " + "({DisplayName}): {Message}", + package.PackageName, + package.DisplayName, + e.Message + ); } } } @@ -194,8 +262,9 @@ public void RemoveLinksForAllPackages() public void SetupSharedModelFolders() { var modelsDir = settingsManager.ModelsDirectory; - if (string.IsNullOrWhiteSpace(modelsDir)) return; - + if (string.IsNullOrWhiteSpace(modelsDir)) + return; + Directory.CreateDirectory(modelsDir); var allSharedFolderTypes = Enum.GetValues(); foreach (var sharedFolder in allSharedFolderTypes) diff --git a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs new file mode 100644 index 000000000..ea0c63044 --- /dev/null +++ b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs @@ -0,0 +1,48 @@ +using LiteDB; + +namespace StabilityMatrix.Core.Models.Database; + +/// +/// Represents a locally indexed image file. +/// +public class LocalImageFile +{ + /// + /// Relative path of the file from the root images directory ("%LIBRARY%/Images"). + /// + [BsonId] + public required string RelativePath { get; set; } + + /// + /// Type of the model file. + /// + public LocalImageFileType ImageType { get; set; } + + /// + /// Creation time of the file. + /// + public DateTimeOffset CreatedAt { get; set; } + + /// + /// Last modified time of the file. + /// + public DateTimeOffset LastModifiedAt { get; set; } + + /// + /// File name of the relative path. + /// + public string FileName => Path.GetFileName(RelativePath); + + /// + /// File name of the relative path without extension. + /// + public string FileNameWithoutExtension => Path.GetFileNameWithoutExtension(RelativePath); + + public string GetFullPath(string rootImageDirectory) + { + return Path.Combine(rootImageDirectory, RelativePath); + } + + public static readonly HashSet SupportedImageExtensions = + new() { ".png", ".jpg", ".jpeg", ".webp" }; +} diff --git a/StabilityMatrix.Core/Models/Database/LocalImageFileType.cs b/StabilityMatrix.Core/Models/Database/LocalImageFileType.cs new file mode 100644 index 000000000..3a46fdb53 --- /dev/null +++ b/StabilityMatrix.Core/Models/Database/LocalImageFileType.cs @@ -0,0 +1,14 @@ +namespace StabilityMatrix.Core.Models.Database; + +[Flags] +public enum LocalImageFileType : ulong +{ + // Source + Automatic = 1 << 1, + Comfy = 1 << 2, + Inference = 1 << 3, + + // Generation Type + TextToImage = 1 << 10, + ImageToImage = 1 << 11 +} diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index f02e0151e..86ea19800 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -20,7 +20,7 @@ public class ComfyUI : BaseGitPackage public override string DisplayName { get; set; } = "ComfyUI"; public override string Author => "comfyanonymous"; public override string LicenseType => "GPL-3.0"; - public override string LicenseUrl => + public override string LicenseUrl => "https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE"; public override string Blurb => "A powerful and modular stable diffusion GUI and backend"; public override string LaunchCommand => "main.py"; @@ -29,81 +29,88 @@ public class ComfyUI : BaseGitPackage new("https://github.com/comfyanonymous/ComfyUI/raw/master/comfyui_screenshot.png"); public override bool ShouldIgnoreReleases => true; - public ComfyUI(IGithubApiCache githubApi, ISettingsManager settingsManager, IDownloadService downloadService, - IPrerequisiteHelper prerequisiteHelper) : - base(githubApi, settingsManager, downloadService, prerequisiteHelper) - { - } + public ComfyUI( + IGithubApiCache githubApi, + ISettingsManager settingsManager, + IDownloadService downloadService, + IPrerequisiteHelper prerequisiteHelper + ) + : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { } // https://github.com/comfyanonymous/ComfyUI/blob/master/folder_paths.py#L11 - public override Dictionary> SharedFolders => new() - { - [SharedFolderType.StableDiffusion] = new[] {"models/checkpoints"}, - [SharedFolderType.Diffusers] = new[] {"models/diffusers"}, - [SharedFolderType.Lora] = new[] {"models/loras"}, - [SharedFolderType.CLIP] = new[] {"models/clip"}, - [SharedFolderType.TextualInversion] = new[] {"models/embeddings"}, - [SharedFolderType.VAE] = new[] {"models/vae"}, - [SharedFolderType.ApproxVAE] = new[] {"models/vae_approx"}, - [SharedFolderType.ControlNet] = new[] {"models/controlnet"}, - [SharedFolderType.GLIGEN] = new[] {"models/gligen"}, - [SharedFolderType.ESRGAN] = new[] {"models/upscale_models"}, - [SharedFolderType.Hypernetwork] = new[] {"models/hypernetworks"}, - }; - - public override List LaunchOptions => new List - { + public override Dictionary> SharedFolders => new() { - Name = "VRAM", - Type = LaunchOptionType.Bool, - InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch + [SharedFolderType.StableDiffusion] = new[] { "models/checkpoints" }, + [SharedFolderType.Diffusers] = new[] { "models/diffusers" }, + [SharedFolderType.Lora] = new[] { "models/loras" }, + [SharedFolderType.CLIP] = new[] { "models/clip" }, + [SharedFolderType.TextualInversion] = new[] { "models/embeddings" }, + [SharedFolderType.VAE] = new[] { "models/vae" }, + [SharedFolderType.ApproxVAE] = new[] { "models/vae_approx" }, + [SharedFolderType.ControlNet] = new[] { "models/controlnet" }, + [SharedFolderType.GLIGEN] = new[] { "models/gligen" }, + [SharedFolderType.ESRGAN] = new[] { "models/upscale_models" }, + [SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }, + }; + + public override List LaunchOptions => + new List + { + new() { - Level.Low => "--lowvram", - Level.Medium => "--normalvram", - _ => null + Name = "VRAM", + Type = LaunchOptionType.Bool, + InitialValue = HardwareHelper + .IterGpuInfo() + .Select(gpu => gpu.MemoryLevel) + .Max() switch + { + Level.Low => "--lowvram", + Level.Medium => "--normalvram", + _ => null + }, + Options = { "--highvram", "--normalvram", "--lowvram", "--novram" } }, - Options = { "--highvram", "--normalvram", "--lowvram", "--novram" } - }, - new() - { - Name = "Use CPU only", - Type = LaunchOptionType.Bool, - InitialValue = !HardwareHelper.HasNvidiaGpu(), - Options = {"--cpu"} - }, - new() - { - Name = "Disable Xformers", - Type = LaunchOptionType.Bool, - InitialValue = !HardwareHelper.HasNvidiaGpu(), - Options = { "--disable-xformers" } - }, - new() - { - Name = "Auto-Launch", - Type = LaunchOptionType.Bool, - Options = { "--auto-launch" } - }, - LaunchOptionDefinition.Extras - }; + new() + { + Name = "Use CPU only", + Type = LaunchOptionType.Bool, + InitialValue = !HardwareHelper.HasNvidiaGpu(), + Options = { "--cpu" } + }, + new() + { + Name = "Disable Xformers", + Type = LaunchOptionType.Bool, + InitialValue = !HardwareHelper.HasNvidiaGpu(), + Options = { "--disable-xformers" } + }, + new() + { + Name = "Auto-Launch", + Type = LaunchOptionType.Bool, + Options = { "--auto-launch" } + }, + LaunchOptionDefinition.Extras + }; public override Task GetLatestVersion() => Task.FromResult("master"); - public override async Task> GetAllVersions(bool isReleaseMode = true) + public override async Task> GetAllVersions( + bool isReleaseMode = true + ) { var allBranches = await GetAllBranches().ConfigureAwait(false); - return allBranches.Select(b => new PackageVersion - { - TagName = $"{b.Name}", - ReleaseNotesMarkdown = string.Empty - }); + return allBranches.Select( + b => new PackageVersion { TagName = $"{b.Name}", ReleaseNotesMarkdown = string.Empty } + ); } public override async Task InstallPackage(IProgress? progress = null) { await UnzipPackage(progress); - + progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true)); // Setup venv await using var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv")); @@ -114,46 +121,62 @@ public override async Task InstallPackage(IProgress? progress = var gpus = HardwareHelper.IterGpuInfo().ToList(); if (gpus.Any(g => g.IsNvidia)) { - progress?.Report(new ProgressReport(-1, "Installing PyTorch for CUDA", isIndeterminate: true)); - + progress?.Report( + new ProgressReport(-1, "Installing PyTorch for CUDA", isIndeterminate: true) + ); + Logger.Info("Starting torch install (CUDA)..."); - await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCuda, OnConsoleOutput) + await venvRunner + .PipInstall(PyVenvRunner.TorchPipInstallArgsCuda, OnConsoleOutput) .ConfigureAwait(false); - + Logger.Info("Installing xformers..."); await venvRunner.PipInstall("xformers", OnConsoleOutput).ConfigureAwait(false); } else if (HardwareHelper.PreferRocm()) { - progress?.Report(new ProgressReport(-1, "Installing PyTorch for ROCm", isIndeterminate: true)); + progress?.Report( + new ProgressReport(-1, "Installing PyTorch for ROCm", isIndeterminate: true) + ); await venvRunner .PipInstall(PyVenvRunner.TorchPipInstallArgsRocm542, OnConsoleOutput) .ConfigureAwait(false); } else { - progress?.Report(new ProgressReport(-1, "Installing PyTorch for CPU", isIndeterminate: true)); + progress?.Report( + new ProgressReport(-1, "Installing PyTorch for CPU", isIndeterminate: true) + ); Logger.Info("Starting torch install (CPU)..."); - await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, OnConsoleOutput) + await venvRunner + .PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, OnConsoleOutput) .ConfigureAwait(false); } // Install requirements file - progress?.Report(new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true)); + progress?.Report( + new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true) + ); Logger.Info("Installing requirements.txt"); await venvRunner.PipInstall($"-r requirements.txt", OnConsoleOutput).ConfigureAwait(false); - - progress?.Report(new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false)); + + progress?.Report( + new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false) + ); } - - public override async Task RunPackage(string installedPackagePath, string command, string arguments) + + public override async Task RunPackage( + string installedPackagePath, + string command, + string arguments + ) { await SetupVenv(installedPackagePath).ConfigureAwait(false); void HandleConsoleOutput(ProcessOutput s) { OnConsoleOutput(s); - + if (s.Text.Contains("To see the GUI go to", StringComparison.OrdinalIgnoreCase)) { var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); @@ -174,10 +197,7 @@ void HandleExit(int i) var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}"; - VenvRunner?.RunDetached( - args.TrimEnd(), - HandleConsoleOutput, - HandleExit); + VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit); } public override Task SetupModelFolders(DirectoryPath installDirectory) @@ -197,19 +217,22 @@ public override Task SetupModelFolders(DirectoryPath installDirectory) File.WriteAllText(extraPathsYamlPath, string.Empty); } var yaml = File.ReadAllText(extraPathsYamlPath); - var comfyModelPaths = deserializer.Deserialize(yaml) ?? - // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - // cuz it can actually be null lol - new ComfyModelPathsYaml(); - + var comfyModelPaths = + deserializer.Deserialize(yaml) + ?? + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract + // cuz it can actually be null lol + new ComfyModelPathsYaml(); + comfyModelPaths.StabilityMatrix ??= new ComfyModelPathsYaml.SmData(); comfyModelPaths.StabilityMatrix.Checkpoints = Path.Combine(modelsDir, "StableDiffusion"); comfyModelPaths.StabilityMatrix.Vae = Path.Combine(modelsDir, "VAE"); - comfyModelPaths.StabilityMatrix.Loras = $"{Path.Combine(modelsDir, "Lora")}\n" + - $"{Path.Combine(modelsDir, "LyCORIS")}"; - comfyModelPaths.StabilityMatrix.UpscaleModels = $"{Path.Combine(modelsDir, "ESRGAN")}\n" + - $"{Path.Combine(modelsDir, "RealESRGAN")}\n" + - $"{Path.Combine(modelsDir, "SwinIR")}"; + comfyModelPaths.StabilityMatrix.Loras = + $"{Path.Combine(modelsDir, "Lora")}\n" + $"{Path.Combine(modelsDir, "LyCORIS")}"; + comfyModelPaths.StabilityMatrix.UpscaleModels = + $"{Path.Combine(modelsDir, "ESRGAN")}\n" + + $"{Path.Combine(modelsDir, "RealESRGAN")}\n" + + $"{Path.Combine(modelsDir, "SwinIR")}"; comfyModelPaths.StabilityMatrix.Embeddings = Path.Combine(modelsDir, "TextualInversion"); comfyModelPaths.StabilityMatrix.Hypernetworks = Path.Combine(modelsDir, "Hypernetwork"); comfyModelPaths.StabilityMatrix.Controlnet = Path.Combine(modelsDir, "ControlNet"); @@ -217,7 +240,7 @@ public override Task SetupModelFolders(DirectoryPath installDirectory) comfyModelPaths.StabilityMatrix.Diffusers = Path.Combine(modelsDir, "Diffusers"); comfyModelPaths.StabilityMatrix.Gligen = Path.Combine(modelsDir, "GLIGEN"); comfyModelPaths.StabilityMatrix.VaeApprox = Path.Combine(modelsDir, "ApproxVAE"); - + var serializer = new SerializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) .Build(); @@ -233,6 +256,19 @@ public override Task UpdateModelFolders(DirectoryPath installDirectory) => public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) => Task.CompletedTask; + public async Task SetupInferenceOutputFolderLinks(DirectoryPath installDirectory) + { + var inferenceDir = installDirectory.JoinDir("output", "Inference"); + + var sharedInferenceDir = SettingsManager.ImagesInferenceDirectory; + + await Task.Run(() => + { + Helper.SharedFolders.CreateLinkOrJunctionWithMove(sharedInferenceDir, inferenceDir); + }) + .ConfigureAwait(false); + } + public class ComfyModelPathsYaml { public class SmData diff --git a/StabilityMatrix.Core/Services/IImageIndexService.cs b/StabilityMatrix.Core/Services/IImageIndexService.cs new file mode 100644 index 000000000..19849b8f5 --- /dev/null +++ b/StabilityMatrix.Core/Services/IImageIndexService.cs @@ -0,0 +1,21 @@ +using StabilityMatrix.Core.Models.Database; + +namespace StabilityMatrix.Core.Services; + +public interface IImageIndexService +{ + /// + /// Gets a list of local images that start with the given path prefix + /// + Task> GetLocalImagesByPrefix(string pathPrefix); + + /// + /// Refreshes the index of local images + /// + Task RefreshIndex(string subPath = ""); + + /// + /// Refreshes the index of local images in the background + /// + void BackgroundRefreshIndex(); +} diff --git a/StabilityMatrix.Core/Services/ImageIndexService.cs b/StabilityMatrix.Core/Services/ImageIndexService.cs new file mode 100644 index 000000000..a7bffe733 --- /dev/null +++ b/StabilityMatrix.Core/Services/ImageIndexService.cs @@ -0,0 +1,111 @@ +using System.Diagnostics; +using AsyncAwaitBestPractices; +using Microsoft.Extensions.Logging; +using StabilityMatrix.Core.Database; +using StabilityMatrix.Core.Models.Database; +using StabilityMatrix.Core.Models.FileInterfaces; + +namespace StabilityMatrix.Core.Services; + +public class ImageIndexService : IImageIndexService +{ + private readonly ILogger logger; + private readonly ILiteDbContext liteDbContext; + private readonly ISettingsManager settingsManager; + + public ImageIndexService( + ILogger logger, + ILiteDbContext liteDbContext, + ISettingsManager settingsManager + ) + { + this.logger = logger; + this.liteDbContext = liteDbContext; + this.settingsManager = settingsManager; + } + + /// + public async Task> GetLocalImagesByPrefix(string pathPrefix) + { + return await liteDbContext.LocalImageFiles + .Query() + .Where(imageFile => imageFile.RelativePath.StartsWith(pathPrefix)) + .ToArrayAsync() + .ConfigureAwait(false); + } + + /// + public async Task RefreshIndex(string subPath = "") + { + var imagesDir = settingsManager.ImagesDirectory; + + // 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; + + var localImage = new LocalImageFile + { + RelativePath = relativePath, + ImageType = imageType + }; + + // 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() + { + RefreshIndex().SafeFireAndForget(); + } +} diff --git a/StabilityMatrix.Core/StabilityMatrix.Core.csproj b/StabilityMatrix.Core/StabilityMatrix.Core.csproj index d3d20b275..9648e9ced 100644 --- a/StabilityMatrix.Core/StabilityMatrix.Core.csproj +++ b/StabilityMatrix.Core/StabilityMatrix.Core.csproj @@ -21,6 +21,7 @@ + From 5d6791f1e1de27173faeb6f0e7d482e8a33afc4d Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 7 Sep 2023 03:49:39 -0400 Subject: [PATCH 235/474] Skip index if image dir not exist --- StabilityMatrix.Core/Services/ImageIndexService.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/StabilityMatrix.Core/Services/ImageIndexService.cs b/StabilityMatrix.Core/Services/ImageIndexService.cs index a7bffe733..b5d795e9f 100644 --- a/StabilityMatrix.Core/Services/ImageIndexService.cs +++ b/StabilityMatrix.Core/Services/ImageIndexService.cs @@ -38,6 +38,10 @@ public async Task> GetLocalImagesByPrefix(string p public async Task RefreshIndex(string subPath = "") { var imagesDir = settingsManager.ImagesDirectory; + if (!imagesDir.Exists) + { + return; + } // Start var stopwatch = Stopwatch.StartNew(); From ed8689bc8a2ff0e3cfe198ef5b50c17a62d87d0c Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 7 Sep 2023 20:48:31 -0400 Subject: [PATCH 236/474] Ignore comments in tokenizing for send --- StabilityMatrix.Avalonia/Models/Inference/Prompt.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs b/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs index 0389f4e51..176d4eaae 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs @@ -136,7 +136,13 @@ private int GetSafeEndIndex(int index) // Find start of network token, until then just add to output if (!token.Scopes.Contains("punctuation.definition.network.begin.prompt")) { - // Push both token and text + // Comments - ignore + if (token.Scopes.Any(s => s.Contains("comment.line"))) + { + continue; + } + + // Normal tags - Push to output outputTokens.Push(token); outputText.Push(RawText[token.StartIndex..GetSafeEndIndex(token.EndIndex)]); continue; From ea3ab2cdcb8a605abd63435516bad127b36792c4 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 8 Sep 2023 22:06:33 -0400 Subject: [PATCH 237/474] Use last modified time for image viewer sorting --- .../ViewModels/Inference/ImageFolderCardViewModel.cs | 11 +++-------- StabilityMatrix.Core/Services/ImageIndexService.cs | 4 +++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs index 96f38a140..df53c6233 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs @@ -1,6 +1,4 @@ using System; -using System.Reactive.Linq; -using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; using DynamicData; @@ -27,12 +25,6 @@ public partial class ImageFolderCardViewModel : ViewModelBase private readonly SourceCache localImagesSource = new(imageFile => imageFile.RelativePath); - /// - /// Collection of image files to display - /// - public IObservableCollection LocalImages { get; } = - new ObservableCollectionExtended(); - /// /// Collection of image items to display /// @@ -49,6 +41,8 @@ ISettingsManager settingsManager this.imageIndexService = imageIndexService; this.settingsManager = settingsManager; + var minDatetime = DateTimeOffset.FromUnixTimeMilliseconds(0); + localImagesSource .Connect() .DeferUntilLoaded() @@ -62,6 +56,7 @@ ISettingsManager settingsManager : imageFile.GetFullPath(settingsManager.ImagesDirectory) } ) + .SortBy(x => x.LocalImageFile?.LastModifiedAt ?? minDatetime, SortDirection.Descending) .Bind(Items) .Subscribe(); } diff --git a/StabilityMatrix.Core/Services/ImageIndexService.cs b/StabilityMatrix.Core/Services/ImageIndexService.cs index b5d795e9f..c935c1d85 100644 --- a/StabilityMatrix.Core/Services/ImageIndexService.cs +++ b/StabilityMatrix.Core/Services/ImageIndexService.cs @@ -80,7 +80,9 @@ var file in imagesDir.Info var localImage = new LocalImageFile { RelativePath = relativePath, - ImageType = imageType + ImageType = imageType, + CreatedAt = file.Info.CreationTimeUtc, + LastModifiedAt = file.Info.LastWriteTimeUtc }; // Insert into database From 557edcb5cbd76ca2fe531ee00a6ca3bd5891a6dd Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 8 Sep 2023 22:09:30 -0400 Subject: [PATCH 238/474] Image folder control visuals --- .../Controls/ImageFolderCard.axaml | 101 +++++++++++++----- .../Controls/ImageFolderCard.axaml.cs | 1 + .../Controls/PromptCard.axaml | 2 +- .../Controls/PromptCard.axaml.cs | 4 +- .../DesignData/DesignData.cs | 17 ++- .../Styles/ThemeMaterials.axaml | 8 ++ 6 files changed, 103 insertions(+), 30 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml index 0e292e4f0..3467a40e9 100644 --- a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml @@ -2,16 +2,17 @@ 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:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models" + xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" x:DataType="vmInference:ImageFolderCardViewModel"> - + @@ -23,6 +24,9 @@ + [RelayCommand] - private async Task OnImageDelete(LocalImageFile item) + private async Task OnImageDelete(LocalImageFile? item) { - if (item.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) + if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) { return; } @@ -159,17 +165,136 @@ private async Task OnImageDelete(LocalImageFile item) await imageIndexService.RemoveImage(item); } + /// + /// Handles clicks to the image delete button + /// + [RelayCommand] + private async Task OnImageCopy(LocalImageFile? item) + { + if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) + { + return; + } + + var clipboard = App.Clipboard; + + var dataObject = new DataObject(); + + // TODO: Not working currently + dataObject.Set(DataFormats.Files, $"file:///{imagePath}"); + + await clipboard.SetDataObjectAsync(dataObject); + } + /// /// Handles clicks to the image open-in-explorer button /// [RelayCommand] - private async Task OnImageOpen(LocalImageFile item) + private async Task OnImageOpen(LocalImageFile? item) { - if (item.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) + if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath) { return; } await ProcessRunner.OpenFileBrowser(imagePath); } + + /// + /// Handles clicks to the image export button + /// + private async Task ImageExportImpl( + LocalImageFile? item, + SKEncodedImageFormat format, + bool includeMetadata = false + ) + { + if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } sourcePath) + { + return; + } + + var sourceFile = new FilePath(sourcePath); + + var storageFile = await App.StorageProvider.SaveFilePickerAsync( + new FilePickerSaveOptions + { + Title = "Export Image", + ShowOverwritePrompt = true, + SuggestedFileName = item.FileName + } + ); + + if (storageFile?.TryGetLocalPath() is not { } targetPath) + { + return; + } + + var targetFile = new FilePath(targetPath); + + try + { + if (format is SKEncodedImageFormat.Png) + { + // For include metadata, just copy the file + if (includeMetadata) + { + await sourceFile.CopyToAsync(targetFile, true); + } + else + { + // Otherwise read and strip the metadata + var imageBytes = await sourceFile.ReadAllBytesAsync(); + + imageBytes = PngDataHelper.RemoveMetadata(imageBytes); + + await targetFile.WriteAllBytesAsync(imageBytes); + } + } + else + { + await Task.Run(() => + { + using var fs = sourceFile.Info.OpenRead(); + var image = SKImage.FromEncodedData(fs); + fs.Dispose(); + + using var targetStream = targetFile.Info.OpenWrite(); + image.Encode(format, 100).SaveTo(targetStream); + }); + } + } + catch (IOException e) + { + logger.LogWarning(e, "Failed to export image"); + notificationService.ShowPersistent( + "Failed to export image", + e.Message, + NotificationType.Error + ); + return; + } + + notificationService.Show( + "Image Exported", + $"Saved to {targetPath}", + NotificationType.Success + ); + } + + [RelayCommand] + private Task OnImageExportPng(LocalImageFile? item) => + ImageExportImpl(item, SKEncodedImageFormat.Png); + + [RelayCommand] + private Task OnImageExportPngWithMetadata(LocalImageFile? item) => + ImageExportImpl(item, SKEncodedImageFormat.Png, true); + + [RelayCommand] + private Task OnImageExportJpeg(LocalImageFile? item) => + ImageExportImpl(item, SKEncodedImageFormat.Jpeg); + + [RelayCommand] + private Task OnImageExportWebp(LocalImageFile? item) => + ImageExportImpl(item, SKEncodedImageFormat.Webp); } From a2cf576088ae7d9f365f84729379690233ec28ed Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 9 Sep 2023 18:04:47 -0400 Subject: [PATCH 254/474] Add suggested types for export dialog --- .../Inference/ImageFolderCardViewModel.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs index d771daa13..c2db1d975 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs @@ -216,12 +216,23 @@ private async Task ImageExportImpl( var sourceFile = new FilePath(sourcePath); + var formatName = format.ToString(); + var storageFile = await App.StorageProvider.SaveFilePickerAsync( new FilePickerSaveOptions { Title = "Export Image", ShowOverwritePrompt = true, - SuggestedFileName = item.FileName + SuggestedFileName = item.FileNameWithoutExtension, + DefaultExtension = formatName.ToLowerInvariant(), + FileTypeChoices = new FilePickerFileType[] + { + new(formatName) + { + Patterns = new[] { $"*.{formatName.ToLowerInvariant()}" }, + MimeTypes = new[] { $"image/{formatName.ToLowerInvariant()}" } + } + } } ); From fb15cb812e1f1efd393d4eac91fa62c2537d05aa Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 9 Sep 2023 18:32:21 -0400 Subject: [PATCH 255/474] Fix crash if completions not loaded when enabled --- .../Behaviors/TextEditorCompletionBehavior.cs | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs index 1d01085a4..f3c5cf19f 100644 --- a/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs +++ b/StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs @@ -74,7 +74,6 @@ protected override void OnAttached() textEditor = editor; textEditor.TextArea.TextEntered += TextArea_TextEntered; - textEditor.TextArea.TextEntering += TextArea_TextEntering; } protected override void OnDetaching() @@ -82,7 +81,6 @@ protected override void OnDetaching() base.OnDetaching(); textEditor.TextArea.TextEntered -= TextArea_TextEntered; - textEditor.TextArea.TextEntering -= TextArea_TextEntering; } private CompletionWindow CreateCompletionWindow(TextArea textArea) @@ -100,7 +98,7 @@ private CompletionWindow CreateCompletionWindow(TextArea textArea) private void TextArea_TextEntered(object? sender, TextInputEventArgs e) { - if (!IsEnabled || e.Text is not { } triggerText) + if (!IsEnabled || CompletionProvider?.IsLoaded != true || e.Text is not { } triggerText) return; if (triggerText.All(IsCompletionChar)) @@ -150,35 +148,6 @@ private void HighlightTextSegment(ISegment segment) textEditor.TextArea.Selection = Selection.Create(textEditor.TextArea, segment); } - private void TextArea_TextEntering(object? sender, TextInputEventArgs e) - { - if (completionWindow is null) - return; - - /*Dispatcher.UIThread.Post(() => - { - // When completion window is open, parse and update token offsets - if (GetCaretToken(textEditor) is not { } tokenSegment) - { - Logger.Trace("Token segment not found"); - return; - } - - completionWindow.StartOffset = tokenSegment.Offset; - completionWindow.EndOffset = tokenSegment.EndOffset; - });*/ - - /*if (e.Text?.Length > 0) { - if (!char.IsLetterOrDigit(e.Text[0])) { - // Whenever a non-letter is typed while the completion window is open, - // insert the currently selected element. - completionWindow?.CompletionList.RequestInsertion(e); - } - }*/ - // Do not set e.Handled=true. - // We still want to insert the character that was typed. - } - private static bool IsCompletionChar(char c) { const string extraAllowedChars = "._-:<"; From f7ed7cf84dc30618b5034e8d44432383f24e8ad1 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 9 Sep 2023 18:32:46 -0400 Subject: [PATCH 256/474] Fix tabs replaced by name not closed in db --- StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs index 0789aa31f..d0276cd74 100644 --- a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs @@ -474,8 +474,7 @@ await JsonSerializer.SerializeAsync( // Update project file currentTab.ProjectFile = new FilePath(result.TryGetLocalPath()!); - // Update the database with the current tab - await SyncTabStateWithDatabase(currentTab); + await SyncTabStatesWithDatabase(); notificationService.Show( "Saved", @@ -576,7 +575,7 @@ private async Task AddTabFromFile(FilePath file) SelectedTab = vm; - await SyncTabStateWithDatabase(vm); + await SyncTabStatesWithDatabase(); } /// From ae8283f6122b675ca0cb9873282f11eaa87f8f65 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 9 Sep 2023 18:41:20 -0400 Subject: [PATCH 257/474] Reorder parameters to be first, add json chunk --- StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs b/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs index 76657e903..9f67ab697 100644 --- a/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs +++ b/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs @@ -42,6 +42,9 @@ InferenceProjectDocument projectDocument + $"Model hash: {generationParameters.ModelHash}, Model: {generationParameters.ModelName}"; var paramsChunk = GetTextChunk("parameters", paramsData); + var paramsJson = JsonSerializer.Serialize(generationParameters); + var paramsJsonChunk = GetTextChunk("parameters-json", paramsJson); + // Go back 4 from the idat index because we need the length of the data idatIndex -= 4; @@ -50,8 +53,9 @@ InferenceProjectDocument projectDocument var actualImageData = inputImage[idatIndex..iendIndex]; var finalImage = existingData - .Concat(smprojChunk) .Concat(paramsChunk) + .Concat(paramsJsonChunk) + .Concat(smprojChunk) .Concat(actualImageData); return finalImage.ToArray(); From e1b18d552714c65e93068989379df26a48954fc4 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 9 Sep 2023 19:40:35 -0400 Subject: [PATCH 258/474] Add parameters image metadata parsing --- .../Helpers/ImageMetadata.cs | 50 ++++++++--- .../Models/Inference/GenerationParameters.cs | 86 +++++++++++++++++-- .../ViewModels/SettingsViewModel.cs | 14 +-- 3 files changed, 127 insertions(+), 23 deletions(-) diff --git a/StabilityMatrix.Avalonia/Helpers/ImageMetadata.cs b/StabilityMatrix.Avalonia/Helpers/ImageMetadata.cs index 79b66563f..568177c1a 100644 --- a/StabilityMatrix.Avalonia/Helpers/ImageMetadata.cs +++ b/StabilityMatrix.Avalonia/Helpers/ImageMetadata.cs @@ -1,12 +1,15 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; using MetadataExtractor; +using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Avalonia.Helpers; -public class ImageMetadata +public partial class ImageMetadata { private IReadOnlyList? Directories { get; set; } @@ -15,30 +18,55 @@ public static ImageMetadata ParseFile(FilePath path) return new ImageMetadata() { Directories = ImageMetadataReader.ReadMetadata(path) }; } - public string? GetComfyMetadata() + public IEnumerable? GetTextualData() { - if (Directories is null) + // Get the PNG-tEXt directory + if (Directories?.FirstOrDefault(d => d.Name == "PNG-tEXt") is not { } pngText) { return null; } - // For Comfy, we want the PNG-tEXt directory - if (Directories.FirstOrDefault(d => d.Name == "PNG-tEXt") is not { } pngText) + // Expect the 'Textual Data' tag + return pngText.Tags.Where(tag => tag.Name == "Textual Data"); + } + + public GenerationParameters? GetGenerationParameters() + { + var textualData = GetTextualData()?.ToArray(); + if (textualData is null) { return null; } - // Expect the 'Textual Data' tag + // Use "parameters-json" tag if exists if ( - pngText.Tags.FirstOrDefault(tag => tag.Name == "Textual Data") is not { } textTag - || textTag.Description is null + textualData.FirstOrDefault( + tag => tag.Description is { } desc && desc.StartsWith("parameters-json: ") + ) is + { Description: { } description } ) { - return null; + description = description.StripStart("parameters-json: "); + + return JsonSerializer.Deserialize(description); } - // Strip `prompt: ` and the rest of the description is json + // Otherwise parse "parameters" tag + if ( + textualData.FirstOrDefault( + tag => tag.Description is { } desc && desc.StartsWith("parameters: ") + ) is + { Description: { } parameters } + ) + { + parameters = parameters.StripStart("parameters: "); + + if (GenerationParameters.TryParse(parameters, out var generationParameters)) + { + return generationParameters; + } + } - return textTag.Description.StripStart("prompt:").TrimStart(); + return null; } } diff --git a/StabilityMatrix.Avalonia/Models/Inference/GenerationParameters.cs b/StabilityMatrix.Avalonia/Models/Inference/GenerationParameters.cs index 7658a74a8..846af61db 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/GenerationParameters.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/GenerationParameters.cs @@ -1,15 +1,87 @@ -namespace StabilityMatrix.Avalonia.Models.Inference; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; -public class GenerationParameters +namespace StabilityMatrix.Avalonia.Models.Inference; + +[JsonSerializable(typeof(GenerationParameters))] +public partial class GenerationParameters { - public string PositivePrompt { get; set; } - public string NegativePrompt { get; set; } + public string? PositivePrompt { get; set; } + public string? NegativePrompt { get; set; } public int Steps { get; set; } - public string Sampler { get; set; } + public string? Sampler { get; set; } public double CfgScale { get; set; } public ulong Seed { get; set; } public int Height { get; set; } public int Width { get; set; } - public string ModelHash { get; set; } - public string ModelName { get; set; } + public string? ModelHash { get; set; } + public string? ModelName { get; set; } + + public static bool TryParse( + string text, + [NotNullWhen(true)] out GenerationParameters? generationParameters + ) + { + var lines = text.Split('\n'); + + if (lines.LastOrDefault() is not { } lastLine) + { + generationParameters = null; + return false; + } + + if (lastLine.StartsWith("Steps:") != true) + { + lines = text.Split("\r\n"); + lastLine = lines.LastOrDefault() ?? string.Empty; + + if (lastLine.StartsWith("Steps:") != true) + { + generationParameters = null; + return false; + } + } + + // Join lines before last line, split at 'Negative prompt: ' + var joinedLines = string.Join("\n", lines[..^1]); + + var splitFirstPart = joinedLines.Split("Negative prompt: "); + if (splitFirstPart.Length != 2) + { + generationParameters = null; + return false; + } + + var positivePrompt = splitFirstPart[0]; + var negativePrompt = splitFirstPart[1]; + + // Parse last line + var match = ParseLastLineRegex().Match(lastLine); + if (!match.Success) + { + generationParameters = null; + return false; + } + + generationParameters = new GenerationParameters + { + PositivePrompt = positivePrompt, + NegativePrompt = negativePrompt, + Steps = int.Parse(match.Groups["Steps"].Value), + Sampler = match.Groups["Sampler"].Value, + CfgScale = double.Parse(match.Groups["CfgScale"].Value), + Seed = ulong.Parse(match.Groups["Seed"].Value), + ModelHash = match.Groups["ModelHash"].Value, + ModelName = match.Groups["ModelName"].Value, + }; + + return true; + } + + [GeneratedRegex( + """^Steps: (?\d+), Sampler: (?.+?), CFG scale: (?\d+(\.\d+)?), Seed: (?\d+), Size: \d+x\d+, Model hash: (?.+?), Model: (?.+)$""" + )] + private static partial Regex ParseLastLineRegex(); } diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index c498b64a2..ee5ae01f3 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -738,16 +738,20 @@ private async Task DebugImageMetadata() return; var metadata = ImageMetadata.ParseFile(files[0].TryGetLocalPath()!); - var comfyJson = metadata.GetComfyMetadata(); + var textualTags = metadata.GetTextualData()?.ToArray(); - if (comfyJson is null) + if (textualTags is null) { - notificationService.Show("No Comfy metadata found", ""); + notificationService.Show("No textual data found", ""); return; } - var dialog = DialogHelper.CreateJsonDialog(comfyJson); - await dialog.ShowAsync(); + if (metadata.GetGenerationParameters() is { } parameters) + { + var parametersJson = JsonSerializer.Serialize(parameters); + var dialog = DialogHelper.CreateJsonDialog(parametersJson, "Generation Parameters"); + await dialog.ShowAsync(); + } } [RelayCommand] From 7a7810a07a8d422a037a079478bc3fe27bfc7a03 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 9 Sep 2023 21:57:59 -0400 Subject: [PATCH 259/474] Update margins on progress ring --- StabilityMatrix.Avalonia/Views/InferencePage.axaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml b/StabilityMatrix.Avalonia/Views/InferencePage.axaml index 068ea2c29..2bcea636b 100644 --- a/StabilityMatrix.Avalonia/Views/InferencePage.axaml +++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml @@ -87,7 +87,10 @@ Spacing="2"> - + From 37751257595d25b86e52fdbfe3da752dae7d66be Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 9 Sep 2023 22:33:50 -0400 Subject: [PATCH 260/474] Use new embedding syntax for help dialog --- .../ViewModels/Inference/PromptCardViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs index d7c1bc299..419c1610b 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs @@ -148,8 +148,8 @@ They may be used in either the positive or negative prompts. Essentially they are text presets, so the position where you place them could make a difference. ```python - embedding:model - (embedding:model:weight) + + ``` ## {Resources.Label_NetworksLoraOrLycoris} From 5925bbadc0280c42f80ff0fcb41142e762cebc3a Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 9 Sep 2023 22:34:13 -0400 Subject: [PATCH 261/474] Update Avalonia Nugets --- .../StabilityMatrix.Avalonia.csproj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index ad402e900..9e19c515e 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -15,17 +15,17 @@ - + - - + + - + - + From 88325cdbf0c95538c76366c85a0641de770551cf Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 10 Sep 2023 05:24:48 -0400 Subject: [PATCH 262/474] Add generation parameters to image index db entry --- .../Helpers/PngDataHelper.cs | 1 + .../InferenceTextToImageViewModel.cs | 1 + .../Helper}/ImageMetadata.cs | 25 ++++++++----------- .../Models/Database/LocalImageFile.cs | 5 ++++ .../Models}/GenerationParameters.cs | 3 +-- .../Services/ImageIndexService.cs | 7 +++++- 6 files changed, 24 insertions(+), 18 deletions(-) rename {StabilityMatrix.Avalonia/Helpers => StabilityMatrix.Core/Helper}/ImageMetadata.cs (72%) rename {StabilityMatrix.Avalonia/Models/Inference => StabilityMatrix.Core/Models}/GenerationParameters.cs (97%) diff --git a/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs b/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs index 9f67ab697..8d253518b 100644 --- a/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs +++ b/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs @@ -5,6 +5,7 @@ using Force.Crc32; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; +using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.Helpers; diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 32982b670..86b5411ee 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -24,6 +24,7 @@ using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Inference; +using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; diff --git a/StabilityMatrix.Avalonia/Helpers/ImageMetadata.cs b/StabilityMatrix.Core/Helper/ImageMetadata.cs similarity index 72% rename from StabilityMatrix.Avalonia/Helpers/ImageMetadata.cs rename to StabilityMatrix.Core/Helper/ImageMetadata.cs index 568177c1a..dcda0d4d3 100644 --- a/StabilityMatrix.Avalonia/Helpers/ImageMetadata.cs +++ b/StabilityMatrix.Core/Helper/ImageMetadata.cs @@ -1,33 +1,28 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Text.RegularExpressions; +using System.Text.Json; using MetadataExtractor; -using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Core.Extensions; +using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; +using Directory = MetadataExtractor.Directory; -namespace StabilityMatrix.Avalonia.Helpers; +namespace StabilityMatrix.Core.Helper; -public partial class ImageMetadata +public class ImageMetadata { private IReadOnlyList? Directories { get; set; } public static ImageMetadata ParseFile(FilePath path) { - return new ImageMetadata() { Directories = ImageMetadataReader.ReadMetadata(path) }; + return new ImageMetadata { Directories = ImageMetadataReader.ReadMetadata(path) }; } public IEnumerable? GetTextualData() { // Get the PNG-tEXt directory - if (Directories?.FirstOrDefault(d => d.Name == "PNG-tEXt") is not { } pngText) - { - return null; - } - - // Expect the 'Textual Data' tag - return pngText.Tags.Where(tag => tag.Name == "Textual Data"); + return Directories? + .Where(d => d.Name == "PNG-tEXt") + .SelectMany(d => d.Tags) + .Where(t => t.Name == "Textual Data"); } public GenerationParameters? GetGenerationParameters() diff --git a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs index 8480c3a9e..6f7fb5360 100644 --- a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs +++ b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs @@ -28,6 +28,11 @@ public class LocalImageFile /// public DateTimeOffset LastModifiedAt { get; set; } + /// + /// Generation parameters metadata of the file. + /// + public GenerationParameters? GenerationParameters { get; set; } + /// /// File name of the relative path. /// diff --git a/StabilityMatrix.Avalonia/Models/Inference/GenerationParameters.cs b/StabilityMatrix.Core/Models/GenerationParameters.cs similarity index 97% rename from StabilityMatrix.Avalonia/Models/Inference/GenerationParameters.cs rename to StabilityMatrix.Core/Models/GenerationParameters.cs index 846af61db..086aa2722 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/GenerationParameters.cs +++ b/StabilityMatrix.Core/Models/GenerationParameters.cs @@ -1,9 +1,8 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text.Json.Serialization; using System.Text.RegularExpressions; -namespace StabilityMatrix.Avalonia.Models.Inference; +namespace StabilityMatrix.Core.Models; [JsonSerializable(typeof(GenerationParameters))] public partial class GenerationParameters diff --git a/StabilityMatrix.Core/Services/ImageIndexService.cs b/StabilityMatrix.Core/Services/ImageIndexService.cs index 655488e5c..adae0bebf 100644 --- a/StabilityMatrix.Core/Services/ImageIndexService.cs +++ b/StabilityMatrix.Core/Services/ImageIndexService.cs @@ -2,6 +2,7 @@ using AsyncAwaitBestPractices; using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Database; +using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; @@ -77,12 +78,16 @@ var file in imagesDir.Info const LocalImageFileType imageType = LocalImageFileType.Inference | LocalImageFileType.TextToImage; + // Get metadata + var metadata = ImageMetadata.ParseFile(file); + var localImage = new LocalImageFile { RelativePath = relativePath, ImageType = imageType, CreatedAt = file.Info.CreationTimeUtc, - LastModifiedAt = file.Info.LastWriteTimeUtc + LastModifiedAt = file.Info.LastWriteTimeUtc, + GenerationParameters = metadata.GetGenerationParameters() }; // Insert into database From 569bb7c770a0756056efa0558ea7b1c9c59a6668 Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 10 Sep 2023 03:15:17 -0700 Subject: [PATCH 263/474] Add ReadTextChunk method for loading image metadata quickly --- .../Controls/ImageFolderCard.axaml | 6 +-- .../Helpers/PngDataHelper.cs | 10 ++--- StabilityMatrix.Core/Helper/ImageMetadata.cs | 40 +++++++++++++++++-- .../Services/ImageIndexService.cs | 21 +++++++++- 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml index 809955d27..d3f7a9f6d 100644 --- a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml @@ -162,11 +162,11 @@ - + diff --git a/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs b/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs index 8d253518b..095ea2ce8 100644 --- a/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs +++ b/StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs @@ -1,10 +1,10 @@ using System; +using System.IO; using System.Linq; using System.Text; using System.Text.Json; using Force.Crc32; using StabilityMatrix.Avalonia.Models; -using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Core.Models; namespace StabilityMatrix.Avalonia.Helpers; @@ -33,7 +33,7 @@ InferenceProjectDocument projectDocument var existingData = inputImage[..textEndIndex]; var smprojJson = JsonSerializer.Serialize(projectDocument); - var smprojChunk = GetTextChunk("smproj", smprojJson); + var smprojChunk = BuildTextChunk("smproj", smprojJson); var paramsData = $"{generationParameters.PositivePrompt}\nNegative prompt: {generationParameters.NegativePrompt}\n" @@ -41,10 +41,10 @@ InferenceProjectDocument projectDocument + $"CFG scale: {generationParameters.CfgScale}, Seed: {generationParameters.Seed}, " + $"Size: {imageWidth}x{imageHeight}, " + $"Model hash: {generationParameters.ModelHash}, Model: {generationParameters.ModelName}"; - var paramsChunk = GetTextChunk("parameters", paramsData); + var paramsChunk = BuildTextChunk("parameters", paramsData); var paramsJson = JsonSerializer.Serialize(generationParameters); - var paramsJsonChunk = GetTextChunk("parameters-json", paramsJson); + var paramsJsonChunk = BuildTextChunk("parameters-json", paramsJson); // Go back 4 from the idat index because we need the length of the data idatIndex -= 4; @@ -84,7 +84,7 @@ public static byte[] RemoveMetadata(byte[] inputImage) return finalImage.ToArray(); } - private static byte[] GetTextChunk(string key, string value) + private static byte[] BuildTextChunk(string key, string value) { var textData = $"{key}\0{value}"; var textDataLength = BitConverter.GetBytes(textData.Length).Reverse(); diff --git a/StabilityMatrix.Core/Helper/ImageMetadata.cs b/StabilityMatrix.Core/Helper/ImageMetadata.cs index dcda0d4d3..a785e191f 100644 --- a/StabilityMatrix.Core/Helper/ImageMetadata.cs +++ b/StabilityMatrix.Core/Helper/ImageMetadata.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Text; +using System.Text.Json; using MetadataExtractor; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models; @@ -11,6 +12,9 @@ public class ImageMetadata { private IReadOnlyList? Directories { get; set; } + private static readonly byte[] Idat = { 0x49, 0x44, 0x41, 0x54 }; + private static readonly byte[] Text = { 0x74, 0x45, 0x58, 0x74 }; + public static ImageMetadata ParseFile(FilePath path) { return new ImageMetadata { Directories = ImageMetadataReader.ReadMetadata(path) }; @@ -19,8 +23,8 @@ public static ImageMetadata ParseFile(FilePath path) public IEnumerable? GetTextualData() { // Get the PNG-tEXt directory - return Directories? - .Where(d => d.Name == "PNG-tEXt") + return Directories + ?.Where(d => d.Name == "PNG-tEXt") .SelectMany(d => d.Tags) .Where(t => t.Name == "Textual Data"); } @@ -64,4 +68,34 @@ public static ImageMetadata ParseFile(FilePath path) return null; } + + public static string ReadTextChunk(BinaryReader byteStream, string key) + { + // skip to end of png header stuff + byteStream.BaseStream.Position = 0x21; + while (byteStream.BaseStream.Position < byteStream.BaseStream.Length) + { + var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).Reverse().ToArray()); + var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4)); + if (chunkType == Encoding.UTF8.GetString(Idat)) + { + return string.Empty; + } + + if (chunkType == Encoding.UTF8.GetString(Text)) + { + var textBytes = byteStream.ReadBytes(chunkSize); + var text = Encoding.UTF8.GetString(textBytes); + if (text.StartsWith($"{key}\0")) + { + return text[(key.Length + 1)..]; + } + } + + // skip crc + byteStream.BaseStream.Position += 4; + } + + return string.Empty; + } } diff --git a/StabilityMatrix.Core/Services/ImageIndexService.cs b/StabilityMatrix.Core/Services/ImageIndexService.cs index adae0bebf..de36dab57 100644 --- a/StabilityMatrix.Core/Services/ImageIndexService.cs +++ b/StabilityMatrix.Core/Services/ImageIndexService.cs @@ -1,8 +1,10 @@ using System.Diagnostics; +using System.Text.Json; using AsyncAwaitBestPractices; using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; @@ -79,7 +81,22 @@ var file in imagesDir.Info LocalImageFileType.Inference | LocalImageFileType.TextToImage; // Get metadata - var metadata = ImageMetadata.ParseFile(file); + 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 { @@ -87,7 +104,7 @@ var file in imagesDir.Info ImageType = imageType, CreatedAt = file.Info.CreationTimeUtc, LastModifiedAt = file.Info.LastWriteTimeUtc, - GenerationParameters = metadata.GetGenerationParameters() + GenerationParameters = genParams }; // Insert into database From e9fda5ba82e498222f04e42157523f95b05a4aa9 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 10 Sep 2023 13:40:00 -0400 Subject: [PATCH 264/474] Add null check for images --- .../ViewModels/Inference/InferenceTextToImageViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 86b5411ee..39bd140a4 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -411,7 +411,7 @@ private async Task GenerateImageImpl( ImageGalleryCardViewModel.ImageSources.Clear(); - if (!imageOutputs.TryGetValue(outputNodeNames[0], out var images)) + if (!imageOutputs.TryGetValue(outputNodeNames[0], out var images) || images is null) { // No images match notificationService.Show("No output", "Did not receive any output images"); From 096fab0d427ef62fda9f014695e883893cbcef51 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 10 Sep 2023 14:39:36 -0400 Subject: [PATCH 265/474] Fix progress ring margins --- StabilityMatrix.Avalonia/Views/InferencePage.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml b/StabilityMatrix.Avalonia/Views/InferencePage.axaml index 2bcea636b..90480d1de 100644 --- a/StabilityMatrix.Avalonia/Views/InferencePage.axaml +++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml @@ -88,7 +88,7 @@ From 1cc26a37721396bc5770dd7f8b584ba68e29e3ea Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 10 Sep 2023 20:42:37 -0400 Subject: [PATCH 266/474] Add host, port, preview launch arg options to ComfyUI --- .../Models/Packages/ComfyUI.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index b53cdbdb1..a47c2b4cd 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -60,6 +60,20 @@ IPrerequisiteHelper prerequisiteHelper public override List LaunchOptions => new List { + new() + { + Name = "Host", + Type = LaunchOptionType.String, + DefaultValue = "127.0.0.1", + Options = { "--listen" } + }, + new() + { + Name = "Port", + Type = LaunchOptionType.String, + DefaultValue = "8188", + Options = { "--port" } + }, new() { Name = "VRAM", @@ -76,6 +90,18 @@ IPrerequisiteHelper prerequisiteHelper Options = { "--highvram", "--normalvram", "--lowvram", "--novram" } }, new() + { + Name = "Preview Method", + Type = LaunchOptionType.Bool, + InitialValue = "--preview-method auto", + Options = + { + "--preview-method auto", + "--preview-method latent2rgb", + "--preview-method taesd" + } + }, + new() { Name = "Enable DirectML", Type = LaunchOptionType.Bool, @@ -97,6 +123,12 @@ IPrerequisiteHelper prerequisiteHelper Options = { "--disable-xformers" } }, new() + { + Name = "Disable upcasting of attention", + Type = LaunchOptionType.Bool, + Options = { "--dont-upcast-attention" } + }, + new() { Name = "Auto-Launch", Type = LaunchOptionType.Bool, From bf5c6434f21b5799c37d407d002473524045951b Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 10 Sep 2023 20:44:44 -0400 Subject: [PATCH 267/474] Move Tab vm base to base namespace --- .../{Inference => Base}/InferenceTabViewModelBase.cs | 3 +-- .../ViewModels/Inference/InferenceTextToImageViewModel.cs | 1 - StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs | 1 + 3 files changed, 2 insertions(+), 3 deletions(-) rename StabilityMatrix.Avalonia/ViewModels/{Inference => Base}/InferenceTabViewModelBase.cs (92%) diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTabViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs similarity index 92% rename from StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTabViewModelBase.cs rename to StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs index 68b95c105..758eda8e3 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTabViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs @@ -3,12 +3,11 @@ using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; using StabilityMatrix.Avalonia.Models; -using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Models.FileInterfaces; #pragma warning disable CS0657 // Not a valid attribute location for this declaration -namespace StabilityMatrix.Avalonia.ViewModels.Inference; +namespace StabilityMatrix.Avalonia.ViewModels.Base; public abstract partial class InferenceTabViewModelBase : LoadableViewModelBase, IDisposable, IPersistentViewProvider { diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 39bd140a4..c79a4550d 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -18,7 +18,6 @@ using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; -using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; diff --git a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs index d0276cd74..0cafe78ab 100644 --- a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs @@ -34,6 +34,7 @@ using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services; +using InferenceTabViewModelBase = StabilityMatrix.Avalonia.ViewModels.Base.InferenceTabViewModelBase; using Path = System.IO.Path; using Symbol = FluentIcons.Common.Symbol; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; From 1873e0c07fd05aef8edd1dd9fdfc15de2a0c69f2 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 10 Sep 2023 20:45:05 -0400 Subject: [PATCH 268/474] Fix GetLaunchArgs Host and Port locators --- StabilityMatrix.Core/Models/InstalledPackage.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Core/Models/InstalledPackage.cs b/StabilityMatrix.Core/Models/InstalledPackage.cs index 00090f237..4695bb129 100644 --- a/StabilityMatrix.Core/Models/InstalledPackage.cs +++ b/StabilityMatrix.Core/Models/InstalledPackage.cs @@ -56,7 +56,7 @@ public class InstalledPackage : IJsonOnDeserialized /// public string? GetLaunchArgsHost() { - var hostOption = LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "host"); + var hostOption = LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "--host"); if (hostOption?.OptionValue != null) { return hostOption.OptionValue as string; @@ -69,7 +69,7 @@ public class InstalledPackage : IJsonOnDeserialized /// public string? GetLaunchArgsPort() { - var portOption = LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "port"); + var portOption = LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "--port"); if (portOption?.OptionValue != null) { return portOption.OptionValue as string; From 77a4aa441fdf18d4620014d832baea44a12a7baf Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 10 Sep 2023 20:45:26 -0400 Subject: [PATCH 269/474] Invalidate cached images on deletion --- .../FallbackRamCachedWebImageLoader.cs | 34 ++++++++++++++----- .../Inference/ImageFolderCardViewModel.cs | 26 ++++---------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs b/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs index 5b79748e4..9dbe6ef94 100644 --- a/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs +++ b/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; using AsyncAwaitBestPractices; using AsyncImageLoader.Loaders; using Avalonia.Media.Imaging; +using StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Avalonia; @@ -14,15 +16,19 @@ namespace StabilityMatrix.Avalonia; public class FallbackRamCachedWebImageLoader : RamCachedWebImageLoader { private readonly WeakEventManager loadFailedEventManager = new(); - + public event EventHandler LoadFailed { add => loadFailedEventManager.AddEventHandler(value); remove => loadFailedEventManager.RemoveEventHandler(value); } - - protected void OnLoadFailed(string url, Exception exception) => loadFailedEventManager.RaiseEvent( - this, new ImageLoadFailedEventArgs(url, exception), nameof(LoadFailed)); + + protected void OnLoadFailed(string url, Exception exception) => + loadFailedEventManager.RaiseEvent( + this, + new ImageLoadFailedEventArgs(url, exception), + nameof(LoadFailed) + ); /// /// Attempts to load bitmap @@ -44,17 +50,19 @@ protected void OnLoadFailed(string url, Exception exception) => loadFailedEventM return null; } } - - var internalOrCachedBitmap = + + var internalOrCachedBitmap = await LoadFromInternalAsync(url).ConfigureAwait(false) ?? await LoadFromGlobalCache(url).ConfigureAwait(false); - - if (internalOrCachedBitmap != null) return internalOrCachedBitmap; + + if (internalOrCachedBitmap != null) + return internalOrCachedBitmap; try { var externalBytes = await LoadDataFromExternalAsync(url).ConfigureAwait(false); - if (externalBytes == null) return null; + if (externalBytes == null) + return null; using var memoryStream = new MemoryStream(externalBytes); var bitmap = new Bitmap(memoryStream); @@ -67,4 +75,12 @@ await LoadFromInternalAsync(url).ConfigureAwait(false) } } + public void RemoveFromCache(string url) + { + // ConcurrentDictionary> _memoryCache + var cache = + this.GetPrivateField>>("_memoryCache") + ?? throw new NullReferenceException("Memory cache not found"); + cache.TryRemove(url, out _); + } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs index c2db1d975..3ef00b3b7 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Threading.Tasks; +using AsyncImageLoader; using Avalonia.Controls.Notifications; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -64,25 +65,6 @@ INotificationService notificationService this.settingsManager = settingsManager; this.notificationService = notificationService; - // var minDatetime = DateTimeOffset.FromUnixTimeMilliseconds(0); - - /*localImagesSource - .Connect() - .DeferUntilLoaded() - .Transform( - imageFile => - new ImageFolderCardItemViewModel - { - LocalImageFile = imageFile, - ImagePath = Design.IsDesignMode - ? imageFile.RelativePath - : imageFile.GetFullPath(settingsManager.ImagesDirectory) - } - ) - .SortBy(x => x.LocalImageFile?.LastModifiedAt ?? minDatetime, SortDirection.Descending) - .Bind(Items) - .Subscribe();*/ - localImagesSource .Connect() .DeferUntilLoaded() @@ -163,6 +145,12 @@ private async Task OnImageDelete(LocalImageFile? item) // Remove from index await imageIndexService.RemoveImage(item); + + // Invalidate cache + if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) + { + loader.RemoveFromCache(imagePath); + } } /// From 2eb9ca99e9153434d7b229556a8e2f8d62746822 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 10 Sep 2023 21:00:54 -0400 Subject: [PATCH 270/474] Fix cached image appearing after deletion --- .../FallbackRamCachedWebImageLoader.cs | 21 +++++- .../Inference/ImageFolderCardViewModel.cs | 5 +- .../Extensions/ObjectExtensions.cs | 68 ++++++++++++------- 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs b/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs index 9dbe6ef94..f8ae8235f 100644 --- a/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs +++ b/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs @@ -75,12 +75,27 @@ await LoadFromInternalAsync(url).ConfigureAwait(false) } } - public void RemoveFromCache(string url) + public void RemovePathFromCache(string filePath) { - // ConcurrentDictionary> _memoryCache var cache = this.GetPrivateField>>("_memoryCache") ?? throw new NullReferenceException("Memory cache not found"); - cache.TryRemove(url, out _); + + cache.TryRemove(filePath, out _); + } + + public void RemoveAllNamesFromCache(string fileName) + { + var cache = + this.GetPrivateField>>("_memoryCache") + ?? throw new NullReferenceException("Memory cache not found"); + + foreach (var (key, _) in cache) + { + if (Path.GetFileName(key) == fileName) + { + cache.TryRemove(key, out _); + } + } } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs index 3ef00b3b7..cdbd9642e 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs @@ -133,7 +133,8 @@ private async Task OnImageDelete(LocalImageFile? item) } // Delete the file - var result = await notificationService.TryAsync(new FilePath(imagePath).DeleteAsync()); + var imageFile = new FilePath(imagePath); + var result = await notificationService.TryAsync(imageFile.DeleteAsync()); if (!result.IsSuccessful) { @@ -149,7 +150,7 @@ private async Task OnImageDelete(LocalImageFile? item) // Invalidate cache if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) { - loader.RemoveFromCache(imagePath); + loader.RemoveAllNamesFromCache(imageFile.Name); } } diff --git a/StabilityMatrix.Core/Extensions/ObjectExtensions.cs b/StabilityMatrix.Core/Extensions/ObjectExtensions.cs index af9668b27..9cbd9923f 100644 --- a/StabilityMatrix.Core/Extensions/ObjectExtensions.cs +++ b/StabilityMatrix.Core/Extensions/ObjectExtensions.cs @@ -3,19 +3,24 @@ namespace StabilityMatrix.Core.Extensions; - public static class ObjectExtensions { /// /// Cache of Types to named field getters /// - private static readonly Dictionary>> FieldGetterTypeCache = new(); - + private static readonly Dictionary< + Type, + Dictionary> + > FieldGetterTypeCache = new(); + /// /// Cache of Types to named field setters /// - private static readonly Dictionary>> FieldSetterTypeCache = new(); - + private static readonly Dictionary< + Type, + Dictionary> + > FieldSetterTypeCache = new(); + /// /// Get the value of a named private field from an object /// @@ -27,22 +32,29 @@ public static class ObjectExtensions if (!fieldGetterCache.TryGetValue(fieldName, out var fieldGetter)) { // Get the field - var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + var field = obj.GetType() + .GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + // Try get from parent + field ??= obj.GetType() + .BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + if (field is null) { - throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}"); + throw new ArgumentException( + $"Field {fieldName} not found on type {obj.GetType().Name}" + ); } - + // Create a getter for the field fieldGetter = field.CreateGetter(); - + // Add to cache fieldGetterCache.Add(fieldName, fieldGetter); } - return (T?) fieldGetter(obj); + return (T?)fieldGetter(obj); } - + /// /// Set the value of a named private field on an object /// @@ -54,25 +66,29 @@ public static void SetPrivateField(this object obj, string fieldName, object val if (!fieldSetterCache.TryGetValue(fieldName, out var fieldSetter)) { // Get the field - var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + var field = obj.GetType() + .GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); // Try get from parent - field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); - + field ??= obj.GetType() + .BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + if (field is null) { - throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}"); + throw new ArgumentException( + $"Field {fieldName} not found on type {obj.GetType().Name}" + ); } - + // Create a setter for the field fieldSetter = field.CreateSetter(); - + // Add to cache fieldSetterCache.Add(fieldName, fieldSetter); } fieldSetter(obj, value); } - + /// /// Set the value of a named private field on an object /// @@ -84,18 +100,22 @@ public static void SetPrivateField(this object obj, string fieldName, T? valu if (!fieldSetterCache.TryGetValue(fieldName, out var fieldSetter)) { // Get the field - var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + var field = obj.GetType() + .GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); // Try get from parent - field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); - + field ??= obj.GetType() + .BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + if (field is null) { - throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}"); + throw new ArgumentException( + $"Field {fieldName} not found on type {obj.GetType().Name}" + ); } - + // Create a setter for the field fieldSetter = field.CreateSetter(); - + // Add to cache fieldSetterCache.Add(fieldName, fieldSetter); } From 39d89b94b87b92de6b5208e820f0604ef22cf865 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 10 Sep 2023 21:37:00 -0400 Subject: [PATCH 271/474] Add model name suggestions on validation errors --- StabilityMatrix.Avalonia/DialogHelper.cs | 48 ++++++++++++++++++- .../Models/Inference/Prompt.cs | 1 + .../Inference/PromptCardViewModel.cs | 8 ++-- .../Exceptions/PromptUnknownModelError.cs | 24 ++++++++++ .../Exceptions/PromptValidationError.cs | 18 +++++-- 5 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 StabilityMatrix.Core/Exceptions/PromptUnknownModelError.cs diff --git a/StabilityMatrix.Avalonia/DialogHelper.cs b/StabilityMatrix.Avalonia/DialogHelper.cs index bb05756e3..e49d58773 100644 --- a/StabilityMatrix.Avalonia/DialogHelper.cs +++ b/StabilityMatrix.Avalonia/DialogHelper.cs @@ -23,7 +23,11 @@ using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Database; +using StabilityMatrix.Core.Services; using TextMateSharp.Grammars; +using Process = FuzzySharp.Process; namespace StabilityMatrix.Avalonia; @@ -298,9 +302,16 @@ public static BetterContentDialog CreateJsonDialog( return dialog; } + /// + /// Create a dialog for displaying a prompt error + /// + /// Target exception to display + /// Full text of the target Document + /// Optional model index service to look for similar names public static BetterContentDialog CreatePromptErrorDialog( PromptError exception, - string sourceText + string sourceText, + IModelIndexService? modelIndexService = null ) { Dispatcher.UIThread.VerifyAccess(); @@ -383,6 +394,41 @@ string sourceText } }; + // Check model typos + if (modelIndexService is not null && exception is PromptUnknownModelError unknownModelError) + { + var sharedFolderType = unknownModelError.ModelType.ConvertTo(); + if (modelIndexService.ModelIndex.TryGetValue(sharedFolderType, out var models)) + { + var result = Process.ExtractOne( + unknownModelError.ModelName, + models.Select(m => m.FileNameWithoutExtension) + ); + + if (result.Score > 40) + { + /*mainGrid.Children.Add( + new TextBlock + { + Text = $"Did you mean: {result.Value}?", + FontSize = 18, + FontWeight = FontWeight.Medium, + Margin = new Thickness(0, 8), + } + );*/ + + mainGrid.Children.Add( + new InfoBar + { + Title = $"Did you mean: {result.Value}?", + IsClosable = false, + IsOpen = true + } + ); + } + } + } + textEditor.ScrollToHorizontalOffset(errorLineEndOffset - errorLineOffset); var dialog = new BetterContentDialog diff --git a/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs b/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs index 4ad88b07f..9477b8960 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs @@ -239,6 +239,7 @@ private int GetSafeEndIndex(int index) { throw PromptValidationError.Network_UnknownModel( modelName, + parsedNetworkType, currentToken.StartIndex, GetSafeEndIndex(currentToken.EndIndex) ); diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs index 419c1610b..fbcd239ee 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs @@ -108,7 +108,7 @@ public async Task ValidatePrompts() } catch (PromptError e) { - var dialog = DialogHelper.CreatePromptErrorDialog(e, promptText); + var dialog = DialogHelper.CreatePromptErrorDialog(e, promptText, modelIndexService); await dialog.ShowAsync(); return false; } @@ -120,7 +120,7 @@ public async Task ValidatePrompts() } catch (PromptError e) { - var dialog = DialogHelper.CreatePromptErrorDialog(e, negPromptText); + var dialog = DialogHelper.CreatePromptErrorDialog(e, negPromptText, modelIndexService); await dialog.ShowAsync(); return false; } @@ -190,7 +190,9 @@ private async Task DebugShowTokens() } catch (PromptError e) { - await DialogHelper.CreatePromptErrorDialog(e, prompt.RawText).ShowAsync(); + await DialogHelper + .CreatePromptErrorDialog(e, prompt.RawText, modelIndexService) + .ShowAsync(); return; } diff --git a/StabilityMatrix.Core/Exceptions/PromptUnknownModelError.cs b/StabilityMatrix.Core/Exceptions/PromptUnknownModelError.cs new file mode 100644 index 000000000..0d3cbe800 --- /dev/null +++ b/StabilityMatrix.Core/Exceptions/PromptUnknownModelError.cs @@ -0,0 +1,24 @@ +using StabilityMatrix.Core.Models.Tokens; + +namespace StabilityMatrix.Core.Exceptions; + +public class PromptUnknownModelError : PromptValidationError +{ + public string ModelName { get; } + + public PromptExtraNetworkType ModelType { get; } + + /// + public PromptUnknownModelError( + string message, + int textOffset, + int textEndOffset, + string modelName, + PromptExtraNetworkType modelType + ) + : base(message, textOffset, textEndOffset) + { + ModelName = modelName; + ModelType = modelType; + } +} diff --git a/StabilityMatrix.Core/Exceptions/PromptValidationError.cs b/StabilityMatrix.Core/Exceptions/PromptValidationError.cs index b68bd022f..d69615b61 100644 --- a/StabilityMatrix.Core/Exceptions/PromptValidationError.cs +++ b/StabilityMatrix.Core/Exceptions/PromptValidationError.cs @@ -1,4 +1,6 @@ -namespace StabilityMatrix.Core.Exceptions; +using StabilityMatrix.Core.Models.Tokens; + +namespace StabilityMatrix.Core.Exceptions; public class PromptValidationError : PromptError { @@ -9,11 +11,19 @@ public PromptValidationError(string message, int textOffset, int textEndOffset) public static PromptValidationError Network_UnknownType(int textOffset, int textEndOffset) => new("Unknown network type", textOffset, textEndOffset); - public static PromptValidationError Network_UnknownModel( - string model, + public static PromptUnknownModelError Network_UnknownModel( + string modelName, + PromptExtraNetworkType modelType, int textOffset, int textEndOffset - ) => new($"Model '{model}' was not found locally", textOffset, textEndOffset); + ) => + new( + $"Model '{modelName}' was not found locally", + textOffset, + textEndOffset, + modelName, + modelType + ); public static PromptSyntaxError Network_InvalidWeight(int textOffset, int textEndOffset) => new("Invalid network weight, could not be parsed as double", textOffset, textEndOffset); From c93b984483a55ce0200397903ba97cc4b2685981 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 11 Sep 2023 02:40:48 -0400 Subject: [PATCH 272/474] Remove commented code --- StabilityMatrix.Avalonia/DialogHelper.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/StabilityMatrix.Avalonia/DialogHelper.cs b/StabilityMatrix.Avalonia/DialogHelper.cs index e49d58773..2d8d04378 100644 --- a/StabilityMatrix.Avalonia/DialogHelper.cs +++ b/StabilityMatrix.Avalonia/DialogHelper.cs @@ -407,16 +407,6 @@ public static BetterContentDialog CreatePromptErrorDialog( if (result.Score > 40) { - /*mainGrid.Children.Add( - new TextBlock - { - Text = $"Did you mean: {result.Value}?", - FontSize = 18, - FontWeight = FontWeight.Medium, - Margin = new Thickness(0, 8), - } - );*/ - mainGrid.Children.Add( new InfoBar { From 954493f6620268db9b13716b7c6ae32918b3d359 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 12 Sep 2023 18:35:25 -0400 Subject: [PATCH 273/474] Add debug only code timer --- StabilityMatrix.Core/Helper/CodeTimer.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/StabilityMatrix.Core/Helper/CodeTimer.cs b/StabilityMatrix.Core/Helper/CodeTimer.cs index 348e57c5e..702bc98a6 100644 --- a/StabilityMatrix.Core/Helper/CodeTimer.cs +++ b/StabilityMatrix.Core/Helper/CodeTimer.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Reactive.Disposables; using System.Runtime.CompilerServices; using System.Text; @@ -30,6 +31,24 @@ public CodeTimer(string postFix = "", [CallerMemberName] string callerName = "") RunningTimers.Push(this); } + /// + /// Starts a new timer and returns it if DEBUG is defined, otherwise returns an empty IDisposable + /// + /// + /// + /// + public static IDisposable StartDebug( + string postFix = "", + [CallerMemberName] string callerName = "" + ) + { +#if DEBUG + return new CodeTimer(postFix, callerName); +#else + return Disposable.Empty; +#endif + } + /// /// Formats a TimeSpan into a string. Chooses the most appropriate unit of time. /// From f2afcada66e0589c138983ec73d13d6d3e89a867 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 12 Sep 2023 18:35:59 -0400 Subject: [PATCH 274/474] Add syntax highlight to help dialog --- StabilityMatrix.Avalonia/DialogHelper.cs | 49 ++++++++++++++++++- .../Helpers/TextEditorConfigs.cs | 11 ++++- .../Models/Inference/Prompt.cs | 1 + .../Models/TextEditorPreset.cs | 7 +++ .../Inference/PromptCardViewModel.cs | 6 ++- 5 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Models/TextEditorPreset.cs diff --git a/StabilityMatrix.Avalonia/DialogHelper.cs b/StabilityMatrix.Avalonia/DialogHelper.cs index 9c87c7850..7a70b2a86 100644 --- a/StabilityMatrix.Avalonia/DialogHelper.cs +++ b/StabilityMatrix.Avalonia/DialogHelper.cs @@ -9,6 +9,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Data; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Threading; using AvaloniaEdit; @@ -18,6 +19,7 @@ using FluentAvalonia.UI.Controls; using Markdown.Avalonia; using Markdown.Avalonia.SyntaxHigh.Extensions; +using NLog; using Refit; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Helpers; @@ -29,11 +31,15 @@ using TextMateSharp.Grammars; using Process = FuzzySharp.Process; using StabilityMatrix.Avalonia.Languages; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Core.Helper; namespace StabilityMatrix.Avalonia; public static class DialogHelper { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + /// /// Create a generic textbox entry content dialog. /// @@ -130,12 +136,53 @@ IReadOnlyList textFields /// /// Create a generic dialog for showing a markdown document /// - public static BetterContentDialog CreateMarkdownDialog(string markdown, string? title = null) + public static BetterContentDialog CreateMarkdownDialog( + string markdown, + string? title = null, + TextEditorPreset editorPreset = default + ) { Dispatcher.UIThread.VerifyAccess(); var viewer = new MarkdownScrollViewer { Markdown = markdown }; + // Apply syntax highlighting to code blocks if preset is provided + if (editorPreset != default) + { + using var _ = CodeTimer.StartDebug(); + + var appliedCount = 0; + + if ( + viewer.GetLogicalDescendants().FirstOrDefault()?.GetLogicalDescendants() is + { } stackDescendants + ) + { + foreach (var editor in stackDescendants.OfType()) + { + TextEditorConfigs.Configure(editor, editorPreset); + + editor.FontFamily = "Cascadia Code,Consolas,Menlo,Monospace"; + editor.Margin = new Thickness(0); + editor.Padding = new Thickness(4); + editor.IsEnabled = false; + + if (editor.GetLogicalParent() is Border border) + { + border.BorderThickness = new Thickness(0); + border.CornerRadius = new CornerRadius(4); + } + + appliedCount++; + } + } + + Logger.Log( + appliedCount > 0 ? LogLevel.Trace : LogLevel.Warn, + $"Applied syntax highlighting to {appliedCount} code blocks" + ); + } + return new BetterContentDialog { Title = title, diff --git a/StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs b/StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs index 964396626..211390271 100644 --- a/StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs +++ b/StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs @@ -4,6 +4,7 @@ using AvaloniaEdit; using AvaloniaEdit.TextMate; using StabilityMatrix.Avalonia.Extensions; +using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Styles; using StabilityMatrix.Core.Extensions; using TextMateSharp.Grammars; @@ -13,8 +14,16 @@ namespace StabilityMatrix.Avalonia.Helpers; -public class TextEditorConfigs +public static class TextEditorConfigs { + public static void Configure(TextEditor editor, TextEditorPreset preset) + { + if (preset == TextEditorPreset.Prompt) + { + ConfigForPrompt(editor); + } + } + public static void ConfigForPrompt(TextEditor editor) { const ThemeName themeName = ThemeName.DimmedMonokai; diff --git a/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs b/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs index 9477b8960..a7a7ba738 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/Prompt.cs @@ -309,6 +309,7 @@ private int GetSafeEndIndex(int index) GetSafeEndIndex(currentToken.EndIndex) ); } + currentToken = tokens.Current; if (!currentToken.Scopes.Contains("punctuation.definition.network.end.prompt")) diff --git a/StabilityMatrix.Avalonia/Models/TextEditorPreset.cs b/StabilityMatrix.Avalonia/Models/TextEditorPreset.cs new file mode 100644 index 000000000..3caf0b86c --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/TextEditorPreset.cs @@ -0,0 +1,7 @@ +namespace StabilityMatrix.Avalonia.Models; + +public enum TextEditorPreset +{ + None, + Prompt +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs index fbcd239ee..6c7b362ae 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs @@ -172,7 +172,11 @@ a red cat # also comments ``` """; - var dialog = DialogHelper.CreateMarkdownDialog(md, "Prompt Syntax"); + var dialog = DialogHelper.CreateMarkdownDialog( + md, + "Prompt Syntax", + TextEditorPreset.Prompt + ); dialog.MinDialogWidth = 800; dialog.MaxDialogHeight = 1000; dialog.MaxDialogWidth = 1000; From cce94d4d13044945ef29b64c093eb156dfa30d49 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 13 Sep 2023 16:38:14 -0400 Subject: [PATCH 275/474] Fix connect errors for blank port config --- .../Services/InferenceClientManager.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs index 926fc3a34..33490c76c 100644 --- a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs @@ -263,9 +263,18 @@ await comfyPackage.SetupInferenceOutputFolderLinks( ); // Get user defined host and port - var host = packagePair.InstalledPackage.GetLaunchArgsHost() ?? "127.0.0.1"; + var host = packagePair.InstalledPackage.GetLaunchArgsHost(); + if (string.IsNullOrWhiteSpace(host)) + { + host = "127.0.0.1"; + } host = host.Replace("localhost", "127.0.0.1"); - var port = packagePair.InstalledPackage.GetLaunchArgsPort() ?? "8188"; + + var port = packagePair.InstalledPackage.GetLaunchArgsPort(); + if (string.IsNullOrWhiteSpace(port)) + { + port = "8188"; + } var uri = new UriBuilder("http", host, int.Parse(port)).Uri; From 119e7b262f58998ad626e92bb171b772850fc96b Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 13 Sep 2023 16:38:30 -0400 Subject: [PATCH 276/474] Add ItemsRepeaterArrangeAnimation --- .../ItemsRepeaterArrangeAnimation.cs | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 StabilityMatrix.Avalonia/Animations/ItemsRepeaterArrangeAnimation.cs diff --git a/StabilityMatrix.Avalonia/Animations/ItemsRepeaterArrangeAnimation.cs b/StabilityMatrix.Avalonia/Animations/ItemsRepeaterArrangeAnimation.cs new file mode 100644 index 000000000..507d9872e --- /dev/null +++ b/StabilityMatrix.Avalonia/Animations/ItemsRepeaterArrangeAnimation.cs @@ -0,0 +1,108 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Animations; + +namespace StabilityMatrix.Avalonia.Animations; + +public class ItemsRepeaterArrangeAnimation : AvaloniaObject +{ + public static readonly AttachedProperty EnableItemsArrangeAnimationProperty = + AvaloniaProperty.RegisterAttached( + "EnableItemsArrangeAnimation", + typeof(ItemsRepeaterArrangeAnimation) + ); + + static ItemsRepeaterArrangeAnimation() + { + EnableItemsArrangeAnimationProperty.Changed.AddClassHandler( + OnEnableItemsArrangeAnimationChanged + ); + } + + private static void OnEnableItemsArrangeAnimationChanged( + ItemsRepeater itemsRepeater, + AvaloniaPropertyChangedEventArgs eventArgs + ) + { + if (eventArgs.NewValue is true) + { + itemsRepeater.ElementPrepared += OnElementPrepared; + itemsRepeater.ElementIndexChanged += OnElementIndexChanged; + } + else + { + // itemsRepeater.Opened -= OnOpened; + } + } + + private static void CreateAnimation(Visual item) + { + var compositionVisual = + ElementComposition.GetElementVisual(item) ?? throw new NullReferenceException(); + + if (compositionVisual.ImplicitAnimations is { } animations && animations.HasKey("Offset")) + { + return; + } + + var compositor = compositionVisual.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + // Using the "this.FinalValue" to indicate the last value of the Offset property + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + + // Create a new implicit animation collection and bind the offset animation + var implicitAnimationCollection = compositor.CreateImplicitAnimationCollection(); + implicitAnimationCollection["Offset"] = offsetAnimation; + compositionVisual.ImplicitAnimations = implicitAnimationCollection; + } + + private static void OnElementPrepared(object? sender, ItemsRepeaterElementPreparedEventArgs e) + { + if ( + sender is not ItemsRepeater itemsRepeater + || !GetEnableItemsArrangeAnimation(itemsRepeater) + ) + return; + + CreateAnimation(itemsRepeater); + } + + private static void OnElementIndexChanged( + object? sender, + ItemsRepeaterElementIndexChangedEventArgs e + ) + { + if ( + sender is not ItemsRepeater itemsRepeater + || !GetEnableItemsArrangeAnimation(itemsRepeater) + ) + return; + + CreateAnimation(itemsRepeater); + } + + /*private static void OnOpened(object sender, EventArgs e) + { + if (sender is not WindowBase windowBase || !GetEnableScaleShowAnimation(windowBase)) + return; + + // Here we explicitly animate the "Scale" property + // The implementation is the same as `Offset` at the beginning, but just with the Scale property + windowBase.StartWindowScaleAnimation(); + }*/ + + public static bool GetEnableItemsArrangeAnimation(ItemsRepeater element) + { + return element.GetValue(EnableItemsArrangeAnimationProperty); + } + + public static void SetEnableItemsArrangeAnimation(ItemsRepeater element, bool value) + { + element.SetValue(EnableItemsArrangeAnimationProperty, value); + } +} From d12e1c78095b3db634a20dc1f164200a89bf18d5 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 13 Sep 2023 16:38:47 -0400 Subject: [PATCH 277/474] Add faster memory based image indexing --- .../Controls/ImageFolderCard.axaml | 33 ++-- .../DesignData/MockImageIndexService.cs | 20 ++- .../Inference/ImageFolderCardViewModel.cs | 29 +--- .../InferenceTextToImageViewModel.cs | 152 ++++++++++-------- .../Inference/PromptCardViewModel.cs | 18 +-- .../ViewModels/InferenceViewModel.cs | 2 +- .../Models/Database/LocalImageFile.cs | 80 +++++++++ .../Models/GenerationParameters.cs | 8 +- .../Models/IndexCollection.cs | 59 +++++++ .../Services/IImageIndexService.cs | 15 +- .../Services/ImageIndexService.cs | 104 +++++++++++- 11 files changed, 392 insertions(+), 128 deletions(-) create mode 100644 StabilityMatrix.Core/Models/IndexCollection.cs diff --git a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml index d3f7a9f6d..568afb72b 100644 --- a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml @@ -9,6 +9,7 @@ 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" x:DataType="vmInference:ImageFolderCardViewModel"> @@ -37,6 +38,12 @@ VerticalAlignment="{TemplateBinding VerticalAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalAlignment}" VerticalContentAlignment="{TemplateBinding VerticalAlignment}"> + + + + - + @@ -107,6 +120,7 @@ @@ -54,14 +62,57 @@ INotificationService notificationService this.settingsManager = settingsManager; this.notificationService = notificationService; + var predicate = this.WhenPropertyChanged(vm => vm.SearchQuery) + .Throttle(TimeSpan.FromMilliseconds(50))! + .Select, Func>( + p => (LocalImageFile file) => SearchPredicate(file, p.Value) + ) + .AsObservable(); + imageIndexService.InferenceImages.ItemsSource .Connect() .DeferUntilLoaded() + .Filter(predicate) .SortBy(file => file.LastModifiedAt, SortDirection.Descending) .Bind(LocalImages) .Subscribe(); } + private static bool SearchPredicate(LocalImageFile file, string? query) + { + if ( + string.IsNullOrWhiteSpace(query) + || file.FileName.Contains(query, StringComparison.OrdinalIgnoreCase) + ) + { + return true; + } + + // File name + var filenameScore = Fuzz.WeightedRatio(query, file.FileName, PreprocessMode.Full); + if (filenameScore > 80) + { + return true; + } + + // Generation params + if (file.GenerationParameters is { } parameters) + { + if ( + parameters.Seed.ToString().StartsWith(query, StringComparison.OrdinalIgnoreCase) + || parameters.Sampler is { } sampler + && sampler.StartsWith(query, StringComparison.OrdinalIgnoreCase) + || parameters.ModelName is { } modelName + && modelName.StartsWith(query, StringComparison.OrdinalIgnoreCase) + ) + { + return true; + } + } + + return false; + } + /// public override async Task OnLoadedAsync() { From 5593820d687034458aae9f6b040baf04085711d1 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 14 Sep 2023 04:03:31 -0400 Subject: [PATCH 281/474] Update inference states on model index changes --- .../Services/InferenceClientManager.cs | 17 +++++++++++++++++ StabilityMatrix.Core/Helper/EventManager.cs | 4 ++++ .../Services/ModelIndexService.cs | 3 +++ 3 files changed, 24 insertions(+) diff --git a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs index 33490c76c..ac6f6dc50 100644 --- a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs @@ -9,6 +9,7 @@ using DynamicData.Binding; using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Api; +using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Inference; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; @@ -110,6 +111,22 @@ ISettingsManager settingsManager schedulersSource.Connect().DeferUntilLoaded().Bind(Schedulers).Subscribe(); ResetSharedProperties(); + + EventManager.Instance.ModelIndexChanged += (s, e) => + { + logger.LogDebug("Model index changed, reloading shared properties for Inference"); + if (IsConnected) + { + LoadSharedPropertiesAsync() + .SafeFireAndForget( + onException: ex => logger.LogError(ex, "Error loading shared properties") + ); + } + else + { + ResetSharedProperties(); + } + }; } private async Task LoadSharedPropertiesAsync() diff --git a/StabilityMatrix.Core/Helper/EventManager.cs b/StabilityMatrix.Core/Helper/EventManager.cs index 5736a49a7..4a40d1f77 100644 --- a/StabilityMatrix.Core/Helper/EventManager.cs +++ b/StabilityMatrix.Core/Helper/EventManager.cs @@ -30,6 +30,8 @@ private EventManager() { } public event EventHandler? CultureChanged; + public event EventHandler? ModelIndexChanged; + public void OnGlobalProgressChanged(int progress) => GlobalProgressChanged?.Invoke(this, progress); @@ -65,4 +67,6 @@ public void OnPackageInstallProgressAdded(IPackageModificationRunner runner) => public void OnToggleProgressFlyout() => ToggleProgressFlyout?.Invoke(this, EventArgs.Empty); public void OnCultureChanged(CultureInfo culture) => CultureChanged?.Invoke(this, culture); + + public void OnModelIndexChanged() => ModelIndexChanged?.Invoke(this, EventArgs.Empty); } diff --git a/StabilityMatrix.Core/Services/ModelIndexService.cs b/StabilityMatrix.Core/Services/ModelIndexService.cs index f36d74791..190d6d0d0 100644 --- a/StabilityMatrix.Core/Services/ModelIndexService.cs +++ b/StabilityMatrix.Core/Services/ModelIndexService.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Extensions; +using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; @@ -152,6 +153,8 @@ await jsonPath.ReadAllTextAsync().ConfigureAwait(false) indexDuration.TotalMilliseconds, dbDuration.TotalMilliseconds ); + + EventManager.Instance.OnModelIndexChanged(); } /// From de350040c7ab51510d56cf9f22357786e6ccc216 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 14 Sep 2023 04:03:53 -0400 Subject: [PATCH 282/474] Handle drops onto the folder card itself --- .../DropTargetTemplatedControlBase.cs | 32 +++++++++++++++++++ .../Controls/ImageFolderCard.axaml.cs | 19 +++++++++-- .../Inference/ImageFolderCardViewModel.cs | 2 +- 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Controls/DropTargetTemplatedControlBase.cs diff --git a/StabilityMatrix.Avalonia/Controls/DropTargetTemplatedControlBase.cs b/StabilityMatrix.Avalonia/Controls/DropTargetTemplatedControlBase.cs new file mode 100644 index 000000000..8bd10e05f --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/DropTargetTemplatedControlBase.cs @@ -0,0 +1,32 @@ +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using StabilityMatrix.Avalonia.ViewModels; + +namespace StabilityMatrix.Avalonia.Controls; + +public abstract class DropTargetTemplatedControlBase : TemplatedControl +{ + protected DropTargetTemplatedControlBase() + { + AddHandler(DragDrop.DropEvent, DropHandler); + AddHandler(DragDrop.DragOverEvent, DragOverHandler); + + DragDrop.SetAllowDrop(this, true); + } + + protected virtual void DragOverHandler(object? sender, DragEventArgs e) + { + if (DataContext is IDropTarget dropTarget) + { + dropTarget.DragOver(sender, e); + } + } + + protected virtual void DropHandler(object? sender, DragEventArgs e) + { + if (DataContext is IDropTarget dropTarget) + { + dropTarget.Drop(sender, e); + } + } +} diff --git a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs index e46bb93cf..41c0709a1 100644 --- a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs +++ b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs @@ -1,13 +1,12 @@ using AsyncAwaitBestPractices; -using Avalonia; -using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Threading; using StabilityMatrix.Avalonia.ViewModels.Base; namespace StabilityMatrix.Avalonia.Controls; -public class ImageFolderCard : TemplatedControl +public class ImageFolderCard : DropTargetTemplatedControlBase { /// protected override void OnLoaded(RoutedEventArgs e) @@ -25,4 +24,18 @@ protected override void OnLoaded(RoutedEventArgs e) .SafeFireAndForget(); } } + + /// + protected override void DropHandler(object? sender, DragEventArgs e) + { + base.DropHandler(sender, e); + e.Handled = true; + } + + /// + protected override void DragOverHandler(object? sender, DragEventArgs e) + { + base.DragOverHandler(sender, e); + e.Handled = true; + } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs index 88d98994b..199fa04eb 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs @@ -65,7 +65,7 @@ INotificationService notificationService var predicate = this.WhenPropertyChanged(vm => vm.SearchQuery) .Throttle(TimeSpan.FromMilliseconds(50))! .Select, Func>( - p => (LocalImageFile file) => SearchPredicate(file, p.Value) + p => file => SearchPredicate(file, p.Value) ) .AsObservable(); From f837be0d86ac4649e4f07c2353518fd7d9a32f50 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 14 Sep 2023 14:25:42 -0400 Subject: [PATCH 283/474] Remove duplicate package refs --- StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index e1178dd5c..06b2e8bb1 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -58,9 +58,6 @@ - - - From 11cb46abf88c381504cf0979c35f0ac47f186038 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 14 Sep 2023 15:59:57 -0400 Subject: [PATCH 284/474] Abstract common generation functionality to InferenceGenerationViewModelBase --- .../DesignData/MockImageIndexService.cs | 3 - .../Extensions/ComfyNodeBuilderExtensions.cs | 6 +- .../Models/InferenceProjectType.cs | 24 +- .../Services/ServiceManager.cs | 143 ++++--- .../Base/InferenceGenerationViewModelBase.cs | 354 ++++++++++++++++++ .../InferenceTextToImageViewModel.cs | 298 ++------------- .../ViewModels/InferenceViewModel.cs | 19 +- .../Views/InferencePage.axaml | 13 +- .../Views/InferencePage.axaml.cs | 10 +- StabilityMatrix.Core/Helper/EventManager.cs | 5 + .../Api/Comfy/Nodes/ComfyNodeBuilder.cs | 5 + .../Services/IImageIndexService.cs | 2 - .../Services/ImageIndexService.cs | 10 +- 13 files changed, 555 insertions(+), 337 deletions(-) create mode 100644 StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs diff --git a/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs b/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs index 70aa78079..d5ee16500 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs @@ -54,9 +54,6 @@ public Task RefreshIndex(IndexCollection indexCollection return Task.CompletedTask; } - /// - public void OnImageAdded(FilePath filePath) { } - /// public void BackgroundRefreshIndex() { diff --git a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs index df81d4827..e0fc820c0 100644 --- a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs +++ b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs @@ -253,11 +253,13 @@ public static string SetupOutputImage(this ComfyNodeBuilder builder) var vaeDecoder = builder.Nodes.AddNamedNode( ComfyNodeBuilder.VAEDecode( "VAEDecode", - builder.Connections.Latent!, + builder.Connections.Latent + ?? throw new InvalidOperationException("Latent source not set"), builder.Connections.GetRefinerOrBaseVAE() ) ); builder.Connections.Image = vaeDecoder.Output; + builder.Connections.ImageSize = builder.Connections.LatentSize; } var saveImage = builder.Nodes.AddNamedNode( @@ -272,6 +274,8 @@ public static string SetupOutputImage(this ComfyNodeBuilder builder) } ); + builder.Connections.OutputNodes.Add(saveImage); + return saveImage.Name; } } diff --git a/StabilityMatrix.Avalonia/Models/InferenceProjectType.cs b/StabilityMatrix.Avalonia/Models/InferenceProjectType.cs index 75935553b..c11d039ad 100644 --- a/StabilityMatrix.Avalonia/Models/InferenceProjectType.cs +++ b/StabilityMatrix.Avalonia/Models/InferenceProjectType.cs @@ -1,7 +1,29 @@ -namespace StabilityMatrix.Avalonia.Models; +using System; +using StabilityMatrix.Avalonia.ViewModels.Inference; + +namespace StabilityMatrix.Avalonia.Models; public enum InferenceProjectType { Unknown, TextToImage, + ImageToImage, + Inpainting, + Upscale +} + +public static class InferenceProjectTypeExtensions +{ + public static Type? ToViewModelType(this InferenceProjectType type) + { + return type switch + { + InferenceProjectType.TextToImage => typeof(InferenceTextToImageViewModel), + InferenceProjectType.ImageToImage => null, + InferenceProjectType.Inpainting => null, + InferenceProjectType.Upscale => typeof(InferenceImageUpscaleViewModel), + InferenceProjectType.Unknown => null, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } } diff --git a/StabilityMatrix.Avalonia/Services/ServiceManager.cs b/StabilityMatrix.Avalonia/Services/ServiceManager.cs index d519c31f5..26d604f46 100644 --- a/StabilityMatrix.Avalonia/Services/ServiceManager.cs +++ b/StabilityMatrix.Avalonia/Services/ServiceManager.cs @@ -13,94 +13,103 @@ public class ServiceManager { // Holds providers private readonly Dictionary> providers = new(); - + // Holds singleton instances private readonly Dictionary instances = new(); - + /// /// Register a new dialog view model (singleton instance) /// - public ServiceManager Register(TService instance) where TService : T + public ServiceManager Register(TService instance) + where TService : T { - if (instance is null) throw new ArgumentNullException(nameof(instance)); - + if (instance is null) + throw new ArgumentNullException(nameof(instance)); + lock (instances) { if (instances.ContainsKey(typeof(TService)) || providers.ContainsKey(typeof(TService))) { throw new ArgumentException( - $"Service of type {typeof(TService)} is already registered for {typeof(T)}"); + $"Service of type {typeof(TService)} is already registered for {typeof(T)}" + ); } instances[instance.GetType()] = instance; } - + return this; } - + /// /// Register a new dialog view model provider action (called on each dialog creation) /// - public ServiceManager Register(Func provider) where TService : T + public ServiceManager Register(Func provider) + where TService : T { lock (providers) { if (instances.ContainsKey(typeof(TService)) || providers.ContainsKey(typeof(TService))) { throw new ArgumentException( - $"Service of type {typeof(TService)} is already registered for {typeof(T)}"); + $"Service of type {typeof(TService)} is already registered for {typeof(T)}" + ); } // Return type is wrong during build with method group syntax // ReSharper disable once RedundantCast - providers[typeof(TService)] = () => (TService) provider(); + providers[typeof(TService)] = () => (TService)provider(); } return this; } - + /// /// Register a new dialog view model instance using a service provider /// Equal to Register[TService](serviceProvider.GetRequiredService[TService]) /// - public ServiceManager RegisterProvider(IServiceProvider provider) where TService : notnull, T + public ServiceManager RegisterProvider(IServiceProvider provider) + where TService : notnull, T { lock (providers) { if (instances.ContainsKey(typeof(TService)) || providers.ContainsKey(typeof(TService))) { throw new ArgumentException( - $"Service of type {typeof(TService)} is already registered for {typeof(T)}"); + $"Service of type {typeof(TService)} is already registered for {typeof(T)}" + ); } - + // Return type is wrong during build with method group syntax // ReSharper disable once RedundantCast - providers[typeof(TService)] = () => (TService) provider.GetRequiredService(); + providers[typeof(TService)] = () => (TService)provider.GetRequiredService(); } return this; } - + /// /// Get a view model instance from runtime type /// [SuppressMessage("ReSharper", "InconsistentlySynchronizedField")] public T Get(Type serviceType) { - if (!serviceType.IsAssignableFrom(typeof(T))) + if (!serviceType.IsAssignableTo(typeof(T))) { throw new ArgumentException( - $"Service type {serviceType} is not assignable from {typeof(T)}"); + $"Service type {serviceType} is not assignable to {typeof(T)}" + ); } - + if (instances.TryGetValue(serviceType, out var instance)) { if (instance is null) { throw new ArgumentException( - $"Service of type {serviceType} was registered as null"); + $"Service of type {serviceType} was registered as null" + ); } - return (T) instance; + return (T)instance; } if (providers.TryGetValue(serviceType, out var provider)) @@ -108,35 +117,40 @@ public T Get(Type serviceType) if (provider is null) { throw new ArgumentException( - $"Service of type {serviceType} was registered as null"); + $"Service of type {serviceType} was registered as null" + ); } var result = provider(); if (result is null) { throw new ArgumentException( - $"Service provider for type {serviceType} returned null"); + $"Service provider for type {serviceType} returned null" + ); } - return (T) result; + return (T)result; } throw new ArgumentException( - $"Service of type {serviceType} is not registered for {typeof(T)}"); + $"Service of type {serviceType} is not registered for {typeof(T)}" + ); } - + /// /// Get a view model instance /// [SuppressMessage("ReSharper", "InconsistentlySynchronizedField")] - public TService Get() where TService : T + public TService Get() + where TService : T { if (instances.TryGetValue(typeof(TService), out var instance)) { if (instance is null) { throw new ArgumentException( - $"Service of type {typeof(TService)} was registered as null"); + $"Service of type {typeof(TService)} was registered as null" + ); } - return (TService) instance; + return (TService)instance; } if (providers.TryGetValue(typeof(TService), out var provider)) @@ -144,83 +158,102 @@ public TService Get() where TService : T if (provider is null) { throw new ArgumentException( - $"Service of type {typeof(TService)} was registered as null"); + $"Service of type {typeof(TService)} was registered as null" + ); } var result = provider(); if (result is null) { throw new ArgumentException( - $"Service provider for type {typeof(TService)} returned null"); + $"Service provider for type {typeof(TService)} returned null" + ); } - return (TService) result; + return (TService)result; } throw new ArgumentException( - $"Service of type {typeof(TService)} is not registered for {typeof(T)}"); + $"Service of type {typeof(TService)} is not registered for {typeof(T)}" + ); } - + /// /// Get a view model instance with an initializer parameter /// - public TService Get(Func initializer) where TService : T + public TService Get(Func initializer) + where TService : T { var instance = Get(); return initializer(instance); } - + /// /// Get a view model instance with an initializer for a mutable instance /// - public TService Get(Action initializer) where TService : T + public TService Get(Action initializer) + where TService : T { var instance = Get(); initializer(instance); return instance; } - + /// /// Get a view model instance, set as DataContext of its View, and return /// a BetterContentDialog with that View as its Content /// - public BetterContentDialog GetDialog() where TService : T + public BetterContentDialog GetDialog() + where TService : T { var instance = Get()!; - - if (Attribute.GetCustomAttribute(instance.GetType(), typeof(ViewAttribute)) is not ViewAttribute - viewAttr) + + if ( + Attribute.GetCustomAttribute(instance.GetType(), typeof(ViewAttribute)) + is not ViewAttribute viewAttr + ) { - throw new InvalidOperationException($"View not found for {instance.GetType().FullName}"); + throw new InvalidOperationException( + $"View not found for {instance.GetType().FullName}" + ); } if (Activator.CreateInstance(viewAttr.ViewType) is not Control view) { - throw new NullReferenceException($"Unable to create instance for {instance.GetType().FullName}"); + throw new NullReferenceException( + $"Unable to create instance for {instance.GetType().FullName}" + ); } - + return new BetterContentDialog { Content = view }; } - + /// /// Get a view model instance with initializer, set as DataContext of its View, and return /// a BetterContentDialog with that View as its Content /// - public BetterContentDialog GetDialog(Action initializer) where TService : T + public BetterContentDialog GetDialog(Action initializer) + where TService : T { var instance = Get(initializer)!; - - if (Attribute.GetCustomAttribute(instance.GetType(), typeof(ViewAttribute)) is not ViewAttribute - viewAttr) + + if ( + Attribute.GetCustomAttribute(instance.GetType(), typeof(ViewAttribute)) + is not ViewAttribute viewAttr + ) { - throw new InvalidOperationException($"View not found for {instance.GetType().FullName}"); + throw new InvalidOperationException( + $"View not found for {instance.GetType().FullName}" + ); } if (Activator.CreateInstance(viewAttr.ViewType) is not Control view) { - throw new NullReferenceException($"Unable to create instance for {instance.GetType().FullName}"); + throw new NullReferenceException( + $"Unable to create instance for {instance.GetType().FullName}" + ); } - + view.DataContext = instance; - + return new BetterContentDialog { Content = view }; } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs new file mode 100644 index 000000000..b46fe67b7 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -0,0 +1,354 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using AsyncAwaitBestPractices; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.Input; +using NLog; +using Refit; +using SkiaSharp; +using StabilityMatrix.Avalonia.Extensions; +using StabilityMatrix.Avalonia.Helpers; +using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Avalonia.ViewModels.Inference; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Inference; +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Api.Comfy; +using StabilityMatrix.Core.Models.Api.Comfy.Nodes; +using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; + +namespace StabilityMatrix.Avalonia.ViewModels.Base; + +/// +/// Abstract base class for tab view models that generate images using ClientManager. +/// This includes a progress reporter, image output view model, and generation virtual methods. +/// +[SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")] +public abstract partial class InferenceGenerationViewModelBase + : InferenceTabViewModelBase, + IImageGalleryComponent +{ + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private readonly INotificationService notificationService; + + [JsonPropertyName("ImageGallery")] + public ImageGalleryCardViewModel ImageGalleryCardViewModel { get; } + + [JsonIgnore] + public ImageFolderCardViewModel ImageFolderCardViewModel { get; } + + [JsonIgnore] + public ProgressViewModel OutputProgress { get; } = new(); + + [JsonIgnore] + public IInferenceClientManager ClientManager { get; } + + /// + protected InferenceGenerationViewModelBase( + ServiceManager vmFactory, + IInferenceClientManager inferenceClientManager, + INotificationService notificationService + ) + { + this.notificationService = notificationService; + + ClientManager = inferenceClientManager; + + ImageGalleryCardViewModel = vmFactory.Get(); + ImageFolderCardViewModel = vmFactory.Get(); + + GenerateImageCommand.WithConditionalNotificationErrorHandler(notificationService); + } + + /// + /// Builds the image generation prompt + /// + protected virtual void BuildPrompt(BuildPromptEventArgs args) { } + + /// + /// Runs a generation task + /// + /// Thrown if args.Parameters or args.Project are null + protected async Task RunGeneration( + ImageGenerationEventArgs args, + CancellationToken cancellationToken + ) + { + var client = args.Client; + var nodes = args.Nodes; + + // Checks + if (args.Parameters is null) + throw new InvalidOperationException("Parameters is null"); + if (args.Project is null) + throw new InvalidOperationException("Project is null"); + if (args.OutputNodeNames.Count == 0) + throw new InvalidOperationException("OutputNodeNames is empty"); + if (client.OutputImagesDir is null) + throw new InvalidOperationException("OutputImagesDir is null"); + + // Connect preview image handler + client.PreviewImageReceived += OnPreviewImageReceived; + + ComfyTask? promptTask = null; + + try + { + // Register to interrupt if user cancels + cancellationToken.Register(() => + { + Logger.Info("Cancelling prompt"); + client + .InterruptPromptAsync(new CancellationTokenSource(5000).Token) + .SafeFireAndForget(); + }); + + try + { + promptTask = await client.QueuePromptAsync(nodes, cancellationToken); + } + catch (ApiException e) + { + Logger.Warn(e, "Api exception while queuing prompt"); + await DialogHelper.CreateApiExceptionDialog(e, "Api Error").ShowAsync(); + return; + } + + // Register progress handler + promptTask.ProgressUpdate += OnProgressUpdateReceived; + + // Wait for prompt to finish + await promptTask.Task.WaitAsync(cancellationToken); + Logger.Trace($"Prompt task {promptTask.Id} finished"); + + // Get output images + var imageOutputs = await client.GetImagesForExecutedPromptAsync( + promptTask.Id, + cancellationToken + ); + + 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 + { + // Disconnect progress handler + client.PreviewImageReceived -= OnPreviewImageReceived; + + // Clear progress + OutputProgress.Value = 0; + OutputProgress.Text = ""; + ImageGalleryCardViewModel.PreviewImage?.Dispose(); + ImageGalleryCardViewModel.PreviewImage = null; + ImageGalleryCardViewModel.IsPreviewOverlayEnabled = false; + + // Cleanup tasks + promptTask?.Dispose(); + } + } + + /// + /// Handles image output metadata for generation runs + /// + private async Task ProcessOutputImages( + IEnumerable images, + ImageGenerationEventArgs args + ) + { + // Write metadata to images + var outputImages = new List(); + foreach ( + var filePath in images.Select(image => image.ToFilePath(args.Client.OutputImagesDir!)) + ) + { + var bytesWithMetadata = PngDataHelper.AddMetadata( + await filePath.ReadAllBytesAsync(), + args.Parameters!, + args.Project! + ); + + await using (var outputStream = filePath.Info.OpenWrite()) + { + await outputStream.WriteAsync(bytesWithMetadata); + await outputStream.FlushAsync(); + } + + outputImages.Add(new ImageSource(filePath)); + + EventManager.Instance.OnImageFileAdded(filePath); + } + + // Download all images to make grid, if multiple + if (outputImages.Count > 1) + { + var loadedImages = outputImages + .Select(i => SKImage.FromEncodedData(i.LocalFile?.Info.OpenRead())) + .ToImmutableArray(); + + var grid = ImageProcessor.CreateImageGrid(loadedImages); + var gridBytes = grid.Encode().ToArray(); + var gridBytesWithMetadata = PngDataHelper.AddMetadata( + gridBytes, + args.Parameters!, + args.Project! + ); + + // Save to disk + var lastName = outputImages.Last().LocalFile?.Info.Name; + var gridPath = args.Client.OutputImagesDir!.JoinFile($"grid-{lastName}"); + + await using (var fileStream = gridPath.Info.OpenWrite()) + { + await fileStream.WriteAsync(gridBytesWithMetadata); + } + + // Insert to start of images + var gridImage = new ImageSource(gridPath); + // Preload + await gridImage.GetBitmapAsync(); + ImageGalleryCardViewModel.ImageSources.Add(gridImage); + + EventManager.Instance.OnImageFileAdded(gridPath); + } + + // Add rest of images + foreach (var img in outputImages) + { + // Preload + await img.GetBitmapAsync(); + ImageGalleryCardViewModel.ImageSources.Add(img); + } + } + + /// + /// Implementation for Generate Image + /// + protected virtual Task GenerateImageImpl( + GenerateOverrides overrides, + CancellationToken cancellationToken + ) + { + return Task.CompletedTask; + } + + /// + /// Command for the Generate Image button + /// + /// Optional overrides (side buttons) + /// Cancellation token + [RelayCommand(IncludeCancelCommand = true, FlowExceptionsToTaskScheduler = true)] + private async Task GenerateImage( + GenerateFlags options = default, + CancellationToken cancellationToken = default + ) + { + try + { + var overrides = GenerateOverrides.FromFlags(options); + + await GenerateImageImpl(overrides, cancellationToken); + } + catch (OperationCanceledException) + { + Logger.Debug($"Image Generation Canceled"); + } + } + + /// + /// Handles the preview image received event from the websocket. + /// Updates the preview image in the image gallery. + /// + protected virtual void OnPreviewImageReceived(object? sender, ComfyWebSocketImageData args) + { + ImageGalleryCardViewModel.SetPreviewImage(args.ImageBytes); + } + + /// + /// Handles the progress update received event from the websocket. + /// Updates the progress view model. + /// + protected virtual void OnProgressUpdateReceived( + object? sender, + ComfyProgressUpdateEventArgs args + ) + { + Dispatcher.UIThread.Post(() => + { + OutputProgress.Value = args.Value; + OutputProgress.Maximum = args.Maximum; + OutputProgress.IsIndeterminate = false; + + OutputProgress.Text = + $"({args.Value} / {args.Maximum})" + + (args.RunningNode != null ? $" {args.RunningNode}" : ""); + }); + } + + public class ImageGenerationEventArgs : EventArgs + { + public required ComfyClient Client { get; init; } + public required NodeDictionary Nodes { get; init; } + public required IReadOnlyList OutputNodeNames { get; init; } + public GenerationParameters? Parameters { get; set; } + public InferenceProjectDocument? Project { get; set; } + } + + public class BuildPromptEventArgs : EventArgs + { + public ComfyNodeBuilder Builder { get; } = new(); + public GenerateOverrides Overrides { get; set; } = new(); + } + + [Flags] + public enum GenerateFlags + { + None = 0, + HiresFixEnable = 1 << 1, + HiresFixDisable = 1 << 2, + UseCurrentSeed = 1 << 3, + UseRandomSeed = 1 << 4 + } + + public class GenerateOverrides + { + public bool? IsHiresFixEnabled { get; set; } + public bool? UseCurrentSeed { get; set; } + + public static GenerateOverrides FromFlags(GenerateFlags flags) + { + var overrides = new GenerateOverrides() + { + IsHiresFixEnabled = flags.HasFlag(GenerateFlags.HiresFixEnable) + ? true + : flags.HasFlag(GenerateFlags.HiresFixDisable) + ? false + : null, + UseCurrentSeed = flags.HasFlag(GenerateFlags.UseCurrentSeed) + ? true + : flags.HasFlag(GenerateFlags.UseRandomSeed) + ? false + : null + }; + + return overrides; + } + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 222c5b2ec..2460f6557 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -1,35 +1,21 @@ using System; -using System.Collections.Generic; -using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; -using System.IO; using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using AsyncAwaitBestPractices; -using Avalonia.Threading; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; using DynamicData.Binding; using NLog; -using Refit; -using SkiaSharp; using StabilityMatrix.Avalonia.Extensions; -using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; -using StabilityMatrix.Core.Inference; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes; -using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; -using StabilityMatrix.Core.Models.Database; -using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Services; using InferenceTextToImageView = StabilityMatrix.Avalonia.Views.Inference.InferenceTextToImageView; @@ -38,19 +24,12 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(InferenceTextToImageView), persistent: true)] -public partial class InferenceTextToImageViewModel - : InferenceTabViewModelBase, - IImageGalleryComponent +public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly INotificationService notificationService; - private readonly ServiceManager vmFactory; private readonly IModelIndexService modelIndexService; - private readonly IImageIndexService imageIndexService; - - [JsonIgnore] - public IInferenceClientManager ClientManager { get; } [JsonIgnore] public StackCardViewModel StackCardViewModel { get; } @@ -61,12 +40,6 @@ public partial class InferenceTextToImageViewModel [JsonPropertyName("Sampler")] public SamplerCardViewModel SamplerCardViewModel { get; } - [JsonPropertyName("ImageGallery")] - public ImageGalleryCardViewModel ImageGalleryCardViewModel { get; } - - [JsonPropertyName("ImageFolder")] - public ImageFolderCardViewModel ImageFolderCardViewModel { get; } - [JsonPropertyName("Prompt")] public PromptCardViewModel PromptCardViewModel { get; } @@ -97,26 +70,16 @@ public bool IsUpscaleEnabled set => StackCardViewModel.GetCard(1).IsEnabled = value; } - [JsonIgnore] - public ProgressViewModel OutputProgress { get; } = new(); - - [ObservableProperty] - [property: JsonIgnore] - private string? outputImageSource; - public InferenceTextToImageViewModel( INotificationService notificationService, IInferenceClientManager inferenceClientManager, ServiceManager vmFactory, - IModelIndexService modelIndexService, - IImageIndexService imageIndexService + IModelIndexService modelIndexService ) + : base(vmFactory, inferenceClientManager, notificationService) { this.notificationService = notificationService; - this.vmFactory = vmFactory; this.modelIndexService = modelIndexService; - this.imageIndexService = imageIndexService; - ClientManager = inferenceClientManager; // Get sub view models from service manager @@ -133,8 +96,6 @@ IImageIndexService imageIndexService samplerCard.IsSchedulerSelectionEnabled = true; }); - ImageGalleryCardViewModel = vmFactory.Get(); - ImageFolderCardViewModel = vmFactory.Get(); PromptCardViewModel = vmFactory.Get(); HiresSamplerCardViewModel = vmFactory.Get(samplerCard => { @@ -181,17 +142,16 @@ IImageIndexService imageIndexService SamplerCardViewModel.IsRefinerStepsEnabled = e.Sender is { IsRefinerSelectionEnabled: true, SelectedRefiner: not null }; }); - - GenerateImageCommand.WithConditionalNotificationErrorHandler(notificationService); } - private (NodeDictionary prompt, string[] outputs) BuildPrompt( - GenerateOverrides? overrides = null - ) + /// + protected override void BuildPrompt(BuildPromptEventArgs args) { - using var _ = new CodeTimer(); + base.BuildPrompt(args); - var builder = new ComfyNodeBuilder(); + using var _ = CodeTimer.StartDebug(); + + var builder = args.Builder; var nodes = builder.Nodes; // Setup empty latent @@ -232,7 +192,7 @@ ModelCardViewModel is } // If hi-res fix is enabled, add the LatentUpscale node and another KSampler node - if (overrides?.IsHiresFixEnabled ?? IsHiresFixEnabled) + if (args.Overrides.IsHiresFixEnabled ?? IsHiresFixEnabled) { // Requested upscale to this size var hiresSize = builder.Connections.GetScaledLatentSize( @@ -309,35 +269,12 @@ ModelCardViewModel is // Set as the image output builder.Connections.Image = postUpscaleGroup.Output; } - - // Output - var outputName = builder.SetupOutputImage(); - - return (builder.ToNodeDictionary(), new[] { outputName }); - } - - private void OnProgressUpdateReceived(object? sender, ComfyProgressUpdateEventArgs args) - { - Dispatcher.UIThread.Post(() => - { - OutputProgress.Value = args.Value; - OutputProgress.Maximum = args.Maximum; - OutputProgress.IsIndeterminate = false; - - OutputProgress.Text = - $"({args.Value} / {args.Maximum})" - + (args.RunningNode != null ? $" {args.RunningNode}" : ""); - }); - } - - private void OnPreviewImageReceived(object? sender, ComfyWebSocketImageData args) - { - ImageGalleryCardViewModel.SetPreviewImage(args.ImageBytes); } - private async Task GenerateImageImpl( - GenerateOverrides? overrides = null, - CancellationToken cancellationToken = default + /// + protected override async Task GenerateImageImpl( + GenerateOverrides overrides, + CancellationToken cancellationToken ) { // Validate the prompts @@ -359,192 +296,33 @@ private async Task GenerateImageImpl( seedCard.GenerateNewSeed(); } - var client = ClientManager.Client; + var buildPromptArgs = new BuildPromptEventArgs { Overrides = overrides }; + BuildPrompt(buildPromptArgs); - var (nodes, outputNodeNames) = BuildPrompt(overrides); - - var generationInfo = new GenerationParameters - { - Seed = (ulong)seedCard.Seed, - Steps = SamplerCardViewModel.Steps, - CfgScale = SamplerCardViewModel.CfgScale, - Sampler = SamplerCardViewModel.SelectedSampler?.Name, - ModelName = ModelCardViewModel.SelectedModelName, - // TODO: ModelHash - PositivePrompt = PromptCardViewModel.PromptDocument.Text, - NegativePrompt = PromptCardViewModel.NegativePromptDocument.Text - }; - var smproj = InferenceProjectDocument.FromLoadable(this); - - // Connect preview image handler - client.PreviewImageReceived += OnPreviewImageReceived; - - ComfyTask? promptTask = null; - try + var generationArgs = new ImageGenerationEventArgs { - // Register to interrupt if user cancels - cancellationToken.Register(() => - { - Logger.Info("Cancelling prompt"); - client - .InterruptPromptAsync(new CancellationTokenSource(5000).Token) - .SafeFireAndForget(); - }); - - try - { - promptTask = await client.QueuePromptAsync(nodes, cancellationToken); - } - catch (ApiException e) + Client = ClientManager.Client, + Nodes = buildPromptArgs.Builder.ToNodeDictionary(), + OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(), + Parameters = new GenerationParameters { - Logger.Warn(e, "Api exception while queuing prompt"); - await DialogHelper.CreateApiExceptionDialog(e, "Api Error").ShowAsync(); - return; - } - - // Register progress handler - promptTask.ProgressUpdate += OnProgressUpdateReceived; - - // Wait for prompt to finish - await promptTask.Task.WaitAsync(cancellationToken); - Logger.Trace($"Prompt task {promptTask.Id} finished"); - - // Get output images - var imageOutputs = await client.GetImagesForExecutedPromptAsync( - promptTask.Id, - cancellationToken - ); - - ImageGalleryCardViewModel.ImageSources.Clear(); - - if (!imageOutputs.TryGetValue(outputNodeNames[0], out var images) || images is null) - { - // No images match - notificationService.Show("No output", "Did not receive any output images"); - return; - } - - List outputImages; - // Use local file path if available, otherwise use remote URL - if (client.OutputImagesDir is { } outputPath) - { - outputImages = new List(); - foreach (var image in images) - { - var filePath = image.ToFilePath(outputPath); - - var bytesWithMetadata = PngDataHelper.AddMetadata( - await filePath.ReadAllBytesAsync(), - generationInfo, - smproj - ); - - /*await using (var readStream = filePath.Info.OpenWrite()) - { - using (var reader = new BinaryReader(readStream)) - { - - } - }*/ - - await using (var outputStream = filePath.Info.OpenWrite()) - { - await outputStream.WriteAsync(bytesWithMetadata); - await outputStream.FlushAsync(); - } - - outputImages.Add(new ImageSource(filePath)); - - imageIndexService.OnImageAdded(filePath); - } - } - else - { - outputImages = images! - .Select(i => new ImageSource(i.ToUri(client.BaseAddress))) - .ToList(); - } - - // Download all images to make grid, if multiple - if (outputImages.Count > 1) - { - var loadedImages = outputImages - .Select(i => SKImage.FromEncodedData(i.LocalFile?.Info.OpenRead())) - .ToImmutableArray(); - - var grid = ImageProcessor.CreateImageGrid(loadedImages); - var gridBytes = grid.Encode().ToArray(); - var gridBytesWithMetadata = PngDataHelper.AddMetadata( - gridBytes, - generationInfo, - smproj - ); - - // Save to disk - var lastName = outputImages.Last().LocalFile?.Info.Name; - var gridPath = client.OutputImagesDir!.JoinFile($"grid-{lastName}"); - - await using (var fileStream = gridPath.Info.OpenWrite()) - { - await fileStream.WriteAsync(gridBytesWithMetadata, cancellationToken); - } - - // Insert to start of images - var gridImage = new ImageSource(gridPath); - // Preload - await gridImage.GetBitmapAsync(); - ImageGalleryCardViewModel.ImageSources.Add(gridImage); - - imageIndexService.OnImageAdded(gridPath); - } - - // Add rest of images - foreach (var img in outputImages) - { - // Preload - await img.GetBitmapAsync(); - ImageGalleryCardViewModel.ImageSources.Add(img); - } - } - finally - { - // Disconnect progress handler - OutputProgress.Value = 0; - OutputProgress.Text = ""; - ImageGalleryCardViewModel.PreviewImage?.Dispose(); - ImageGalleryCardViewModel.PreviewImage = null; - ImageGalleryCardViewModel.IsPreviewOverlayEnabled = false; - - promptTask?.Dispose(); - client.PreviewImageReceived -= OnPreviewImageReceived; - } - } - - [RelayCommand(IncludeCancelCommand = true, FlowExceptionsToTaskScheduler = true)] - private async Task GenerateImage( - string? options = null, - CancellationToken cancellationToken = default - ) - { - try - { - var overrides = new GenerateOverrides - { - IsHiresFixEnabled = options?.Contains("hires_fix"), - UseCurrentSeed = options?.Contains("current_seed") - }; - - await GenerateImageImpl(overrides, cancellationToken); - } - catch (OperationCanceledException e) - { - Logger.Debug($"[Image Generation Canceled] {e.Message}"); - } - } + Seed = (ulong)seedCard.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 + }, + Project = InferenceProjectDocument.FromLoadable(this) + }; - internal class GenerateOverrides - { - public bool? IsHiresFixEnabled { get; set; } - public bool? UseCurrentSeed { get; set; } + await RunGeneration(generationArgs, cancellationToken); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs index a41cf7b56..8fb36d0cf 100644 --- a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs @@ -70,6 +70,8 @@ public partial class InferenceViewModel : PageViewModelBase public IInferenceClientManager ClientManager { get; } + public SharedState SharedState { get; } + public ObservableCollection Tabs { get; } = new(); [ObservableProperty] @@ -93,7 +95,8 @@ public InferenceViewModel( IInferenceClientManager inferenceClientManager, ISettingsManager settingsManager, IModelIndexService modelIndexService, - ILiteDbContext liteDbContext + ILiteDbContext liteDbContext, + SharedState sharedState ) { this.vmFactory = vmFactory; @@ -103,6 +106,7 @@ ILiteDbContext liteDbContext this.liteDbContext = liteDbContext; ClientManager = inferenceClientManager; + SharedState = sharedState; // Keep RunningPackage updated with the current package pair EventManager.Instance.RunningPackageStatusChanged += OnRunningPackageStatusChanged; @@ -240,8 +244,10 @@ private async Task SyncTabStatesWithDatabase() continue; } + var projectPath = projectFile.ToString(); + var entry = await liteDbContext.InferenceProjects.FindOneAsync( - p => p.FilePath == projectFile.ToString() + p => p.FilePath == projectPath ); // Create if not found @@ -299,9 +305,14 @@ private async Task SyncTabStateWithDatabase(InferenceTabViewModelBase tab) /// When the + button on the tab control is clicked, add a new tab. /// [RelayCommand] - private void AddTab() + public void AddTab(InferenceProjectType type = InferenceProjectType.TextToImage) { - var tab = vmFactory.Get(); + if (type.ToViewModelType() is not { } vmType) + { + return; + } + + var tab = (InferenceTabViewModelBase)vmFactory.Get(vmType); Tabs.Add(tab); // Set as new selected tab diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml b/StabilityMatrix.Avalonia/Views/InferencePage.axaml index 90480d1de..4180498a4 100644 --- a/StabilityMatrix.Avalonia/Views/InferencePage.axaml +++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml @@ -40,7 +40,7 @@ --> @@ -54,6 +54,17 @@ IsEnabled="False" Label="Inpaint" ToolTip.Tip="Inpaint" /> + + + + + diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs b/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs index 7f61d588e..31016df7a 100644 --- a/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs @@ -6,6 +6,7 @@ using Avalonia.Interactivity; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.Views; @@ -47,8 +48,13 @@ private void ShowAddTabMenu(bool isTransient) addTabFlyout.ShowAt(AddButton); } - private void AddTabMenu_TextToImageButton_OnClick(object? sender, RoutedEventArgs e) + private void AddTabMenu_TextToImage_OnClick(object? sender, RoutedEventArgs e) { - (DataContext as InferenceViewModel)!.AddTabCommand.Execute(null); + (DataContext as InferenceViewModel)!.AddTab(); + } + + private void AddTabMenu_Upscale_OnClick(object? sender, RoutedEventArgs e) + { + (DataContext as InferenceViewModel)!.AddTab(InferenceProjectType.Upscale); } } diff --git a/StabilityMatrix.Core/Helper/EventManager.cs b/StabilityMatrix.Core/Helper/EventManager.cs index 4a40d1f77..10e730d7b 100644 --- a/StabilityMatrix.Core/Helper/EventManager.cs +++ b/StabilityMatrix.Core/Helper/EventManager.cs @@ -1,5 +1,6 @@ using System.Globalization; using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.PackageModification; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Update; @@ -32,6 +33,8 @@ private EventManager() { } public event EventHandler? ModelIndexChanged; + public event EventHandler? ImageFileAdded; + public void OnGlobalProgressChanged(int progress) => GlobalProgressChanged?.Invoke(this, progress); @@ -69,4 +72,6 @@ public void OnPackageInstallProgressAdded(IPackageModificationRunner runner) => public void OnCultureChanged(CultureInfo culture) => CultureChanged?.Invoke(this, culture); public void OnModelIndexChanged() => ModelIndexChanged?.Invoke(this, EventArgs.Empty); + + public void OnImageFileAdded(FilePath filePath) => ImageFileAdded?.Invoke(this, filePath); } diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs index 7fd23e9fc..b945261e6 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs @@ -529,6 +529,11 @@ public class NodeBuilderConnections public Size LatentSize { get; set; } public ImageNodeConnection? Image { get; set; } + public Size ImageSize { get; set; } + + public List OutputNodes { get; } = new(); + + public IEnumerable OutputNodeNames => OutputNodes.Select(n => n.Name); /// /// Gets the latent size scaled by a given factor diff --git a/StabilityMatrix.Core/Services/IImageIndexService.cs b/StabilityMatrix.Core/Services/IImageIndexService.cs index 73b8e0d1a..5962df870 100644 --- a/StabilityMatrix.Core/Services/IImageIndexService.cs +++ b/StabilityMatrix.Core/Services/IImageIndexService.cs @@ -21,8 +21,6 @@ public interface IImageIndexService Task RefreshIndex(IndexCollection indexCollection); - void OnImageAdded(FilePath filePath); - /// /// Refreshes the index of local images in the background /// diff --git a/StabilityMatrix.Core/Services/ImageIndexService.cs b/StabilityMatrix.Core/Services/ImageIndexService.cs index 8ac0152e2..c009e8ee4 100644 --- a/StabilityMatrix.Core/Services/ImageIndexService.cs +++ b/StabilityMatrix.Core/Services/ImageIndexService.cs @@ -39,12 +39,7 @@ ISettingsManager settingsManager RelativePath = "inference" }; - /*inferenceImagesSource - .Connect() - .DeferUntilLoaded() - .SortBy(file => file.LastModifiedAt, SortDirection.Descending) - .Bind(InferenceImages) - .Subscribe();*/ + EventManager.Instance.ImageFileAdded += OnImageFileAdded; } /// @@ -125,8 +120,7 @@ var file in imagesDir.Info ); } - /// - public void OnImageAdded(FilePath filePath) + private void OnImageFileAdded(object? sender, FilePath filePath) { var fullPath = settingsManager.ImagesDirectory.JoinDir(InferenceImages.RelativePath!); From a40ee8fe0d6af468b840a2d92135dd55e711d54a Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 14 Sep 2023 16:01:23 -0400 Subject: [PATCH 285/474] Remove duplicate package ref --- StabilityMatrix.Core/StabilityMatrix.Core.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/StabilityMatrix.Core/StabilityMatrix.Core.csproj b/StabilityMatrix.Core/StabilityMatrix.Core.csproj index 3bfc1be61..f0456a2f0 100644 --- a/StabilityMatrix.Core/StabilityMatrix.Core.csproj +++ b/StabilityMatrix.Core/StabilityMatrix.Core.csproj @@ -44,6 +44,5 @@ - From fdea007474f7d98d6b35dc4e6f13f7cbdb83c689 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 14 Sep 2023 19:36:32 -0400 Subject: [PATCH 286/474] Add Upscale page --- .../Controls/Dock/DockUserControlBase.cs | 16 +- .../Controls/SelectImageCard.axaml | 4 +- .../Controls/SelectImageCard.axaml.cs | 8 +- .../DesignData/MockInferenceClientManager.cs | 21 +- .../Models/Inference/GenerateFlags.cs | 14 + .../Models/Inference/GenerateOverrides.cs | 26 ++ .../Models/InferenceProjectDocument.cs | 11 +- .../Services/IInferenceClientManager.cs | 9 + .../Services/InferenceClientManager.cs | 55 +++- .../Base/InferenceGenerationViewModelBase.cs | 36 +-- .../Base/InferenceTabViewModelBase.cs | 6 +- .../InferenceImageUpscaleViewModel.cs | 285 ++++++------------ .../InferenceTextToImageViewModel.cs | 5 +- .../Inference/SelectImageCardViewModel.cs | 116 ++++++- .../ViewModels/InferenceViewModel.cs | 9 +- .../Inference/InferenceImageUpscaleView.axaml | 109 ++++--- .../Inference/InferenceTextToImageView.axaml | 37 +-- .../Views/InferencePage.axaml | 5 +- .../Views/InferencePage.axaml.cs | 7 +- StabilityMatrix.Core/Inference/ComfyClient.cs | 5 + .../Api/Comfy/NodeTypes/NodeConnections.cs | 2 + .../Api/Comfy/Nodes/ComfyNodeBuilder.cs | 108 ++++++- 22 files changed, 539 insertions(+), 355 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Models/Inference/GenerateFlags.cs create mode 100644 StabilityMatrix.Avalonia/Models/Inference/GenerateOverrides.cs diff --git a/StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs b/StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs index 8301955d8..7f4b18ef7 100644 --- a/StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs +++ b/StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Threading; using Dock.Avalonia.Controls; using Dock.Model; using Dock.Model.Avalonia.Json; @@ -14,7 +15,7 @@ namespace StabilityMatrix.Avalonia.Controls.Dock; /// public abstract class DockUserControlBase : DropTargetUserControlBase { - private DockControl _dock = null!; + protected DockControl? BaseDock; protected readonly AvaloniaDockSerializer DockSerializer = new(); protected readonly DockState DockState = new(); @@ -23,27 +24,30 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - _dock = + BaseDock = this.FindControl("Dock") ?? throw new NullReferenceException("DockControl not found"); - if (_dock.Layout is { } layout) + if (BaseDock.Layout is { } layout) { - DockState.Save(layout); + Dispatcher.UIThread.Post(() => DockState.Save(layout), DispatcherPriority.Background); } } protected virtual void LoadDockLayout(string data) { + if (BaseDock is null) + return; + if (DockSerializer.Deserialize(data) is { } layout) { - _dock.Layout = layout; + BaseDock.Layout = layout; DockState.Restore(layout); } } protected virtual string SaveDockLayout() { - return DockSerializer.Serialize(_dock.Layout); + return BaseDock is null ? string.Empty : DockSerializer.Serialize(BaseDock.Layout); } } diff --git a/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml b/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml index 53d73b9de..5f4b0b983 100644 --- a/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml @@ -42,8 +42,9 @@ CornerRadius="4" VerticalAlignment="Stretch" VerticalContentAlignment="Stretch" + CurrentImage="{Binding CurrentBitmap, Mode=OneWayToSource}" IsVisible="{Binding !IsSelectionAvailable}" - Source="{Binding ImageSource.RemoteUrl}"/> + Source="{Binding ImageSource}"/> + public bool CanUserDisconnect => IsConnected && !IsConnecting; + /// + public Task CopyImageToInputAsync( + FilePath imageFile, + CancellationToken cancellationToken = default + ) + { + return Task.CompletedTask; + } + + /// + public Task WriteImageToInputAsync( + ImageSource imageSource, + CancellationToken cancellationToken = default + ) + { + return Task.CompletedTask; + } + public async Task ConnectAsync(CancellationToken cancellationToken = default) { IsConnecting = true; diff --git a/StabilityMatrix.Avalonia/Models/Inference/GenerateFlags.cs b/StabilityMatrix.Avalonia/Models/Inference/GenerateFlags.cs new file mode 100644 index 000000000..bf95a7076 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/Inference/GenerateFlags.cs @@ -0,0 +1,14 @@ +using System; + +namespace StabilityMatrix.Avalonia.Models.Inference; + +[Flags] +public enum GenerateFlags +{ + None = 0, + HiresFixEnable = 1 << 1, + HiresFixDisable = 1 << 2, + UseCurrentSeed = 1 << 3, + UseRandomSeed = 1 << 4, + HiresFixAndUseCurrentSeed = HiresFixEnable | UseCurrentSeed, +} diff --git a/StabilityMatrix.Avalonia/Models/Inference/GenerateOverrides.cs b/StabilityMatrix.Avalonia/Models/Inference/GenerateOverrides.cs new file mode 100644 index 000000000..2220a1596 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/Inference/GenerateOverrides.cs @@ -0,0 +1,26 @@ +namespace StabilityMatrix.Avalonia.Models.Inference; + +public class GenerateOverrides +{ + public bool? IsHiresFixEnabled { get; set; } + public bool? UseCurrentSeed { get; set; } + + public static GenerateOverrides FromFlags(GenerateFlags flags) + { + var overrides = new GenerateOverrides + { + IsHiresFixEnabled = flags.HasFlag(GenerateFlags.HiresFixEnable) + ? true + : flags.HasFlag(GenerateFlags.HiresFixDisable) + ? false + : null, + UseCurrentSeed = flags.HasFlag(GenerateFlags.UseCurrentSeed) + ? true + : flags.HasFlag(GenerateFlags.UseRandomSeed) + ? false + : null + }; + + return overrides; + } +} diff --git a/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs b/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs index a28420d0b..c57439d19 100644 --- a/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs +++ b/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs @@ -30,6 +30,7 @@ public static InferenceProjectDocument FromLoadable(IJsonLoadableState loadableM ProjectType = loadableModel switch { InferenceTextToImageViewModel => InferenceProjectType.TextToImage, + InferenceImageUpscaleViewModel => InferenceProjectType.Upscale, _ => throw new InvalidOperationException( $"Unknown loadable model type: {loadableModel.GetType()}" @@ -39,16 +40,6 @@ public static InferenceProjectDocument FromLoadable(IJsonLoadableState loadableM }; } - public Type GetViewModelType() - { - return ProjectType switch - { - InferenceProjectType.TextToImage => typeof(InferenceTextToImageViewModel), - InferenceProjectType.Unknown => throw new InvalidOperationException(), - _ => throw new ArgumentOutOfRangeException(nameof(ProjectType), ProjectType, null) - }; - } - public void VerifyVersion() { if (Version < 2) diff --git a/StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs index 78f879df8..bc6afaff8 100644 --- a/StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs @@ -5,9 +5,11 @@ using System.Threading; using System.Threading.Tasks; using DynamicData.Binding; +using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Core.Inference; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; +using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Avalonia.Services; @@ -45,6 +47,13 @@ public interface IInferenceClientManager IObservableCollection Upscalers { get; } IObservableCollection Schedulers { get; } + Task CopyImageToInputAsync(FilePath imageFile, CancellationToken cancellationToken = default); + + Task WriteImageToInputAsync( + ImageSource imageSource, + CancellationToken cancellationToken = default + ); + Task ConnectAsync(CancellationToken cancellationToken = default); Task ConnectAsync(PackagePair packagePair, CancellationToken cancellationToken = default); diff --git a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs index ac6f6dc50..28ec96a19 100644 --- a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs @@ -8,6 +8,8 @@ using DynamicData; using DynamicData.Binding; using Microsoft.Extensions.Logging; +using SkiaSharp; +using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Inference; @@ -229,6 +231,50 @@ private void ResetSharedProperties() schedulersSource.EditDiff(ComfyScheduler.Defaults, ComfyScheduler.Comparer); } + /// + public async Task CopyImageToInputAsync( + FilePath imageFile, + CancellationToken cancellationToken = default + ) + { + if (!IsConnected) + return; + + if (Client.InputImagesDir is not { } inputImagesDir) + { + throw new InvalidOperationException("InputImagesDir is null"); + } + + var inferenceInputs = inputImagesDir.JoinDir("Inference"); + inferenceInputs.Create(); + + var destination = inferenceInputs.JoinFile(imageFile.Name); + + // Read to SKImage then write to file, to prevent errors from metadata + await using var imageStream = imageFile.Info.OpenRead(); + using var image = SKImage.FromEncodedData(imageStream); + await using var destinationStream = destination.Info.OpenWrite(); + image.Encode(SKEncodedImageFormat.Png, 100).SaveTo(destinationStream); + } + + /// + public async Task WriteImageToInputAsync( + ImageSource imageSource, + CancellationToken cancellationToken = default + ) + { + if (!IsConnected) + return; + + if (Client.InputImagesDir is not { } inputImagesDir) + { + throw new InvalidOperationException("InputImagesDir is null"); + } + + var inferenceInputs = inputImagesDir.JoinDir("Inference"); + inferenceInputs.Create(); + } + private async Task ConnectAsyncImpl(Uri uri, CancellationToken cancellationToken = default) { if (IsConnected) @@ -297,10 +343,11 @@ await comfyPackage.SetupInferenceOutputFolderLinks( await ConnectAsyncImpl(uri, cancellationToken); - // Set output path - Client!.OutputImagesDir = new DirectoryPath(packagePair.InstalledPackage.FullPath).JoinDir( - "output" - ); + var packageDir = new DirectoryPath(packagePair.InstalledPackage.FullPath); + + // Set package paths + Client!.OutputImagesDir = packageDir.JoinDir("output"); + Client!.InputImagesDir = packageDir.JoinDir("input"); } public async Task CloseAsync() diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs index b46fe67b7..80aae8470 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -15,6 +15,7 @@ using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Core.Helper; @@ -316,39 +317,4 @@ public class BuildPromptEventArgs : EventArgs public ComfyNodeBuilder Builder { get; } = new(); public GenerateOverrides Overrides { get; set; } = new(); } - - [Flags] - public enum GenerateFlags - { - None = 0, - HiresFixEnable = 1 << 1, - HiresFixDisable = 1 << 2, - UseCurrentSeed = 1 << 3, - UseRandomSeed = 1 << 4 - } - - public class GenerateOverrides - { - public bool? IsHiresFixEnabled { get; set; } - public bool? UseCurrentSeed { get; set; } - - public static GenerateOverrides FromFlags(GenerateFlags flags) - { - var overrides = new GenerateOverrides() - { - IsHiresFixEnabled = flags.HasFlag(GenerateFlags.HiresFixEnable) - ? true - : flags.HasFlag(GenerateFlags.HiresFixDisable) - ? false - : null, - UseCurrentSeed = flags.HasFlag(GenerateFlags.UseCurrentSeed) - ? true - : flags.HasFlag(GenerateFlags.UseRandomSeed) - ? false - : null - }; - - return overrides; - } - } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs index d2c97d524..c13c8b67e 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs @@ -77,6 +77,7 @@ public void DragOver(object? sender, DragEventArgs e) if (e.Data.GetDataFormats().Contains(DataFormats.Files)) { e.Handled = true; + e.DragEffects = DragDropEffects.None; return; } @@ -103,7 +104,10 @@ public void Drop(object? sender, DragEventArgs e) ); // Check project type matches - if (project?.GetViewModelType() == GetType() && project.State is not null) + if ( + project?.ProjectType.ToViewModelType() == GetType() + && project.State is not null + ) { LoadStateFromJsonObject(project.State); } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs index ba077b42f..e3f81abb1 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs @@ -1,79 +1,66 @@ using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.IO; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; -using Avalonia.Media.Imaging; +using Avalonia.Controls.Shapes; using Avalonia.Threading; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; +using DynamicData.Binding; using NLog; -using Refit; -using SkiaSharp; -using StabilityMatrix.Avalonia.Helpers; +using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Inference; using StabilityMatrix.Core.Attributes; -using StabilityMatrix.Core.Helper; -using StabilityMatrix.Core.Inference; +using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; -using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; -using StabilityMatrix.Core.Services; +using Path = System.IO.Path; + #pragma warning disable CS0657 // Not a valid attribute location for this declaration namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(InferenceImageUpscaleView), persistent: true)] -public partial class InferenceImageUpscaleViewModel : InferenceTabViewModelBase +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly INotificationService notificationService; - private readonly ServiceManager vmFactory; - private readonly IModelIndexService modelIndexService; - - public IInferenceClientManager ClientManager { get; } - public ImageGalleryCardViewModel ImageGalleryCardViewModel { get; } + [JsonIgnore] public StackCardViewModel StackCardViewModel { get; } - public UpscalerCardViewModel UpscalerCardViewModel => - StackCardViewModel.GetCard().GetCard(); + [JsonPropertyName("Upscaler")] + public UpscalerCardViewModel UpscalerCardViewModel { get; } - [JsonIgnore] - public ProgressViewModel OutputProgress { get; } = new(); + [JsonPropertyName("SelectImage")] + public SelectImageCardViewModel SelectImageCardViewModel { get; } - [ObservableProperty] - [property: JsonIgnore] - private string? outputImageSource; + public bool IsUpscaleEnabled + { + get => StackCardViewModel.GetCard().IsEnabled; + set => StackCardViewModel.GetCard().IsEnabled = value; + } public InferenceImageUpscaleViewModel( INotificationService notificationService, IInferenceClientManager inferenceClientManager, - ServiceManager vmFactory, - IModelIndexService modelIndexService + ServiceManager vmFactory ) + : base(vmFactory, inferenceClientManager, notificationService) { this.notificationService = notificationService; - this.vmFactory = vmFactory; - this.modelIndexService = modelIndexService; - ClientManager = inferenceClientManager; - - // Get sub view models from service manager - var seedCard = vmFactory.Get(); - seedCard.GenerateNewSeed(); - - ImageGalleryCardViewModel = vmFactory.Get(); + UpscalerCardViewModel = vmFactory.Get(); + SelectImageCardViewModel = vmFactory.Get(); StackCardViewModel = vmFactory.Get(); - StackCardViewModel.AddCards( new LoadableViewModelBase[] { @@ -81,61 +68,73 @@ IModelIndexService modelIndexService vmFactory.Get(stackExpander => { stackExpander.Title = "Upscale"; - stackExpander.AddCards( - new LoadableViewModelBase[] - { - // Post processing upscaler - vmFactory.Get(), - } - ); + stackExpander.AddCards(new LoadableViewModelBase[] { UpscalerCardViewModel }); }) } ); - // GenerateImageCommand.WithNotificationErrorHandler(notificationService); + // On any new images, copy to input dir + SelectImageCardViewModel + .WhenPropertyChanged(x => x.ImageSource) + .Subscribe(e => + { + if (e.Value?.LocalFile?.FullPath is { } path) + { + ClientManager.CopyImageToInputAsync(path).SafeFireAndForget(); + } + }); } - private (NodeDictionary prompt, string[] outputs) BuildPrompt() + /// + protected override void BuildPrompt(BuildPromptEventArgs args) { - using var _ = new CodeTimer(); + base.BuildPrompt(args); - var builder = new ComfyNodeBuilder(); + var builder = args.Builder; + var nodes = builder.Nodes; - return (builder.ToNodeDictionary(), new[] { "?" }); - } + // Get source image + var sourceImage = SelectImageCardViewModel.ImageSource; + var sourceImageRelativePath = Path.Combine("Inference", sourceImage!.LocalFile!.Name); + var sourceImageSize = + SelectImageCardViewModel.CurrentBitmapSize + ?? throw new InvalidOperationException("Source image size is null"); - private void OnProgressUpdateReceived(object? sender, ComfyProgressUpdateEventArgs args) - { - Dispatcher.UIThread.Post(() => - { - OutputProgress.Value = args.Value; - OutputProgress.Maximum = args.Maximum; - OutputProgress.IsIndeterminate = false; + // Set source size + builder.Connections.ImageSize = sourceImageSize; - OutputProgress.Text = - $"({args.Value} / {args.Maximum})" - + (args.RunningNode != null ? $" {args.RunningNode}" : ""); - }); - } + // Load source + var loadImage = nodes.AddNamedNode( + ComfyNodeBuilder.LoadImage("LoadImage", sourceImageRelativePath) + ); + builder.Connections.Image = loadImage.Output1; - private void OnPreviewImageReceived(object? sender, ComfyWebSocketImageData args) - { - Dispatcher.UIThread.Post(() => + // If upscale is enabled, add another upscale group + if (IsUpscaleEnabled) { - using var stream = new MemoryStream(args.ImageBytes); - - var bitmap = new Bitmap(stream); - - var currentImage = ImageGalleryCardViewModel.PreviewImage; + var upscaleSize = builder.Connections.GetScaledImageSize(UpscalerCardViewModel.Scale); + + // Build group + var upscaleGroup = builder.Group_UpscaleToImage( + "Upscale", + builder.Connections.Image!, + UpscalerCardViewModel.SelectedUpscaler!.Value, + upscaleSize.Width, + upscaleSize.Height + ); - ImageGalleryCardViewModel.PreviewImage = bitmap; - ImageGalleryCardViewModel.IsPreviewOverlayEnabled = true; + // Set as the image output + builder.Connections.Image = upscaleGroup.Output; + } - currentImage?.Dispose(); - }); + builder.SetupOutputImage(); } - private async Task GenerateImageImpl(CancellationToken cancellationToken = default) + /// + protected override async Task GenerateImageImpl( + GenerateOverrides overrides, + CancellationToken cancellationToken + ) { if (!ClientManager.IsConnected) { @@ -143,127 +142,29 @@ private async Task GenerateImageImpl(CancellationToken cancellationToken = defau return; } - var client = ClientManager.Client; - - var (nodes, outputNodeNames) = BuildPrompt(); - - // Connect preview image handler - client.PreviewImageReceived += OnPreviewImageReceived; - - ComfyTask? promptTask = null; - try + if (SelectImageCardViewModel.ImageSource?.LocalFile?.FullPath is not { } path) { - // Register to interrupt if user cancels - cancellationToken.Register(() => - { - Logger.Info("Cancelling prompt"); - client - .InterruptPromptAsync(new CancellationTokenSource(5000).Token) - .SafeFireAndForget(); - }); - - try - { - promptTask = await client.QueuePromptAsync(nodes, cancellationToken); - } - catch (ApiException e) - { - Logger.Warn(e, "Api exception while queuing prompt"); - await DialogHelper.CreateApiExceptionDialog(e, "Api Error").ShowAsync(); - return; - } - - // Register progress handler - promptTask.ProgressUpdate += OnProgressUpdateReceived; - - // Wait for prompt to finish - await promptTask.Task.WaitAsync(cancellationToken); - Logger.Trace($"Prompt task {promptTask.Id} finished"); - - // Get output images - var imageOutputs = await client.GetImagesForExecutedPromptAsync( - promptTask.Id, - cancellationToken - ); - - ImageGalleryCardViewModel.ImageSources.Clear(); - - var images = imageOutputs[outputNodeNames[0]]; - if (images is null) - return; - - List outputImages; - // Use local file path if available, otherwise use remote URL - if (client.OutputImagesDir is { } outputPath) - { - outputImages = images - .Select(i => new ImageSource(i.ToFilePath(outputPath))) - .ToList(); - } - else - { - outputImages = images - .Select(i => new ImageSource(i.ToUri(client.BaseAddress))) - .ToList(); - } - - // Download all images to make grid, if multiple - if (outputImages.Count > 1) - { - var loadedImages = outputImages - .Select(i => SKImage.FromEncodedData(i.LocalFile?.Info.OpenRead())) - .ToImmutableArray(); - - var grid = ImageProcessor.CreateImageGrid(loadedImages); - - // Save to disk - var lastName = outputImages.Last().LocalFile?.Info.Name; - var gridPath = client.OutputImagesDir!.JoinFile($"grid-{lastName}"); + notificationService.Show("No image selected", "Please select an image first"); + return; + } - await using (var fileStream = gridPath.Info.OpenWrite()) - { - await fileStream.WriteAsync(grid.Encode().ToArray(), cancellationToken); - } + await ClientManager.CopyImageToInputAsync(path, cancellationToken); - // Insert to start of images - var gridImage = new ImageSource(gridPath); - // Preload - await gridImage.GetBitmapAsync(); - ImageGalleryCardViewModel.ImageSources.Add(gridImage); - } + var buildPromptArgs = new BuildPromptEventArgs { Overrides = overrides }; + BuildPrompt(buildPromptArgs); - // Add rest of images - foreach (var img in outputImages) - { - // Preload - await img.GetBitmapAsync(); - ImageGalleryCardViewModel.ImageSources.Add(img); - } - } - finally + var generationArgs = new ImageGenerationEventArgs { - // Disconnect progress handler - OutputProgress.Value = 0; - OutputProgress.Text = ""; - ImageGalleryCardViewModel.PreviewImage?.Dispose(); - ImageGalleryCardViewModel.PreviewImage = null; - ImageGalleryCardViewModel.IsPreviewOverlayEnabled = false; - - promptTask?.Dispose(); - client.PreviewImageReceived -= OnPreviewImageReceived; - } - } + Client = ClientManager.Client, + Nodes = buildPromptArgs.Builder.ToNodeDictionary(), + OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(), + Parameters = new GenerationParameters + { + ModelName = UpscalerCardViewModel.SelectedUpscaler?.Name, + }, + Project = InferenceProjectDocument.FromLoadable(this) + }; - [RelayCommand(IncludeCancelCommand = true)] - private async Task GenerateImage(CancellationToken cancellationToken = default) - { - try - { - await GenerateImageImpl(cancellationToken); - } - catch (OperationCanceledException e) - { - Logger.Debug($"[Image Upscale Canceled] {e.Message}"); - } + await RunGeneration(generationArgs, cancellationToken); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 2460f6557..e09a75093 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -8,6 +8,7 @@ using NLog; using StabilityMatrix.Avalonia.Extensions; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; @@ -257,7 +258,7 @@ ModelCardViewModel is var upscaleSize = builder.Connections.GetScaledLatentSize(UpscalerCardViewModel.Scale); // Build group - var postUpscaleGroup = builder.Group_UpscaleToImage( + var postUpscaleGroup = builder.Group_LatentUpscaleToImage( "PostUpscale", builder.Connections.Latent!, builder.Connections.GetRefinerOrBaseVAE(), @@ -269,6 +270,8 @@ ModelCardViewModel is // Set as the image output builder.Connections.Image = postUpscaleGroup.Output; } + + builder.SetupOutputImage(); } /// diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs index 764933901..ff175ccdd 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs @@ -1,20 +1,132 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Text.Json; +using System.Linq; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Platform.Storage; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using NLog; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Models.Database; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(SelectImageCard))] -public partial class SelectImageCardViewModel : ViewModelBase +public partial class SelectImageCardViewModel : ViewModelBase, IDropTarget { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private readonly INotificationService notificationService; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsSelectionAvailable))] private ImageSource? imageSource; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CurrentBitmapSize))] + private IImage? currentBitmap; + [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsSelectionAvailable))] private bool isSelectionEnabled = true; public bool IsSelectionAvailable => IsSelectionEnabled && ImageSource == null; + + public Size? CurrentBitmapSize => + new Size( + Convert.ToInt32(CurrentBitmap?.Size.Width ?? 0), + Convert.ToInt32(CurrentBitmap?.Size.Height ?? 0) + ); + + public SelectImageCardViewModel(INotificationService notificationService) + { + this.notificationService = notificationService; + } + + /// + public void DragOver(object? sender, DragEventArgs e) + { + // 1. Context drop for LocalImageFile + if (e.Data.GetDataFormats().Contains("Context")) + { + if (e.Data.Get("Context") is LocalImageFile imageFile) + { + e.Handled = true; + return; + } + + e.DragEffects = DragDropEffects.None; + } + // 2. OS Files + if (e.Data.GetDataFormats().Contains(DataFormats.Files)) + { + e.Handled = true; + return; + } + + e.DragEffects = DragDropEffects.None; + } + + /// + public void Drop(object? sender, DragEventArgs e) + { + // 1. Context drop for LocalImageFile + if (e.Data.GetDataFormats().Contains("Context")) + { + if (e.Data.Get("Context") is LocalImageFile imageFile) + { + e.Handled = true; + + Dispatcher.UIThread.Post(() => + { + var current = ImageSource; + + ImageSource = new ImageSource(imageFile.GlobalFullPath); + + current?.Dispose(); + }); + + return; + } + } + // 2. OS Files + if (e.Data.GetDataFormats().Contains(DataFormats.Files)) + { + e.Handled = true; + + try + { + if (e.Data.Get(DataFormats.Files) is IEnumerable files) + { + var path = files.Select(f => f.Path.LocalPath).FirstOrDefault(); + + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + + Dispatcher.UIThread.Post(() => + { + var current = ImageSource; + + ImageSource = new ImageSource(path); + + current?.Dispose(); + }); + } + } + catch (Exception ex) + { + Logger.Warn(ex, "Failed to load image from drop"); + notificationService.ShowPersistent("Failed to load source image", ex.Message); + } + } + } } diff --git a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs index 8fb36d0cf..40ccd0f3f 100644 --- a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs @@ -41,7 +41,6 @@ namespace StabilityMatrix.Avalonia.ViewModels; -[Preload] [View(typeof(InferencePage))] public partial class InferenceViewModel : PageViewModelBase { @@ -224,7 +223,7 @@ project with if (Tabs.Count == 0) { - AddTab(); + AddTab(InferenceProjectType.TextToImage); } // Start a model index update @@ -305,14 +304,16 @@ private async Task SyncTabStateWithDatabase(InferenceTabViewModelBase tab) /// When the + button on the tab control is clicked, add a new tab. /// [RelayCommand] - public void AddTab(InferenceProjectType type = InferenceProjectType.TextToImage) + public void AddTab(InferenceProjectType type) { if (type.ToViewModelType() is not { } vmType) { return; } - var tab = (InferenceTabViewModelBase)vmFactory.Get(vmType); + var tab = + vmFactory.Get(vmType) as InferenceTabViewModelBase + ?? throw new NullReferenceException($"Could not create view model of type {vmType}"); Tabs.Add(tab); // Set as new selected tab diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml index c48e05a56..cbc81740a 100644 --- a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml +++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml @@ -1,12 +1,14 @@ - - - + + Proportion="0.2"> - + + + + + + + + + + + Proportion="0.5"> - public GenerationParameters? GenerationParameters { get; set; } + /// + /// Dimensions of the image + /// + public Size? ImageSize { get; set; } + /// /// File name of the relative path. /// @@ -101,6 +107,9 @@ public static LocalImageFile FromPath(FilePath filePath) FileShare.Read ); using var reader = new BinaryReader(stream); + + var imageSize = ImageMetadata.GetImageSize(reader); + var metadata = ImageMetadata.ReadTextChunk(reader, "parameters-json"); GenerationParameters? genParams = null; @@ -121,7 +130,8 @@ public static LocalImageFile FromPath(FilePath filePath) ImageType = imageType, CreatedAt = filePath.Info.CreationTimeUtc, LastModifiedAt = filePath.Info.LastWriteTimeUtc, - GenerationParameters = genParams + GenerationParameters = genParams, + ImageSize = imageSize }; } From 060f907520255ce26d702c5df48b93bf5194550f Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 16 Sep 2023 18:02:16 -0400 Subject: [PATCH 296/474] Add details footer to ImageViewerDialog --- .../Dialogs/ImageViewerViewModel.cs | 40 +++++++++++++++ .../Views/Dialogs/ImageViewerDialog.axaml | 49 ++++++++++++++----- .../Views/Dialogs/ImageViewerDialog.axaml.cs | 16 +++--- StabilityMatrix.Core/Helper/Size.cs | 24 +++++++-- 4 files changed, 108 insertions(+), 21 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs index b3e573422..5dc6e6771 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs @@ -3,6 +3,8 @@ using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models.Database; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; @@ -11,4 +13,42 @@ public partial class ImageViewerViewModel : ContentDialogViewModelBase { [ObservableProperty] private ImageSource? imageSource; + + [ObservableProperty] + private LocalImageFile? localImageFile; + + [ObservableProperty] + private bool isFooterEnabled; + + [ObservableProperty] + private string? fileNameText; + + [ObservableProperty] + private string? fileSizeText; + + [ObservableProperty] + private string? imageSizeText; + + partial void OnLocalImageFileChanged(LocalImageFile? value) + { + ImageSource?.Dispose(); + if (value?.GlobalFullPath is { } path) + { + ImageSource = new ImageSource(path); + } + } + + partial void OnImageSourceChanged(ImageSource? value) + { + if (value?.LocalFile is { } localFile) + { + FileNameText = localFile.Name; + FileSizeText = Size.FormatBase10Bytes(localFile.GetSize(true)); + + if (LocalImageFile?.GenerationParameters is { Width: > 0, Height: > 0 } parameters) + { + ImageSizeText = $"{parameters.Width} x {parameters.Height}"; + } + } + } } diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml index 7f99181c4..a67d1ec03 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml @@ -12,22 +12,16 @@ x:DataType="vmDialogs:ImageViewerViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="StabilityMatrix.Avalonia.Views.Dialogs.ImageViewerDialog"> - + + + - - - - + + + - + - - - - - - - - + + - - - + + + + + - - - - - - - - - - - - - - + VerticalAlignment="Stretch" + IsVisible="{Binding IsProgressVisible}" /> + + + + - - - - - + + + + - + - + IsOpen="{Binding !Packages.Count}" /> + diff --git a/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml b/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml index d6f672669..3fe4071e1 100644 --- a/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/ImageGalleryCard.axaml @@ -32,11 +32,11 @@ - + Margin="0,0,0,4"> + diff --git a/StabilityMatrix.Avalonia/Styles/ThemeColors.axaml b/StabilityMatrix.Avalonia/Styles/ThemeColors.axaml index 77f074e9e..7fa5a4fc3 100644 --- a/StabilityMatrix.Avalonia/Styles/ThemeColors.axaml +++ b/StabilityMatrix.Avalonia/Styles/ThemeColors.axaml @@ -36,6 +36,8 @@ + + @@ -49,6 +51,8 @@ + + @@ -62,6 +66,8 @@ + + From 3867bcaba5421a83d150da0e7285eb6d8d06806b Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 24 Sep 2023 17:27:58 -0400 Subject: [PATCH 361/474] Handle model indexing errors --- StabilityMatrix.Core/Services/ModelIndexService.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Core/Services/ModelIndexService.cs b/StabilityMatrix.Core/Services/ModelIndexService.cs index 3caea01b9..fa535f82c 100644 --- a/StabilityMatrix.Core/Services/ModelIndexService.cs +++ b/StabilityMatrix.Core/Services/ModelIndexService.cs @@ -175,6 +175,10 @@ await jsonPath.ReadAllTextAsync().ConfigureAwait(false) /// public void BackgroundRefreshIndex() { - RefreshIndex().SafeFireAndForget(); + Task.Run(async () => await RefreshIndex().ConfigureAwait(false)) + .SafeFireAndForget(ex => + { + logger.LogError(ex, "Error in background model indexing"); + }); } } From 1524fe31f272c59861fb417bfddd7522fcc2aac9 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 24 Sep 2023 17:28:23 -0400 Subject: [PATCH 362/474] Add teaching tip for inference compatible packages --- .../Views/Dialogs/OneClickInstallDialog.axaml | 38 ++++++++++++++++-- .../Dialogs/OneClickInstallDialog.axaml.cs | 39 +++++++++++++++++-- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml index fd1fbdec9..1f1e6e629 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml @@ -7,6 +7,7 @@ xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:packages="clr-namespace:StabilityMatrix.Core.Models.Packages;assembly=StabilityMatrix.Core" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="700" x:DataType="dialogs:OneClickInstallViewModel" d:DataContext="{x:Static designData:DesignData.OneClickInstallViewModel}" @@ -27,6 +28,26 @@ + + + + + + - - - - + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml.cs index 6e5f95fc7..3e6ff5117 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml.cs @@ -1,5 +1,11 @@ -using Avalonia.Controls; +using System; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; using Avalonia.Markup.Xaml; +using FluentAvalonia.UI.Controls; +using StabilityMatrix.Core.Models.Packages; namespace StabilityMatrix.Avalonia.Views.Dialogs; @@ -10,8 +16,33 @@ public OneClickInstallDialog() InitializeComponent(); } - private void InitializeComponent() + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - AvaloniaXamlLoader.Load(this); + base.OnApplyTemplate(e); } -} \ No newline at end of file + + /// + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + var teachingTip = + this.FindControl("InferenceTeachingTip") + ?? throw new InvalidOperationException("TeachingTip not found"); + ; + // Find ComfyUI listbox item + var listBox = this.FindControl("PackagesListBox"); + + // Find ComfyUI listbox item + if (listBox?.Items.Cast().FirstOrDefault(p => p is ComfyUI) is { } comfy) + { + var comfyItem = listBox.ContainerFromItem(comfy) as ListBoxItem; + + // comfyItem!.IsSelected = true; + + teachingTip.Target = comfyItem; + teachingTip.IsOpen = true; + } + } +} From 052f7aa60e532c466266b934dc2ab8c7f2e3a541 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 24 Sep 2023 17:28:37 -0400 Subject: [PATCH 363/474] Improve tab loading performance --- .../ViewModels/InferenceViewModel.cs | 81 ++++++++++--------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs index 905c45a1d..4b2ff8740 100644 --- a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs @@ -168,56 +168,60 @@ RunningPackageStatusChangedEventArgs e }); } - public override async Task OnLoadedAsync() + public override void OnLoaded() { - await base.OnLoadedAsync(); - if (!Design.IsDesignMode && !isFirstLoadComplete) { isFirstLoadComplete = true; + OnInitialLoad().SafeFireAndForget(); + } + + modelIndexService.BackgroundRefreshIndex(); + } - // Load any open projects - var openProjects = await liteDbContext.InferenceProjects.FindAsync(p => p.IsOpen); + private async Task OnInitialLoad() + { + // Load any open projects + var openProjects = await liteDbContext.InferenceProjects.FindAsync(p => p.IsOpen); - if (openProjects is not null) + if (openProjects is not null) + { + foreach (var project in openProjects.OrderBy(p => p.CurrentTabIndex)) { - foreach (var project in openProjects.OrderBy(p => p.CurrentTabIndex)) + var file = new FilePath(project.FilePath); + + if (!file.Exists) { - var file = new FilePath(project.FilePath); + // Remove from database + await liteDbContext.InferenceProjects.DeleteAsync(project.Id); + } - if (!file.Exists) + try + { + if (file.Exists) { - // Remove from database - await liteDbContext.InferenceProjects.DeleteAsync(project.Id); + await AddTabFromFile(project.FilePath); } + } + catch (Exception e) + { + Logger.Warn(e, "Failed to open project file {FilePath}", project.FilePath); - try - { - if (file.Exists) + notificationService.Show( + "Failed to open project file", + $"[{e.GetType().Name}] {e.Message}", + NotificationType.Error + ); + + // Set not open + await liteDbContext.InferenceProjects.UpdateAsync( + project with { - await AddTabFromFile(project.FilePath); + IsOpen = false, + IsSelected = false, + CurrentTabIndex = -1 } - } - catch (Exception e) - { - Logger.Warn(e, "Failed to open project file {FilePath}", project.FilePath); - - notificationService.Show( - "Failed to open project file", - $"[{e.GetType().Name}] {e.Message}", - NotificationType.Error - ); - - // Set not open - await liteDbContext.InferenceProjects.UpdateAsync( - project with - { - IsOpen = false, - IsSelected = false, - CurrentTabIndex = -1 - } - ); - } + ); } } } @@ -226,9 +230,6 @@ project with { AddTab(InferenceProjectType.TextToImage); } - - // Start a model index update - modelIndexService.BackgroundRefreshIndex(); } /// @@ -305,7 +306,7 @@ private async Task SyncTabStateWithDatabase(InferenceTabViewModelBase tab) /// When the + button on the tab control is clicked, add a new tab. /// [RelayCommand] - public void AddTab(InferenceProjectType type) + private void AddTab(InferenceProjectType type) { if (type.ToViewModelType() is not { } vmType) { From 95affba390c031f6b95a47bf83bc67065d9061ac Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 24 Sep 2023 17:30:40 -0400 Subject: [PATCH 364/474] Add launch dialog on generate when not connected --- .../Base/InferenceGenerationViewModelBase.cs | 21 +++++++++++++++++-- .../InferenceTextToImageViewModel.cs | 3 +-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs index b1a7ef6fd..498a3549d 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -17,6 +17,7 @@ using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Inference; @@ -39,6 +40,7 @@ public abstract partial class InferenceGenerationViewModelBase private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly INotificationService notificationService; + private readonly ServiceManager vmFactory; [JsonPropertyName("ImageGallery")] public ImageGalleryCardViewModel ImageGalleryCardViewModel { get; } @@ -60,6 +62,7 @@ INotificationService notificationService ) { this.notificationService = notificationService; + this.vmFactory = vmFactory; ClientManager = inferenceClientManager; @@ -273,10 +276,10 @@ private async Task GenerateImage( CancellationToken cancellationToken = default ) { + var overrides = GenerateOverrides.FromFlags(options); + try { - var overrides = GenerateOverrides.FromFlags(options); - await GenerateImageImpl(overrides, cancellationToken); } catch (OperationCanceledException) @@ -285,6 +288,20 @@ private async Task GenerateImage( } } + /// + /// Shows a prompt and return false if client not connected + /// + protected async Task CheckClientConnectedWithPrompt() + { + if (ClientManager.IsConnected) + return true; + + var vm = vmFactory.Get(); + await vm.CreateDialog().ShowAsync(); + + return ClientManager.IsConnected; + } + /// /// Handles the preview image received event from the websocket. /// Updates the preview image in the image gallery. diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index e09a75093..249aa449d 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -286,9 +286,8 @@ CancellationToken cancellationToken return; } - if (!ClientManager.IsConnected) + if (!await CheckClientConnectedWithPrompt() || !ClientManager.IsConnected) { - notificationService.Show("Client not connected", "Please connect first"); return; } From 5da30625bf4f1c1cf94467a89fff40405b13c549 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 24 Sep 2023 17:30:55 -0400 Subject: [PATCH 365/474] Remove 2 column gallery limit --- StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml | 1 - 1 file changed, 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml index 9ee42e3ca..1bcf40198 100644 --- a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml @@ -116,7 +116,6 @@ From 7d8516ac0d929b859b5b305140620e75fe024853 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 24 Sep 2023 17:31:20 -0400 Subject: [PATCH 366/474] Add dark border style --- StabilityMatrix.Avalonia/App.axaml | 1 + 1 file changed, 1 insertion(+) diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml index 018287999..8488c76fb 100644 --- a/StabilityMatrix.Avalonia/App.axaml +++ b/StabilityMatrix.Avalonia/App.axaml @@ -42,6 +42,7 @@ + From 7b06ff68130e7ac3e344c8a7b21dd11f3e39e7bc Mon Sep 17 00:00:00 2001 From: JT Date: Sun, 24 Sep 2023 19:47:56 -0700 Subject: [PATCH 367/474] Added Favorite button to model browser cards, moved the Open in CivitAI link to the three-dots menu & fixed Japanese translation display issue --- CHANGELOG.md | 3 + .../CheckpointBrowserCardViewModel.cs | 21 ++++++- .../ViewModels/CheckpointBrowserViewModel.cs | 13 ++-- .../Views/CheckpointBrowserPage.axaml | 61 +++++++++++++++---- .../Views/LaunchPageView.axaml | 4 +- .../Models/Api/CivitSortMode.cs | 6 ++ .../Models/Settings/Settings.cs | 2 + 7 files changed, 91 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d85a14275..b629c48a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,15 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 ## v2.5.0 ### Added - Added option to change the Shared Folder method for packages using the three-dots menu on the Packages page +- Added the ability to Favorite models in the Model Browser +- Added "Favorites" sort option to the Model Browser ### Changed - Model Browser page size is now 20 instead of 14 ### Fixed - Fixed [#141](https://github.com/LykosAI/StabilityMatrix/issues/141) - Search not working when sorting by Installed on Model Browser - Fixed SD.Next not showing "Open Web UI" button when finished loading - Fixed model index startup errors when `./Models` contains unknown custom folder names +- Fixed ストップ button being cut off in Japanese translation ## v2.4.6 ### Added diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs index 6905cc071..a4785ae63 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs @@ -44,10 +44,12 @@ public CivitModel CivitModel set { civitModel = value; + IsFavorite = settingsManager.Settings.FavoriteModels.Contains(value.Id); UpdateImage(); CheckIfInstalled(); } } + private CivitModel civitModel; public override bool IsTextVisible => Value > 0; @@ -62,7 +64,9 @@ public CivitModel CivitModel [ObservableProperty] private bool showUpdateCard; - private CivitModel civitModel; + + [ObservableProperty] + private bool isFavorite; public CheckpointBrowserCardViewModel( IDownloadService downloadService, @@ -167,6 +171,21 @@ private void OpenModel() ProcessRunner.OpenUrl($"https://civitai.com/models/{CivitModel.Id}"); } + [RelayCommand] + private void ToggleFavorite() + { + if (settingsManager.Settings.FavoriteModels.Contains(CivitModel.Id)) + { + settingsManager.Transaction(s => s.FavoriteModels.Remove(CivitModel.Id)); + } + else + { + settingsManager.Transaction(s => s.FavoriteModels.Add(CivitModel.Id)); + } + + IsFavorite = settingsManager.Settings.FavoriteModels.Contains(CivitModel.Id); + } + [RelayCommand] private async Task Import(CivitModel model) { diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs index c9a24bfbf..60e4b2ea2 100644 --- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs @@ -441,11 +441,6 @@ private async Task SearchModels() .GetAllCheckpointFiles(settingsManager.ModelsDirectory) .Where(c => c.IsConnectedModel); - if (SelectedModelType != CivitModelType.All) - { - connectedModels = connectedModels.Where(c => c.ModelType == SelectedModelType); - } - modelRequest.CommaSeparatedModelIds = string.Join( ",", connectedModels @@ -456,6 +451,14 @@ private async Task SearchModels() modelRequest.Sort = null; modelRequest.Period = null; } + else if (SortMode == CivitSortMode.Favorites) + { + var favoriteModels = settingsManager.Settings.FavoriteModels; + + modelRequest.CommaSeparatedModelIds = string.Join(",", favoriteModels); + modelRequest.Sort = null; + modelRequest.Period = null; + } // See if query is cached var cachedQuery = await liteDbContext.CivitModelQueryCache diff --git a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml index ec5c557ee..7ad45f0c7 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml @@ -26,11 +26,36 @@ Width="330"> - + + + + + + - - - + - + + + + Margin="0,8,0,0" /> - @@ -94,15 +104,27 @@ VerticalAlignment="Center" /> - internal static string? FormatChangelog(string markdown, SemVersion currentVersion) { - var pattern = $@"(##[\s\S]+?)(?:## v{currentVersion.WithoutPrereleaseOrMetadata()})"; + var pattern = RegexChangelog(); + + var results = pattern + .Matches(markdown) + .Select( + m => + new + { + Block = m.Groups[1].Value.Trim(), + Version = m.Groups[2].Value.Trim(), + Content = m.Groups[3].Value.Trim() + } + ) + .ToList(); + + // Join all blocks until and excluding the current version + // If we're on a pre-release, include the current release + + var currentVersionBlock = results.FindIndex( + x => x.Version == $"v{currentVersion.WithoutPrereleaseOrMetadata()}" + ); + + if (currentVersionBlock == -1) + { + return null; + } - var match = Regex.Match(markdown, pattern); + var blocks = results + .Take(currentVersionBlock + (currentVersion.IsPrerelease ? 1 : 0)) + .Select(x => x.Block) + .ToList(); - return match.Success ? match.Groups[1].Value.TrimEnd() : null; + return string.Join(Environment.NewLine + Environment.NewLine, blocks); } public async Task Preload() @@ -99,7 +133,7 @@ public async Task Preload() if (UpdateInfo.ChangelogUrl.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) { ReleaseNotes = - FormatChangelog(changelog, UpdateInfo.Version) + FormatChangelog(changelog, Compat.AppVersion) ?? "## Unable to format release notes"; } } diff --git a/StabilityMatrix.Tests/Avalonia/UpdateViewModelTests.cs b/StabilityMatrix.Tests/Avalonia/UpdateViewModelTests.cs index 93a05de33..7c5bb4e02 100644 --- a/StabilityMatrix.Tests/Avalonia/UpdateViewModelTests.cs +++ b/StabilityMatrix.Tests/Avalonia/UpdateViewModelTests.cs @@ -50,6 +50,19 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 """; Assert.AreEqual(expected, result); - Assert.AreEqual(expected, resultPre); + + // Pre-release should include the current release + const string expectedPre = """ + ## v2.4.6 + ### Added + - Stuff + ### Changed + - Things + + ## v2.4.5 + ### Fixed + - Fixed bug + """; + Assert.AreEqual(expectedPre, resultPre); } } From d6765c7643e6cf596c08b5808c2433fcc77ad10c Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 27 Sep 2023 17:33:51 -0400 Subject: [PATCH 403/474] Fix design data for updateviewmodel --- StabilityMatrix.Avalonia/DesignData/DesignData.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index eac6cf7ad..1f538779d 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -306,8 +306,8 @@ public static void Initialize() ); UpdateViewModel = Services.GetRequiredService(); - UpdateViewModel.UpdateText = - $"Stability Matrix v2.0.1 is now available! You currently have v2.0.0. Would you like to update now?"; + UpdateViewModel.CurrentVersionText = "v2.0.0"; + UpdateViewModel.NewVersionText = "v2.0.1"; UpdateViewModel.ReleaseNotes = "## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature"; From 2ca5443391e6ffa892ec3b86218ef15c0100cb26 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 27 Sep 2023 17:34:15 -0400 Subject: [PATCH 404/474] Use async void for update dialog showing --- StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs index 368b4ba53..043009e6d 100644 --- a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs @@ -321,14 +321,15 @@ private void FooterDownloadItem_OnTapped(object? sender, TappedEventArgs e) progressFlyout = flyout; } - private void FooterUpdateItem_OnTapped(object? sender, TappedEventArgs e) + private async void FooterUpdateItem_OnTapped(object? sender, TappedEventArgs e) { // show update window thing if (DataContext is not MainWindowViewModel vm) { throw new NullReferenceException("DataContext is not MainWindowViewModel"); } - Dispatcher.UIThread.InvokeAsync(vm.ShowUpdateDialog).SafeFireAndForget(); + + await vm.ShowUpdateDialog(); } private void FooterDiscordItem_OnTapped(object? sender, TappedEventArgs e) From 2297e4d59828154bd7587936aa4bc161a929d0e2 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 27 Sep 2023 17:43:56 -0400 Subject: [PATCH 405/474] Create update temp folder as hidden --- StabilityMatrix.Core/Updater/UpdateHelper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Core/Updater/UpdateHelper.cs b/StabilityMatrix.Core/Updater/UpdateHelper.cs index 97cb25a5d..401736c1d 100644 --- a/StabilityMatrix.Core/Updater/UpdateHelper.cs +++ b/StabilityMatrix.Core/Updater/UpdateHelper.cs @@ -57,7 +57,8 @@ public async Task DownloadUpdate(UpdateInfo updateInfo, IProgress Date: Wed, 27 Sep 2023 17:49:42 -0400 Subject: [PATCH 406/474] Update localization resources --- .../Languages/Resources.ja-JP.resx | 2 +- .../Languages/Resources.zh-Hans.resx | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx index 3b92f7615..86dcb5b7e 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx @@ -311,7 +311,7 @@ Stability Matrixがバージョンアップ! - 直近のインポート + 最新版DL すべてのバージョン diff --git a/StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx b/StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx index c88f3805e..99c38fb7e 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx @@ -172,7 +172,7 @@ 强调 - 轻描淡写 + 淡化 I am not sure about this, need to know what background are we talking about @@ -334,10 +334,10 @@ 基础模型 - 显示 NSFW 内容 + 显示NSFW内容 - 数据由 CivitAI 提供 + 数据由CivitAI提供 页次 @@ -361,7 +361,7 @@ 删除 - 在 CivitAI上打开 + 在CivitAI上打开 已连接CivitAI元数据 @@ -426,7 +426,7 @@ 关闭时删除共享模型目录的符号链接 - 如果您在将 Stability Matrix 移至另一个驱动器时遇到问题,请选择此选项 + 如果您在将Stability Matrix移至另一个驱动器时遇到问题,请选择此选项 重置模型缓存 @@ -466,7 +466,7 @@ 使用当前应用程序的位置,如果应用程序移动了,可以再次运行此功能 - 仅适用于 Windows + 仅适用于Windows 为当前用户添加 @@ -554,7 +554,7 @@ 在资源管理器中打开 - 在 Finder中打开 + 在Finder中打开 卸载 From d78e7bf9f54a70cea3a1b8ea032a25de8054d63c Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 27 Sep 2023 17:54:12 -0400 Subject: [PATCH 407/474] Use filled icon for nav item update button --- StabilityMatrix.Avalonia/Views/MainWindow.axaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Avalonia/Views/MainWindow.axaml b/StabilityMatrix.Avalonia/Views/MainWindow.axaml index 7248b7793..fb139dc78 100644 --- a/StabilityMatrix.Avalonia/Views/MainWindow.axaml +++ b/StabilityMatrix.Avalonia/Views/MainWindow.axaml @@ -9,6 +9,7 @@ xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:base="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Base" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" + xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="550" d:DataContext="{x:Static mocks:DesignData.MainWindowViewModel}" x:DataType="vm:MainWindowViewModel" @@ -38,7 +39,6 @@ - + - + From 898426a2292eba88ce42b938479c93d5cdf7f8c8 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 27 Sep 2023 18:14:24 -0400 Subject: [PATCH 408/474] Wrap exception details --- StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml index d768e0ab6..2612d4db7 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/ExceptionDialog.axaml @@ -38,7 +38,9 @@ - From 06b3d65341dbd966b5f5edbe7fab14750d242e06 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 27 Sep 2023 18:28:02 -0400 Subject: [PATCH 409/474] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a20d5d84..ea2bfab8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,10 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Added option to change the Shared Folder method for packages using the three-dots menu on the Packages page - Added the ability to Favorite models in the Model Browser - Added "Favorites" sort option to the Model Browser +- Added notification flyout for new available updates. Dismiss to hide until the next update version. ### Changed - Model Browser page size is now 20 instead of 14 +- Update changelog now only shows the difference between the current version and the latest version ### Fixed - Fixed [#141](https://github.com/LykosAI/StabilityMatrix/issues/141) - Search not working when sorting by Installed on Model Browser - Fixed SD.Next not showing "Open Web UI" button when finished loading From 863c9440a130e1287190dcfd7a2f3f4f9b3dd138 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 27 Sep 2023 20:52:28 -0400 Subject: [PATCH 410/474] Show app startup time in logs --- StabilityMatrix.Avalonia/Program.cs | 4 ++++ StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs | 5 +++++ StabilityMatrix.Core/Helper/CodeTimer.cs | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Program.cs b/StabilityMatrix.Avalonia/Program.cs index 11f001c5a..b8c0cd007 100644 --- a/StabilityMatrix.Avalonia/Program.cs +++ b/StabilityMatrix.Avalonia/Program.cs @@ -38,12 +38,16 @@ public class Program public static bool IsDebugBuild { get; private set; } + public static Stopwatch StartupTimer { get; } = new(); + // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. [STAThread] public static void Main(string[] args) { + StartupTimer.Start(); + Args.DebugExceptionDialog = args.Contains("--debug-exception-dialog"); Args.DebugSentry = args.Contains("--debug-sentry"); Args.DebugOneClickInstall = args.Contains("--debug-one-click-install"); diff --git a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs index 431d4f535..1abe5f17c 100644 --- a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs @@ -103,6 +103,11 @@ public override async Task OnLoadedAsync() Task.Run(() => settingsManager.IndexCheckpoints()).SafeFireAndForget(); PreloadPages(); + + Program.StartupTimer.Stop(); + var startupTime = CodeTimer.FormatTime(Program.StartupTimer.Elapsed); + Logger.Info($"App started ({startupTime})"); + if (Program.Args.DebugOneClickInstall || !settingsManager.Settings.InstalledPackages.Any()) { var viewModel = dialogFactory.Get(); diff --git a/StabilityMatrix.Core/Helper/CodeTimer.cs b/StabilityMatrix.Core/Helper/CodeTimer.cs index 702bc98a6..f881c4c57 100644 --- a/StabilityMatrix.Core/Helper/CodeTimer.cs +++ b/StabilityMatrix.Core/Helper/CodeTimer.cs @@ -52,7 +52,7 @@ public static IDisposable StartDebug( /// /// Formats a TimeSpan into a string. Chooses the most appropriate unit of time. /// - private static string FormatTime(TimeSpan duration) + public static string FormatTime(TimeSpan duration) { if (duration.TotalSeconds < 1) { From a62c093bcefa6c0f1596a9f194a163f7ab17a83f Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 28 Sep 2023 01:38:15 -0400 Subject: [PATCH 411/474] Add metadata parsing from FilePath --- StabilityMatrix.Core/Helper/ImageMetadata.cs | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/StabilityMatrix.Core/Helper/ImageMetadata.cs b/StabilityMatrix.Core/Helper/ImageMetadata.cs index 78804fb09..b3b6b2399 100644 --- a/StabilityMatrix.Core/Helper/ImageMetadata.cs +++ b/StabilityMatrix.Core/Helper/ImageMetadata.cs @@ -65,6 +65,29 @@ public static System.Drawing.Size GetImageSize(BinaryReader reader) return new System.Drawing.Size(imageWidth, imageHeight); } + public static ( + string? Parameters, + string? ParametersJson, + string? SMProject, + string? ComfyNodes + ) GetAllFileMetadata(FilePath filePath) + { + using var stream = filePath.Info.OpenRead(); + using var reader = new BinaryReader(stream); + + var parameters = ReadTextChunk(reader, "parameters"); + var parametersJson = ReadTextChunk(reader, "parameters-json"); + var smProject = ReadTextChunk(reader, "smproj"); + var comfyNodes = ReadTextChunk(reader, "prompt"); + + return ( + string.IsNullOrEmpty(parameters) ? null : parameters, + string.IsNullOrEmpty(parametersJson) ? null : parametersJson, + string.IsNullOrEmpty(smProject) ? null : smProject, + string.IsNullOrEmpty(comfyNodes) ? null : comfyNodes + ); + } + public IEnumerable? GetTextualData() { // Get the PNG-tEXt directory From 598371e14086e136d7959362fa8087bb1f36cbfb Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 28 Sep 2023 16:19:48 -0400 Subject: [PATCH 412/474] Add Sampler Scheduler conversions --- .../Helper/GenerationParametersConverter.cs | 64 +++++++++++++++ .../Models/Api/Comfy/ComfySampler.cs | 81 +++++++++++++------ .../Models/Api/Comfy/ComfySamplerScheduler.cs | 25 ++++++ .../Models/Api/Comfy/ComfyScheduler.cs | 6 +- .../Models/GenerationParameters.cs | 46 ++++++++++- 5 files changed, 196 insertions(+), 26 deletions(-) create mode 100644 StabilityMatrix.Core/Helper/GenerationParametersConverter.cs create mode 100644 StabilityMatrix.Core/Models/Api/Comfy/ComfySamplerScheduler.cs diff --git a/StabilityMatrix.Core/Helper/GenerationParametersConverter.cs b/StabilityMatrix.Core/Helper/GenerationParametersConverter.cs new file mode 100644 index 000000000..1736ddf33 --- /dev/null +++ b/StabilityMatrix.Core/Helper/GenerationParametersConverter.cs @@ -0,0 +1,64 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using StabilityMatrix.Core.Models.Api.Comfy; + +namespace StabilityMatrix.Core.Helper; + +public static class GenerationParametersConverter +{ + private static readonly ImmutableDictionary< + string, + ComfySamplerScheduler + > ParamsToSamplerSchedulers = new Dictionary + { + ["DPM++ 2M Karras"] = (ComfySampler.Dpmpp2M, ComfyScheduler.Karras), + ["DPM++ SDE Karras"] = (ComfySampler.DpmppSde, ComfyScheduler.Karras), + ["DPM++ 2M SDE Exponential"] = (ComfySampler.Dpmpp2MSde, ComfyScheduler.Exponential), + ["DPM++ 2M SDE Karras"] = (ComfySampler.Dpmpp2MSde, ComfyScheduler.Karras), + ["Euler a"] = (ComfySampler.EulerAncestral, ComfyScheduler.Normal), + ["Euler"] = (ComfySampler.Euler, ComfyScheduler.Normal), + ["LMS"] = (ComfySampler.LMS, ComfyScheduler.Normal), + ["Heun"] = (ComfySampler.Heun, ComfyScheduler.Normal), + ["DPM2"] = (ComfySampler.Dpm2, ComfyScheduler.Normal), + ["DPM2 Karras"] = (ComfySampler.Dpm2, ComfyScheduler.Karras), + ["DPM2 a"] = (ComfySampler.Dpm2Ancestral, ComfyScheduler.Normal), + ["DPM2 a Karras"] = (ComfySampler.Dpm2Ancestral, ComfyScheduler.Karras), + ["DPM++ 2S a"] = (ComfySampler.Dpmpp2SAncestral, ComfyScheduler.Normal), + ["DPM++ 2S a Karras"] = (ComfySampler.Dpmpp2SAncestral, ComfyScheduler.Karras), + ["DPM++ 2M"] = (ComfySampler.Dpmpp2M, ComfyScheduler.Normal), + ["DPM++ SDE"] = (ComfySampler.DpmppSde, ComfyScheduler.Normal), + ["DPM++ 2M SDE"] = (ComfySampler.Dpmpp2MSde, ComfyScheduler.Normal), + ["DPM++ 3M SDE"] = (ComfySampler.Dpmpp3MSde, ComfyScheduler.Normal), + ["DPM++ 3M SDE Karras"] = (ComfySampler.Dpmpp3MSde, ComfyScheduler.Karras), + ["DPM++ 3M SDE Exponential"] = (ComfySampler.Dpmpp3MSde, ComfyScheduler.Exponential), + ["DPM fast"] = (ComfySampler.DpmFast, ComfyScheduler.Normal), + ["DPM adaptive"] = (ComfySampler.DpmAdaptive, ComfyScheduler.Normal), + ["LMS Karras"] = (ComfySampler.LMS, ComfyScheduler.Karras), + ["DDIM"] = (ComfySampler.DDIM, ComfyScheduler.Normal), + ["UniPC"] = (ComfySampler.UniPC, ComfyScheduler.Normal), + }.ToImmutableDictionary(); + + private static readonly ImmutableDictionary< + ComfySamplerScheduler, + string + > SamplerSchedulersToParams = ParamsToSamplerSchedulers.ToImmutableDictionary( + x => x.Value, + x => x.Key + ); + + public static bool TryGetSamplerScheduler( + string parameters, + out ComfySamplerScheduler samplerScheduler + ) + { + return ParamsToSamplerSchedulers.TryGetValue(parameters, out samplerScheduler); + } + + public static bool TryGetParameters( + ComfySamplerScheduler samplerScheduler, + [NotNullWhen(true)] out string? parameters + ) + { + return SamplerSchedulersToParams.TryGetValue(samplerScheduler, out parameters); + } +} diff --git a/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs b/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs index d244e3241..d327a195e 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs @@ -1,39 +1,74 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace StabilityMatrix.Core.Models.Api.Comfy; +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public readonly record struct ComfySampler(string Name) { - private static Dictionary ConvertDict { get; } = + public static ComfySampler Euler { get; } = new("euler"); + public static ComfySampler EulerAncestral { get; } = new("euler_ancestral"); + public static ComfySampler Heun { get; } = new("heun"); + public static ComfySampler Dpm2 { get; } = new("dpm_2"); + public static ComfySampler Dpm2Ancestral { get; } = new("dpm_2_ancestral"); + public static ComfySampler LMS { get; } = new("lms"); + public static ComfySampler DpmFast { get; } = new("dpm_fast"); + public static ComfySampler DpmAdaptive { get; } = new("dpm_adaptive"); + public static ComfySampler Dpmpp2SAncestral { get; } = new("dpmpp_2s_ancestral"); + public static ComfySampler DpmppSde { get; } = new("dpmpp_sde"); + public static ComfySampler DpmppSdeGpu { get; } = new("dpmpp_sde_gpu"); + public static ComfySampler Dpmpp2M { get; } = new("dpmpp_2m"); + public static ComfySampler Dpmpp2MSde { get; } = new("dpmpp_2m_sde"); + public static ComfySampler Dpmpp2MSdeGpu { get; } = new("dpmpp_2m_sde_gpu"); + public static ComfySampler Dpmpp3M { get; } = new("dpmpp_3m"); + public static ComfySampler Dpmpp3MSde { get; } = new("dpmpp_3m_sde"); + public static ComfySampler Dpmpp3MSdeGpu { get; } = new("dpmpp_3m_sde_gpu"); + public static ComfySampler DDIM { get; } = new("ddim"); + public static ComfySampler UniPC { get; } = new("uni_pc"); + public static ComfySampler UniPCBh2 { get; } = new("uni_pc_bh2"); + + private static Dictionary ConvertDict { get; } = new() { - ["euler"] = "Euler", - ["euler_ancestral"] = "Euler Ancestral", - ["heun"] = "Heun", - ["dpm_2"] = "DPM 2", - ["dpm_2_ancestral"] = "DPM 2 Ancestral", - ["lms"] = "LMS", - ["dpm_fast"] = "DPM Fast", - ["dpm_adaptive"] = "DPM Adaptive", - ["dpmpp_2s_ancestral"] = "DPM++ 2S Ancestral", - ["dpmpp_sde"] = "DPM++ SDE", - ["dpmpp_sde_gpu"] = "DPM++ SDE GPU", - ["dpmpp_2m"] = "DPM++ 2M", - ["dpmpp_2m_sde"] = "DPM++ 2M SDE", - ["dpmpp_2m_sde_gpu"] = "DPM++ 2M SDE GPU", - ["dpmpp_3m"] = "DPM++ 3M", - ["dpmpp_3m_sde"] = "DPM++ 3M SDE", - ["dpmpp_3m_sde_gpu"] = "DPM++ 3M SDE GPU", - ["ddim"] = "DDIM", - ["uni_pc"] = "UniPC", - ["uni_pc_bh2"] = "UniPC BH2" + [Euler] = "Euler", + [EulerAncestral] = "Euler Ancestral", + [Heun] = "Heun", + [Dpm2] = "DPM 2", + [Dpm2Ancestral] = "DPM 2 Ancestral", + [LMS] = "LMS", + [DpmFast] = "DPM Fast", + [DpmAdaptive] = "DPM Adaptive", + [Dpmpp2SAncestral] = "DPM++ 2S Ancestral", + [DpmppSde] = "DPM++ SDE", + [DpmppSdeGpu] = "DPM++ SDE GPU", + [Dpmpp2M] = "DPM++ 2M", + [Dpmpp2MSde] = "DPM++ 2M SDE", + [Dpmpp2MSdeGpu] = "DPM++ 2M SDE GPU", + [Dpmpp3M] = "DPM++ 3M", + [Dpmpp3MSde] = "DPM++ 3M SDE", + [Dpmpp3MSdeGpu] = "DPM++ 3M SDE GPU", + [DDIM] = "DDIM", + [UniPC] = "UniPC", + [UniPCBh2] = "UniPC BH2" }; public static IReadOnlyList Defaults { get; } = - ConvertDict.Keys.Select(k => new ComfySampler(k)).ToImmutableArray(); + ConvertDict.Keys.ToImmutableArray(); public string DisplayName => - ConvertDict.TryGetValue(Name, out var displayName) ? displayName : Name; + ConvertDict.TryGetValue(this, out var displayName) ? displayName : Name; + + /// + public bool Equals(ComfySampler other) + { + return Name == other.Name; + } + + /// + public override int GetHashCode() + { + return Name.GetHashCode(); + } private sealed class NameEqualityComparer : IEqualityComparer { diff --git a/StabilityMatrix.Core/Models/Api/Comfy/ComfySamplerScheduler.cs b/StabilityMatrix.Core/Models/Api/Comfy/ComfySamplerScheduler.cs new file mode 100644 index 000000000..62b5d6f4a --- /dev/null +++ b/StabilityMatrix.Core/Models/Api/Comfy/ComfySamplerScheduler.cs @@ -0,0 +1,25 @@ +namespace StabilityMatrix.Core.Models.Api.Comfy; + +/// +/// Pair of and +/// +public readonly record struct ComfySamplerScheduler(ComfySampler Sampler, ComfyScheduler Scheduler) +{ + /// + public bool Equals(ComfySamplerScheduler other) + { + return Sampler.Equals(other.Sampler) && Scheduler.Equals(other.Scheduler); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Sampler, Scheduler); + } + + // Implicit conversion from (ComfySampler, ComfyScheduler) + public static implicit operator ComfySamplerScheduler((ComfySampler, ComfyScheduler) tuple) + { + return new ComfySamplerScheduler(tuple.Item1, tuple.Item2); + } +} diff --git a/StabilityMatrix.Core/Models/Api/Comfy/ComfyScheduler.cs b/StabilityMatrix.Core/Models/Api/Comfy/ComfyScheduler.cs index 35f00510d..0085acebf 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/ComfyScheduler.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/ComfyScheduler.cs @@ -4,10 +4,14 @@ namespace StabilityMatrix.Core.Models.Api.Comfy; public readonly record struct ComfyScheduler(string Name) { + public static ComfyScheduler Normal { get; } = new("normal"); + public static ComfyScheduler Karras { get; } = new("karras"); + public static ComfyScheduler Exponential { get; } = new("exponential"); + private static Dictionary ConvertDict { get; } = new() { - ["normal"] = "Normal", + [Normal.Name] = "Normal", ["karras"] = "Karras", ["exponential"] = "Exponential", ["sgm_uniform"] = "SGM Uniform", diff --git a/StabilityMatrix.Core/Models/GenerationParameters.cs b/StabilityMatrix.Core/Models/GenerationParameters.cs index bb36ad2fa..fab9f7933 100644 --- a/StabilityMatrix.Core/Models/GenerationParameters.cs +++ b/StabilityMatrix.Core/Models/GenerationParameters.cs @@ -1,11 +1,12 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using System.Text.RegularExpressions; +using StabilityMatrix.Core.Models.Api.Comfy; namespace StabilityMatrix.Core.Models; [JsonSerializable(typeof(GenerationParameters))] -public partial class GenerationParameters +public partial record GenerationParameters { public string? PositivePrompt { get; set; } public string? NegativePrompt { get; set; } @@ -78,6 +79,8 @@ public static bool TryParse( Sampler = match.Groups["Sampler"].Value, CfgScale = double.Parse(match.Groups["CfgScale"].Value), Seed = ulong.Parse(match.Groups["Seed"].Value), + Height = int.Parse(match.Groups["Height"].Value), + Width = int.Parse(match.Groups["Width"].Value), ModelHash = match.Groups["ModelHash"].Value, ModelName = match.Groups["ModelName"].Value, }; @@ -85,8 +88,47 @@ public static bool TryParse( return true; } + /// + /// Converts current string to and . + /// + /// + public (ComfySampler sampler, ComfyScheduler scheduler)? GetComfySamplers() + { + if (Sampler is not { } source) + return null; + + var scheduler = source switch + { + _ when source.Contains("Karras") => ComfyScheduler.Karras, + _ when source.Contains("Exponential") => ComfyScheduler.Exponential, + _ => ComfyScheduler.Normal, + }; + + var sampler = source switch + { + "LMS" => ComfySampler.LMS, + "DDIM" => ComfySampler.DDIM, + "UniPC" => ComfySampler.UniPC, + "DPM fast" => ComfySampler.DpmFast, + "DPM adaptive" => ComfySampler.DpmAdaptive, + "Heun" => ComfySampler.Heun, + _ when source.StartsWith("DPM2 a") => ComfySampler.Dpm2Ancestral, + _ when source.StartsWith("DPM2") => ComfySampler.Dpm2, + _ when source.StartsWith("DPM++ 2M SDE") => ComfySampler.Dpmpp2MSde, + _ when source.StartsWith("DPM++ 2M") => ComfySampler.Dpmpp2M, + _ when source.StartsWith("DPM++ 3M SDE") => ComfySampler.Dpmpp3MSde, + _ when source.StartsWith("DPM++ 3M") => ComfySampler.Dpmpp3M, + _ when source.StartsWith("DPM++ SDE") => ComfySampler.DpmppSde, + _ when source.StartsWith("DPM++ 2S a") => ComfySampler.Dpmpp2SAncestral, + _ => default + }; + + return (sampler, scheduler); + } + + // 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: (?.+)$""" + """^Steps: (?\d+), Sampler: (?.+?), CFG scale: (?\d+(\.\d+)?), Seed: (?\d+), Size: (?\d+)x(?\d+), Model hash: (?.+?), Model: (?.+)$""" )] private static partial Regex ParseLastLineRegex(); } From e2e85c6f579f70f0911a187908406751938ec88e Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 28 Sep 2023 16:20:30 -0400 Subject: [PATCH 413/474] Add save load support for files with GenerationParameters --- .../Models/IParametersLoadableState.cs | 15 ++++ .../Base/InferenceTabViewModelBase.cs | 76 ++++++++++++++++++- .../InferenceTextToImageViewModel.cs | 28 ++++++- .../Inference/ModelCardViewModel.cs | 49 +++++++++++- .../Inference/PromptCardViewModel.cs | 20 ++++- .../Inference/SamplerCardViewModel.cs | 63 +++++++-------- 6 files changed, 211 insertions(+), 40 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Models/IParametersLoadableState.cs diff --git a/StabilityMatrix.Avalonia/Models/IParametersLoadableState.cs b/StabilityMatrix.Avalonia/Models/IParametersLoadableState.cs new file mode 100644 index 000000000..c06d88d2b --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/IParametersLoadableState.cs @@ -0,0 +1,15 @@ +using StabilityMatrix.Core.Models; + +namespace StabilityMatrix.Avalonia.Models; + +public interface IParametersLoadableState +{ + void LoadStateFromParameters(GenerationParameters parameters); + + GenerationParameters SaveStateToParameters(GenerationParameters parameters); + + public GenerationParameters SaveStateToParameters() + { + return SaveStateToParameters(new GenerationParameters()); + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs index 1ed26e399..b1586022a 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -6,6 +7,7 @@ using AsyncAwaitBestPractices; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -13,6 +15,8 @@ using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.ViewModels.Inference; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; @@ -144,6 +148,66 @@ public void Dispose() GC.SuppressFinalize(this); } + private bool TryLoadImageMetadata(FilePath? filePath) + { + if (filePath is not { Exists: true }) + return false; + + var metadata = ImageMetadata.GetAllFileMetadata(filePath); + + // Has SMProject metadata + if (metadata.SMProject is not null) + { + var project = JsonSerializer.Deserialize(metadata.SMProject); + + // Check project type matches + if (project?.ProjectType.ToViewModelType() == GetType() && project.State is not null) + { + LoadStateFromJsonObject(project.State); + } + else + { + return false; + } + + // Load image + if (this is IImageGalleryComponent imageGalleryComponent) + { + imageGalleryComponent.LoadImagesToGallery(new ImageSource(filePath)); + } + + return true; + } + + // Has generic metadata + if (metadata.Parameters is { } parametersString) + { + if (!GenerationParameters.TryParse(parametersString, out var parameters)) + { + return false; + } + + if (this is IParametersLoadableState paramsLoadableVm) + { + paramsLoadableVm.LoadStateFromParameters(parameters); + } + else + { + return false; + } + + // Load image + if (this is IImageGalleryComponent imageGalleryComponent) + { + imageGalleryComponent.LoadImagesToGallery(new ImageSource(filePath)); + } + + return true; + } + + return false; + } + /// public void DragOver(object? sender, DragEventArgs e) { @@ -162,10 +226,10 @@ public void DragOver(object? sender, DragEventArgs e) if (e.Data.GetDataFormats().Contains(DataFormats.Files)) { e.Handled = true; - e.DragEffects = DragDropEffects.None; return; } + // Other kinds - not supported e.DragEffects = DragDropEffects.None; } @@ -214,6 +278,16 @@ public void Drop(object? sender, DragEventArgs e) if (e.Data.GetDataFormats().Contains(DataFormats.Files)) { e.Handled = true; + + if (e.Data.Get(DataFormats.Files) is IEnumerable files) + { + var paths = files.Select(f => f.TryGetLocalPath()).ToList(); + + if (paths.FirstOrDefault() is { } file) + { + Dispatcher.UIThread.Post(() => TryLoadImageMetadata(file)); + } + } } } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 249aa449d..2606a1f4b 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -25,7 +25,9 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(InferenceTextToImageView), persistent: true)] -public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase +public class InferenceTextToImageViewModel + : InferenceGenerationViewModelBase, + IParametersLoadableState { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); @@ -327,4 +329,28 @@ CancellationToken cancellationToken await RunGeneration(generationArgs, cancellationToken); } + + /// + public void LoadStateFromParameters(GenerationParameters parameters) + { + PromptCardViewModel.LoadStateFromParameters(parameters); + SamplerCardViewModel.LoadStateFromParameters(parameters); + + SeedCardViewModel.Seed = Convert.ToInt64(parameters.Seed); + + ModelCardViewModel.LoadStateFromParameters(parameters); + } + + /// + public GenerationParameters SaveStateToParameters(GenerationParameters parameters) + { + parameters = PromptCardViewModel.SaveStateToParameters(parameters); + parameters = SamplerCardViewModel.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 d46c999b7..2c28da8fc 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs @@ -1,7 +1,9 @@ -using System.Linq; +using System; +using System.Linq; using System.Text.Json.Nodes; using CommunityToolkit.Mvvm.ComponentModel; using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; @@ -10,7 +12,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(ModelCard))] -public partial class ModelCardViewModel : LoadableViewModelBase +public partial class ModelCardViewModel : LoadableViewModelBase, IParametersLoadableState { [ObservableProperty] private HybridModelFile? selectedModel; @@ -71,4 +73,47 @@ internal class ModelCardModel public string? SelectedVaeName { get; init; } public bool IsVaeSelectionEnabled { get; init; } } + + /// + public void LoadStateFromParameters(GenerationParameters parameters) + { + if (parameters.ModelName is not { } paramsModelName) + return; + + var currentModels = ClientManager.Models; + + HybridModelFile? model; + + // First try hash match + if (parameters.ModelHash is not null) + { + model = currentModels.FirstOrDefault( + m => + m.Local?.ConnectedModelInfo?.Hashes.SHA256 is { } sha256 + && sha256.StartsWith( + parameters.ModelHash, + StringComparison.InvariantCultureIgnoreCase + ) + ); + } + else + { + // Name matches + model = currentModels.FirstOrDefault(m => m.FileName.EndsWith(paramsModelName)); + model ??= currentModels.FirstOrDefault( + m => m.ShortDisplayName.StartsWith(paramsModelName) + ); + } + + if (model is not null) + { + SelectedModel = model; + } + } + + /// + public GenerationParameters SaveStateToParameters(GenerationParameters parameters) + { + return parameters with { ModelName = SelectedModel?.FileName }; + } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs index f4f65f913..a7b76ad0d 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs @@ -22,12 +22,13 @@ using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Services; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(PromptCard))] -public partial class PromptCardViewModel : LoadableViewModelBase +public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoadableState { private readonly IModelIndexService modelIndexService; @@ -284,4 +285,21 @@ public override void LoadStateFromJsonObject(JsonObject state) PromptDocument.Text = model.Prompt ?? ""; NegativePromptDocument.Text = model.NegativePrompt ?? ""; } + + /// + public void LoadStateFromParameters(GenerationParameters parameters) + { + PromptDocument.Text = parameters.PositivePrompt ?? ""; + NegativePromptDocument.Text = parameters.NegativePrompt ?? ""; + } + + /// + public GenerationParameters SaveStateToParameters(GenerationParameters parameters) + { + return parameters with + { + PositivePrompt = PromptDocument.Text, + NegativePrompt = NegativePromptDocument.Text + }; + } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs index 17448c131..1200e013e 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs @@ -1,15 +1,18 @@ -using System.Text.Json.Serialization; +using System.Linq; +using System.Text.Json.Serialization; using CommunityToolkit.Mvvm.ComponentModel; using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; namespace StabilityMatrix.Avalonia.ViewModels.Inference; [View(typeof(SamplerCard))] -public partial class SamplerCardViewModel : LoadableViewModelBase +public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLoadableState { [ObservableProperty] private bool isRefinerStepsEnabled; @@ -61,42 +64,32 @@ public SamplerCardViewModel(IInferenceClientManager clientManager) ClientManager = clientManager; } - /*/// - public override void LoadStateFromJsonObject(JsonObject state) + /// + public void LoadStateFromParameters(GenerationParameters parameters) { - var model = DeserializeModel(state); - - Steps = model.Steps; - IsDenoiseStrengthEnabled = model.IsDenoiseStrengthEnabled; - DenoiseStrength = model.DenoiseStrength; - IsCfgScaleEnabled = model.IsCfgScaleEnabled; - CfgScale = model.CfgScale; - IsDimensionsEnabled = model.IsDimensionsEnabled; - Width = model.Width; - Height = model.Height; - IsSamplerSelectionEnabled = model.IsSamplerSelectionEnabled; - SelectedSampler = model.SelectedSampler is null - ? null - : new ComfySampler(model.SelectedSampler); + Width = parameters.Width; + Height = parameters.Height; + Steps = parameters.Steps; + CfgScale = parameters.CfgScale; + + if (parameters.GetComfySamplers() is { } paramSamplers) + { + var (sampler, scheduler) = paramSamplers; + + SelectedSampler = ClientManager.Samplers.FirstOrDefault(s => s.Name == sampler.Name); + } } /// - public override JsonObject SaveStateToJsonObject() + public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { - return SerializeModel( - new SamplerCardModel - { - Steps = Steps, - IsDenoiseStrengthEnabled = IsDenoiseStrengthEnabled, - DenoiseStrength = DenoiseStrength, - IsCfgScaleEnabled = IsCfgScaleEnabled, - CfgScale = CfgScale, - IsDimensionsEnabled = IsDimensionsEnabled, - Width = Width, - Height = Height, - IsSamplerSelectionEnabled = IsSamplerSelectionEnabled, - SelectedSampler = SelectedSampler?.Name - } - ); - }*/ + return parameters with + { + Width = Width, + Height = Height, + Steps = Steps, + CfgScale = CfgScale, + Sampler = SelectedSampler?.Name + }; + } } From 962231609010680605101671283fa5c38ab5f425 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 28 Sep 2023 17:15:10 -0400 Subject: [PATCH 414/474] Add to abbreviation dictionary --- StabilityMatrix.sln.DotSettings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/StabilityMatrix.sln.DotSettings b/StabilityMatrix.sln.DotSettings index 12f44a914..d943853a9 100644 --- a/StabilityMatrix.sln.DotSettings +++ b/StabilityMatrix.sln.DotSettings @@ -1,8 +1,11 @@  AI + DDIM EOF ESRGAN + LMS LRU + PC <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> True True From 7ca106f87ca490d611981bc28e31be70338a9eca Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 28 Sep 2023 17:15:24 -0400 Subject: [PATCH 415/474] Check stream length to fix value errors --- StabilityMatrix.Core/Helper/ImageMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Core/Helper/ImageMetadata.cs b/StabilityMatrix.Core/Helper/ImageMetadata.cs index b3b6b2399..73557993f 100644 --- a/StabilityMatrix.Core/Helper/ImageMetadata.cs +++ b/StabilityMatrix.Core/Helper/ImageMetadata.cs @@ -141,7 +141,7 @@ public static string ReadTextChunk(BinaryReader byteStream, string key) { // skip to end of png header stuff byteStream.BaseStream.Position = 0x21; - while (byteStream.BaseStream.Position < byteStream.BaseStream.Length) + while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4) { var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).Reverse().ToArray()); var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4)); From 8274addb92715311ae96b1274a6e6dac5ac7be84 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 28 Sep 2023 17:15:44 -0400 Subject: [PATCH 416/474] Error handling for file metadata parsing --- .../Base/InferenceGenerationViewModelBase.cs | 1 + .../Base/InferenceTabViewModelBase.cs | 130 ++++++++++++------ .../Helper/GenerationParametersConverter.cs | 6 + 3 files changed, 98 insertions(+), 39 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs index 498a3549d..c17bde51f 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -60,6 +60,7 @@ protected InferenceGenerationViewModelBase( IInferenceClientManager inferenceClientManager, INotificationService notificationService ) + : base(notificationService) { this.notificationService = notificationService; this.vmFactory = vmFactory; diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs index b1586022a..8f0d074a7 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs @@ -1,19 +1,23 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using AsyncAwaitBestPractices; using Avalonia.Controls; +using Avalonia.Controls.Notifications; using Avalonia.Input; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; +using NLog; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; +using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; @@ -30,6 +34,10 @@ public abstract partial class InferenceTabViewModelBase IPersistentViewProvider, IDropTarget { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private readonly INotificationService notificationService; + /// /// The title of the tab /// @@ -103,6 +111,11 @@ protected async Task SaveViewState() #endregion + protected InferenceTabViewModelBase(INotificationService notificationService) + { + this.notificationService = notificationService; + } + [RelayCommand] private async Task DebugSaveViewState() { @@ -148,10 +161,20 @@ public void Dispose() GC.SuppressFinalize(this); } - private bool TryLoadImageMetadata(FilePath? filePath) + /// + /// Loads image and metadata from a file path + /// + /// This is safe to call from non-UI threads + /// File path + /// + /// + /// + private void LoadImageMetadata(FilePath? filePath) { if (filePath is not { Exists: true }) - return false; + { + throw new FileNotFoundException("File does not exist", filePath?.FullPath); + } var metadata = ImageMetadata.GetAllFileMetadata(filePath); @@ -163,11 +186,11 @@ private bool TryLoadImageMetadata(FilePath? filePath) // Check project type matches if (project?.ProjectType.ToViewModelType() == GetType() && project.State is not null) { - LoadStateFromJsonObject(project.State); + Dispatcher.UIThread.Invoke(() => LoadStateFromJsonObject(project.State)); } else { - return false; + throw new ApplicationException("Unsupported project type"); } // Load image @@ -175,37 +198,40 @@ private bool TryLoadImageMetadata(FilePath? filePath) { imageGalleryComponent.LoadImagesToGallery(new ImageSource(filePath)); } - - return true; } - // Has generic metadata - if (metadata.Parameters is { } parametersString) + else if (metadata.Parameters is { } parametersString) { if (!GenerationParameters.TryParse(parametersString, out var parameters)) { - return false; + throw new ApplicationException("Failed to parse parameters"); } if (this is IParametersLoadableState paramsLoadableVm) { - paramsLoadableVm.LoadStateFromParameters(parameters); + Dispatcher.UIThread.Invoke( + () => paramsLoadableVm.LoadStateFromParameters(parameters) + ); } else { - return false; + throw new InvalidOperationException( + "Load parameters target does not implement IParametersLoadableState" + ); } // Load image if (this is IImageGalleryComponent imageGalleryComponent) { - imageGalleryComponent.LoadImagesToGallery(new ImageSource(filePath)); + Dispatcher.UIThread.Invoke( + () => imageGalleryComponent.LoadImagesToGallery(new ImageSource(filePath)) + ); } - - return true; } - - return false; + else + { + throw new ApplicationException("File does not contain any metadata"); + } } /// @@ -245,30 +271,42 @@ public void Drop(object? sender, DragEventArgs e) Dispatcher.UIThread.Post(() => { - var metadata = imageFile.ReadMetadata(); - if (metadata.SMProject is not null) + try { - var project = JsonSerializer.Deserialize( - metadata.SMProject - ); - - // Check project type matches - if ( - project?.ProjectType.ToViewModelType() == GetType() - && project.State is not null - ) - { - LoadStateFromJsonObject(project.State); - } - - // Load image - if (this is IImageGalleryComponent imageGalleryComponent) + var metadata = imageFile.ReadMetadata(); + if (metadata.SMProject is not null) { - imageGalleryComponent.LoadImagesToGallery( - new ImageSource(imageFile.GlobalFullPath) + var project = JsonSerializer.Deserialize( + metadata.SMProject ); + + // Check project type matches + if ( + project?.ProjectType.ToViewModelType() == GetType() + && project.State is not null + ) + { + LoadStateFromJsonObject(project.State); + } + + // Load image + if (this is IImageGalleryComponent imageGalleryComponent) + { + imageGalleryComponent.LoadImagesToGallery( + new ImageSource(imageFile.GlobalFullPath) + ); + } } } + catch (Exception ex) + { + Logger.Warn(ex, "Failed to load image from context drop"); + notificationService.ShowPersistent( + $"Could not parse image metadata", + $"{imageFile.FileName} - {ex.Message}", + NotificationType.Warning + ); + } }); return; @@ -281,11 +319,25 @@ public void Drop(object? sender, DragEventArgs e) if (e.Data.Get(DataFormats.Files) is IEnumerable files) { - var paths = files.Select(f => f.TryGetLocalPath()).ToList(); - - if (paths.FirstOrDefault() is { } file) + if (files.Select(f => f.TryGetLocalPath()).FirstOrDefault() is { } path) { - Dispatcher.UIThread.Post(() => TryLoadImageMetadata(file)); + var file = new FilePath(path); + Dispatcher.UIThread.Post(() => + { + try + { + LoadImageMetadata(file); + } + catch (Exception ex) + { + Logger.Warn(ex, "Failed to load image from OS file drop"); + notificationService.ShowPersistent( + $"Could not parse image metadata", + $"{file.Name} - {ex.Message}", + NotificationType.Warning + ); + } + }); } } } diff --git a/StabilityMatrix.Core/Helper/GenerationParametersConverter.cs b/StabilityMatrix.Core/Helper/GenerationParametersConverter.cs index 1736ddf33..f5e79159c 100644 --- a/StabilityMatrix.Core/Helper/GenerationParametersConverter.cs +++ b/StabilityMatrix.Core/Helper/GenerationParametersConverter.cs @@ -46,6 +46,9 @@ private static readonly ImmutableDictionary< x => x.Key ); + /// + /// Converts a parameters-type string to a . + /// public static bool TryGetSamplerScheduler( string parameters, out ComfySamplerScheduler samplerScheduler @@ -54,6 +57,9 @@ out ComfySamplerScheduler samplerScheduler return ParamsToSamplerSchedulers.TryGetValue(parameters, out samplerScheduler); } + /// + /// Converts a to a parameters-type string. + /// public static bool TryGetParameters( ComfySamplerScheduler samplerScheduler, [NotNullWhen(true)] out string? parameters From 74f4d6e1a7b3bd73b361fd7f07c8335dc7bc799e Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 28 Sep 2023 17:24:23 -0400 Subject: [PATCH 417/474] Use GenerationParametersConvert for samplers --- .../Inference/SamplerCardViewModel.cs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs index 1200e013e..1d70fa42f 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs @@ -6,6 +6,7 @@ using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Api.Comfy; @@ -72,24 +73,39 @@ public void LoadStateFromParameters(GenerationParameters parameters) Steps = parameters.Steps; CfgScale = parameters.CfgScale; - if (parameters.GetComfySamplers() is { } paramSamplers) + if ( + !string.IsNullOrEmpty(parameters.Sampler) + && GenerationParametersConverter.TryGetSamplerScheduler( + parameters.Sampler, + out var samplerScheduler + ) + ) { - var (sampler, scheduler) = paramSamplers; - - SelectedSampler = ClientManager.Samplers.FirstOrDefault(s => s.Name == sampler.Name); + SelectedSampler = ClientManager.Samplers.FirstOrDefault( + s => s == samplerScheduler.Sampler + ); + SelectedScheduler = ClientManager.Schedulers.FirstOrDefault( + s => s == samplerScheduler.Scheduler + ); } } /// public GenerationParameters SaveStateToParameters(GenerationParameters parameters) { + var sampler = GenerationParametersConverter.TryGetParameters( + new ComfySamplerScheduler(SelectedSampler ?? default, SelectedScheduler ?? default), + out var res + ) + ? res + : null; return parameters with { Width = Width, Height = Height, Steps = Steps, CfgScale = CfgScale, - Sampler = SelectedSampler?.Name + Sampler = sampler, }; } } From 4bf22ae7feeb31425cb1f1958619aca8168278af Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 28 Sep 2023 18:42:57 -0400 Subject: [PATCH 418/474] Add restore dock layout menu option --- .../Controls/Dock/DockUserControlBase.cs | 45 +++++++++++++------ .../Languages/Resources.Designer.cs | 27 +++++++++++ .../Languages/Resources.resx | 9 ++++ .../Inference/LoadViewStateEventArgs.cs | 2 +- .../StabilityMatrix.Avalonia.csproj | 3 +- .../Base/InferenceTabViewModelBase.cs | 17 +++++++ .../Inference/InferenceTextToImageView.axaml | 16 ++++--- .../Views/InferencePage.axaml | 13 +++++- 8 files changed, 108 insertions(+), 24 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs b/StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs index 2191689fc..d634c0cc3 100644 --- a/StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs +++ b/StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs @@ -3,6 +3,7 @@ using System.Text.Json.Nodes; using System.Threading.Tasks; using Avalonia; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Threading; @@ -10,6 +11,7 @@ using Dock.Model; using Dock.Model.Avalonia.Json; using Dock.Model.Core; +using Dock.Serializer; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.ViewModels.Base; @@ -21,22 +23,23 @@ namespace StabilityMatrix.Avalonia.Controls.Dock; /// public abstract class DockUserControlBase : DropTargetUserControlBase { - protected DockControl? BaseDock; - protected readonly AvaloniaDockSerializer DockSerializer = new(); - protected readonly DockState DockState = new(); + private DockControl? baseDock; + private readonly DockSerializer dockSerializer = new(typeof(AvaloniaList<>)); + private readonly DockState dockState = new(); /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - BaseDock = + baseDock = this.FindControl("Dock") ?? throw new NullReferenceException("DockControl not found"); - if (BaseDock.Layout is { } layout) + if (baseDock.Layout is { } layout) { - Dispatcher.UIThread.Post(() => DockState.Save(layout), DispatcherPriority.Background); + dockState.Save(layout); + // Dispatcher.UIThread.Post(() => dockState.Save(layout), DispatcherPriority.Background); } } @@ -81,31 +84,45 @@ private void DataContext_OnSaveViewStateRequested(object? sender, SaveViewStateE private void DataContext_OnLoadViewStateRequested(object? sender, LoadViewStateEventArgs args) { - if (args.State.DockLayout is { } layout) + if (args.State?.DockLayout is { } layout) { + // Provided LoadDockLayout(layout); } + else + { + // Restore default + RestoreDockLayout(); + } } - protected void LoadDockLayout(JsonObject data) + private void LoadDockLayout(JsonObject data) { LoadDockLayout(data.ToJsonString()); } - protected void LoadDockLayout(string data) + private void LoadDockLayout(string data) { - if (BaseDock is null) + if (baseDock is null) return; - if (DockSerializer.Deserialize(data) is { } layout) + if (dockSerializer.Deserialize(data) is { } layout) + { + baseDock.Layout = layout; + } + } + + private void RestoreDockLayout() + { + // TODO: idk this doesn't work + if (baseDock?.Layout != null) { - BaseDock.Layout = layout; - DockState.Restore(layout); + dockState.Restore(baseDock.Layout); } } protected string? SaveDockLayout() { - return BaseDock is null ? null : DockSerializer.Serialize(BaseDock.Layout); + return baseDock is null ? null : dockSerializer.Serialize(baseDock.Layout); } } diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index ec365b895..439a5da2c 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -257,6 +257,15 @@ public static string Action_OpenOnCivitAi { } } + /// + /// Looks up a localized string similar to Open Project.... + /// + public static string Action_OpenProjectEllipsis { + get { + return ResourceManager.GetString("Action_OpenProjectEllipsis", resourceCulture); + } + } + /// /// Looks up a localized string similar to Open Web UI. /// @@ -329,6 +338,15 @@ public static string Action_Restart { } } + /// + /// Looks up a localized string similar to Restore Default Layout. + /// + public static string Action_RestoreDefaultLayout { + get { + return ResourceManager.GetString("Action_RestoreDefaultLayout", resourceCulture); + } + } + /// /// Looks up a localized string similar to Retry. /// @@ -347,6 +365,15 @@ public static string Action_Save { } } + /// + /// Looks up a localized string similar to Save As.... + /// + public static string Action_SaveAsEllipsis { + get { + return ResourceManager.GetString("Action_SaveAsEllipsis", resourceCulture); + } + } + /// /// Looks up a localized string similar to Search. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index 8bd387554..a16979647 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -669,4 +669,13 @@ Release Notes + + Open Project... + + + Save As... + + + Restore Default Layout + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Models/Inference/LoadViewStateEventArgs.cs b/StabilityMatrix.Avalonia/Models/Inference/LoadViewStateEventArgs.cs index ae906de4c..5389dc2af 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/LoadViewStateEventArgs.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/LoadViewStateEventArgs.cs @@ -7,5 +7,5 @@ namespace StabilityMatrix.Avalonia.Models.Inference; /// public class LoadViewStateEventArgs : EventArgs { - public required ViewState State { get; init; } + public ViewState? State { get; init; } } diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index 551017506..42bc3d818 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -33,8 +33,9 @@ - + + diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs index 8f0d074a7..b31b55013 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs @@ -14,7 +14,10 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Media.Animation; +using Microsoft.Extensions.DependencyInjection; using NLog; +using StabilityMatrix.Avalonia.Animations; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.Services; @@ -78,6 +81,8 @@ public event EventHandler LoadViewStateRequested protected void LoadViewState(LoadViewStateEventArgs args) => loadViewStateRequestedEventManager?.RaiseEvent(this, args, nameof(LoadViewStateRequested)); + protected void ResetViewState() => LoadViewState(new LoadViewStateEventArgs()); + private WeakEventManager? saveViewStateRequestedEventManager; public event EventHandler SaveViewStateRequested @@ -116,6 +121,18 @@ protected InferenceTabViewModelBase(INotificationService notificationService) this.notificationService = notificationService; } + [RelayCommand] + private void RestoreDefaultViewState() + { + // ResetViewState(); + // TODO: Dock reset not working, using this hack for now to get a new view + + var navService = App.Services.GetRequiredService(); + navService.NavigateTo(new SuppressNavigationTransitionInfo()); + ((IPersistentViewProvider)this).AttachedPersistentView = null; + navService.NavigateTo(new BetterEntranceNavigationTransition()); + } + [RelayCommand] private async Task DebugSaveViewState() { diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml b/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml index 407064db3..2a6e01235 100644 --- a/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml +++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml @@ -36,7 +36,9 @@ x:Name="MainLayout" Id="MainLayout" Orientation="Horizontal"> - + + + - + - + - + + DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DockControl}, Path=DataContext}"> + + diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml b/StabilityMatrix.Avalonia/Views/InferencePage.axaml index 113ca2fa7..4dbc644a1 100644 --- a/StabilityMatrix.Avalonia/Views/InferencePage.axaml +++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml @@ -202,16 +202,25 @@ + IconSource="OpenFolder" + Text="{x:Static lang:Resources.Action_OpenProjectEllipsis}" /> + Text="{x:Static lang:Resources.Action_SaveAsEllipsis}" /> + + + + From 5054536ba659b28b66ff5827f54702924a218cda Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 28 Sep 2023 18:45:10 -0400 Subject: [PATCH 419/474] 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 42bc3d818..9dcafd9dc 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.0-dev.5 + 2.5.0-pre.2 $(Version) true true From f670ee89c2b78cbf77d4b9833ee1843bfe0e09b7 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 28 Sep 2023 19:41:04 -0400 Subject: [PATCH 420/474] Update AsyncImageLoader.Avalonia to 3.2.1 --- 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 9dcafd9dc..d5c33ac0d 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj @@ -19,7 +19,7 @@ - + From 50dd75d0335f0f60740fefb14aeb809d67ecd1e5 Mon Sep 17 00:00:00 2001 From: Ionite Date: Thu, 28 Sep 2023 20:29:29 -0400 Subject: [PATCH 421/474] Fix refiner vae not set for custom vaes --- .../Inference/InferenceTextToImageViewModel.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index 2606a1f4b..f69a44356 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -187,11 +187,12 @@ ModelCardViewModel is // Override with custom VAE if enabled if (ModelCardViewModel is { IsVaeSelectionEnabled: true, SelectedVae.IsDefault: false }) { - builder.Connections.BaseVAE = nodes - .AddNamedNode( - ComfyNodeBuilder.VAELoader("VAELoader", ModelCardViewModel.SelectedVae.FileName) - ) - .Output; + var customVaeLoader = nodes.AddNamedNode( + ComfyNodeBuilder.VAELoader("VAELoader", ModelCardViewModel.SelectedVae.FileName) + ); + + builder.Connections.BaseVAE = customVaeLoader.Output; + builder.Connections.RefinerVAE = customVaeLoader.Output; } // If hi-res fix is enabled, add the LatentUpscale node and another KSampler node From 871e53e75b86335c48f41a9736ef757f114fd4fe Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 29 Sep 2023 17:05:28 -0400 Subject: [PATCH 422/474] Nullability warning fixes --- .../Controls/AdvancedImageBox.axaml.cs | 12 +++++++++--- .../ViewModels/SettingsViewModel.cs | 2 +- .../Models/Packages/PackageVersionOptions.cs | 6 ++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs b/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs index ecae9302c..8d8f7e723 100644 --- a/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs +++ b/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs @@ -487,9 +487,15 @@ public enum SelectionModes #endregion #region UI Controls - public ScrollBar HorizontalScrollBar { get; private set; } - public ScrollBar VerticalScrollBar { get; private set; } - public ContentPresenter ViewPort { get; private set; } + + [NotNull] + public ScrollBar? HorizontalScrollBar { get; private set; } + + [NotNull] + public ScrollBar? VerticalScrollBar { get; private set; } + + [NotNull] + public ContentPresenter? ViewPort { get; private set; } // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract public bool IsViewPortLoaded => ViewPort is not null; diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index 588e85b19..6053e1e0a 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -238,7 +238,7 @@ partial void OnSelectedThemeChanged(string? value) partial void OnSelectedLanguageChanged(CultureInfo? oldValue, CultureInfo newValue) { - if (oldValue is null || newValue.Name == Cultures.Current.Name) + if (oldValue is null || newValue.Name == Cultures.Current?.Name) return; // Set locale diff --git a/StabilityMatrix.Core/Models/Packages/PackageVersionOptions.cs b/StabilityMatrix.Core/Models/Packages/PackageVersionOptions.cs index e91c4caff..3c98af150 100644 --- a/StabilityMatrix.Core/Models/Packages/PackageVersionOptions.cs +++ b/StabilityMatrix.Core/Models/Packages/PackageVersionOptions.cs @@ -2,6 +2,8 @@ public class PackageVersionOptions { - public IEnumerable? AvailableVersions { get; set; } - public IEnumerable? AvailableBranches { get; set; } + public IEnumerable AvailableVersions { get; set; } = + Enumerable.Empty(); + public IEnumerable AvailableBranches { get; set; } = + Enumerable.Empty(); } From 8613ec13594834c1acc0d41776661255b7520421 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 29 Sep 2023 17:05:59 -0400 Subject: [PATCH 423/474] Add TemplatedControlBase --- .../DropTargetTemplatedControlBase.cs | 5 +-- .../Controls/ImageFolderCard.axaml.cs | 24 +---------- .../Controls/TemplatedControlBase.cs | 40 +++++++++++++++++++ .../Inference/ImageFolderCardViewModel.cs | 6 --- 4 files changed, 43 insertions(+), 32 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Controls/TemplatedControlBase.cs diff --git a/StabilityMatrix.Avalonia/Controls/DropTargetTemplatedControlBase.cs b/StabilityMatrix.Avalonia/Controls/DropTargetTemplatedControlBase.cs index 8bd10e05f..daa54ab21 100644 --- a/StabilityMatrix.Avalonia/Controls/DropTargetTemplatedControlBase.cs +++ b/StabilityMatrix.Avalonia/Controls/DropTargetTemplatedControlBase.cs @@ -1,10 +1,9 @@ -using Avalonia.Controls.Primitives; -using Avalonia.Input; +using Avalonia.Input; using StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.Controls; -public abstract class DropTargetTemplatedControlBase : TemplatedControl +public abstract class DropTargetTemplatedControlBase : TemplatedControlBase { protected DropTargetTemplatedControlBase() { diff --git a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs index bdaf351b8..b36e1fe10 100644 --- a/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs +++ b/StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml.cs @@ -1,31 +1,9 @@ -using AsyncAwaitBestPractices; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Threading; -using StabilityMatrix.Avalonia.ViewModels.Base; +using Avalonia.Input; namespace StabilityMatrix.Avalonia.Controls; public class ImageFolderCard : DropTargetTemplatedControlBase { - /// - protected override void OnLoaded(RoutedEventArgs e) - { - base.OnLoaded(e); - - if (DataContext is ViewModelBase vm) - { - vm.OnLoaded(); - Dispatcher.UIThread - .InvokeAsync(async () => - { - await vm.OnLoadedAsync(); - }) - .SafeFireAndForget(); - } - } - /// protected override void DropHandler(object? sender, DragEventArgs e) { diff --git a/StabilityMatrix.Avalonia/Controls/TemplatedControlBase.cs b/StabilityMatrix.Avalonia/Controls/TemplatedControlBase.cs new file mode 100644 index 000000000..4d8fb94d1 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/TemplatedControlBase.cs @@ -0,0 +1,40 @@ +using AsyncAwaitBestPractices; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Threading; +using StabilityMatrix.Avalonia.ViewModels.Base; + +namespace StabilityMatrix.Avalonia.Controls; + +public abstract class TemplatedControlBase : TemplatedControl +{ + /// + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + + if (DataContext is not ViewModelBase viewModel) + return; + + // Run synchronous load then async load + viewModel.OnLoaded(); + + // Can't block here so we'll run as async on UI thread + Dispatcher.UIThread.InvokeAsync(viewModel.OnLoadedAsync).SafeFireAndForget(); + } + + /// + protected override void OnUnloaded(RoutedEventArgs e) + { + base.OnUnloaded(e); + + if (DataContext is not ViewModelBase viewModel) + return; + + // Run synchronous load then async load + viewModel.OnUnloaded(); + + // Can't block here so we'll run as async on UI thread + Dispatcher.UIThread.InvokeAsync(viewModel.OnUnloadedAsync).SafeFireAndForget(); + } +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs index 532b4bf5e..e8097834c 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs @@ -6,9 +6,6 @@ using AsyncImageLoader; using Avalonia; using Avalonia.Controls.Notifications; -using Avalonia.Controls.Primitives; -using Avalonia.Input; -using Avalonia.Layout; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; @@ -17,7 +14,6 @@ using DynamicData.Binding; using FuzzySharp; using FuzzySharp.PreProcess; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SkiaSharp; using StabilityMatrix.Avalonia.Controls; @@ -27,8 +23,6 @@ using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Dialogs; -using StabilityMatrix.Avalonia.Views; -using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; From 3742bc6c3d616102166f617307eb4dbeb6125976 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 29 Sep 2023 17:06:24 -0400 Subject: [PATCH 424/474] Fix gallery DataContext binding when floated --- .../Views/Inference/InferenceTextToImageView.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml b/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml index 2a6e01235..f51da19c9 100644 --- a/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml +++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml @@ -210,7 +210,7 @@ + DataContext="{Binding ElementName=Dock, Path=DataContext}"> From bd50cb7cb9af1ffca200ce75bf8ed14ed42b70fc Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 29 Sep 2023 17:06:42 -0400 Subject: [PATCH 425/474] Simplify UserControlBase events --- .../Controls/UserControlBase.cs | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/UserControlBase.cs b/StabilityMatrix.Avalonia/Controls/UserControlBase.cs index 217b32e31..e4b7639c5 100644 --- a/StabilityMatrix.Avalonia/Controls/UserControlBase.cs +++ b/StabilityMatrix.Avalonia/Controls/UserControlBase.cs @@ -12,40 +12,34 @@ public class UserControlBase : UserControl { static UserControlBase() { - LoadedEvent.AddClassHandler( - (cls, args) => cls.OnLoadedEvent(args)); + LoadedEvent.AddClassHandler((cls, args) => cls.OnLoadedEvent(args)); - UnloadedEvent.AddClassHandler( - (cls, args) => cls.OnUnloadedEvent(args)); + UnloadedEvent.AddClassHandler((cls, args) => cls.OnUnloadedEvent(args)); } // ReSharper disable once UnusedParameter.Global protected virtual void OnLoadedEvent(RoutedEventArgs? e) { - if (DataContext is not ViewModelBase viewModel) return; - + if (DataContext is not ViewModelBase viewModel) + return; + // Run synchronous load then async load viewModel.OnLoaded(); - + // Can't block here so we'll run as async on UI thread - Dispatcher.UIThread.InvokeAsync(async () => - { - await viewModel.OnLoadedAsync(); - }).SafeFireAndForget(); + Dispatcher.UIThread.InvokeAsync(viewModel.OnLoadedAsync).SafeFireAndForget(); } - + // ReSharper disable once UnusedParameter.Global protected virtual void OnUnloadedEvent(RoutedEventArgs? e) { - if (DataContext is not ViewModelBase viewModel) return; - + if (DataContext is not ViewModelBase viewModel) + return; + // Run synchronous load then async load viewModel.OnUnloaded(); - + // Can't block here so we'll run as async on UI thread - Dispatcher.UIThread.InvokeAsync(async () => - { - await viewModel.OnUnloadedAsync(); - }).SafeFireAndForget(); + Dispatcher.UIThread.InvokeAsync(viewModel.OnUnloadedAsync).SafeFireAndForget(); } } From dcbfb60e81efd749d3561aa3a8306179314b1bce Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 29 Sep 2023 17:06:49 -0400 Subject: [PATCH 426/474] Remove unused class --- .../Models/ILoadableState.cs | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 StabilityMatrix.Avalonia/Models/ILoadableState.cs diff --git a/StabilityMatrix.Avalonia/Models/ILoadableState.cs b/StabilityMatrix.Avalonia/Models/ILoadableState.cs deleted file mode 100644 index cce10463a..000000000 --- a/StabilityMatrix.Avalonia/Models/ILoadableState.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Nodes; - -namespace StabilityMatrix.Avalonia.Models; - -public interface ILoadableState : IJsonLoadableState -{ - new Type LoadableStateType => typeof(T); - - void LoadState(T state); - - new void LoadStateFromJsonObject(JsonObject state) - { - state.Deserialize(LoadableStateType); - } - - T SaveState(); - - new JsonObject SaveStateToJsonObject() - { - var node = JsonSerializer.SerializeToNode(SaveState()); - return node?.AsObject() ?? throw new - InvalidOperationException("Failed to serialize state to JSON object."); - } -} From 99b78e91369a34a98a1db286971c640d3b9b0f52 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 29 Sep 2023 17:10:36 -0400 Subject: [PATCH 427/474] Remove unused class --- .../Models/Packages/ComfyUI.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index a4da6ce0b..7e1e4e58c 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -455,24 +455,4 @@ await Task.Run(() => }) .ConfigureAwait(false); } - - public class ComfyModelPathsYaml - { - public class SmData - { - public string Checkpoints { get; set; } - public string Vae { get; set; } - public string Loras { get; set; } - public string UpscaleModels { get; set; } - public string Embeddings { get; set; } - public string Hypernetworks { get; set; } - public string Controlnet { get; set; } - public string Clip { get; set; } - public string Diffusers { get; set; } - public string Gligen { get; set; } - public string VaeApprox { get; set; } - } - - public SmData? StabilityMatrix { get; set; } - } } From dd6b8a066a3c7f4f8a155129effe002751134e05 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 29 Sep 2023 17:21:21 -0400 Subject: [PATCH 428/474] More nullable fixes --- .../Controls/CodeCompletion/CompletionList.cs | 6 +++++- StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs | 2 +- StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs | 9 +++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs index e7b8f09ae..9150209b3 100644 --- a/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs +++ b/StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs @@ -502,7 +502,11 @@ private void SelectItemWithStart(string query) if (string.IsNullOrEmpty(query)) return; - var suggestedIndex = _listBox.SelectedIndex; + var suggestedIndex = _listBox?.SelectedIndex ?? -1; + if (suggestedIndex == -1) + { + return; + } var bestIndex = -1; var bestQuality = -1; diff --git a/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs b/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs index 08e8efc02..a8f2996ee 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs @@ -47,7 +47,7 @@ public Task UpsertCivitModelQueryCacheEntryAsync(CivitModelQueryCacheEntry public Task GetGithubCacheEntry(string cacheKey) { - return Task.FromResult(null); + return Task.FromResult(null); } public Task UpsertGithubCacheEntry(GithubCacheEntry cacheEntry) diff --git a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs index 86706df09..35c5a9158 100644 --- a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs @@ -239,8 +239,13 @@ protected Task UnzipPackage(string installLocation, IProgress? p public override async Task CheckForUpdates(InstalledPackage package) { var currentVersion = package.Version; - if (currentVersion == null) + if (currentVersion is null or { InstalledReleaseVersion: null, InstalledBranch: null }) { + Logger.Warn( + "Could not check updates for package {Name}, version is invalid: {@currentVersion}", + Name, + currentVersion + ); return false; } @@ -254,7 +259,7 @@ public override async Task CheckForUpdates(InstalledPackage package) } var allCommits = ( - await GetAllCommits(currentVersion.InstalledBranch).ConfigureAwait(false) + await GetAllCommits(currentVersion.InstalledBranch!).ConfigureAwait(false) )?.ToList(); if (allCommits == null || !allCommits.Any()) { From e4a2b7ea2daab111fdeeeba33d61765e8ccf9cd5 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 29 Sep 2023 19:43:04 -0400 Subject: [PATCH 429/474] Add FreeU option to text2img --- StabilityMatrix.Avalonia/App.axaml | 1 + StabilityMatrix.Avalonia/App.axaml.cs | 5 +- .../Controls/FreeUCard.axaml | 75 +++++++++++++++++++ .../Controls/FreeUCard.axaml.cs | 5 ++ .../DesignData/DesignData.cs | 3 + .../Extensions/ComfyNodeBuilderExtensions.cs | 56 +++++++------- .../Inference/FreeUCardViewModel.cs | 31 ++++++++ .../InferenceTextToImageViewModel.cs | 60 ++++++++++++++- .../Api/Comfy/Nodes/ComfyNodeBuilder.cs | 25 +++++++ 9 files changed, 231 insertions(+), 30 deletions(-) create mode 100644 StabilityMatrix.Avalonia/Controls/FreeUCard.axaml create mode 100644 StabilityMatrix.Avalonia/Controls/FreeUCard.axaml.cs create mode 100644 StabilityMatrix.Avalonia/ViewModels/Inference/FreeUCardViewModel.cs diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml index 8488c76fb..76cfbbf8c 100644 --- a/StabilityMatrix.Avalonia/App.axaml +++ b/StabilityMatrix.Avalonia/App.axaml @@ -58,6 +58,7 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/FreeUCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/FreeUCard.axaml.cs new file mode 100644 index 000000000..555b87d4a --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/FreeUCard.axaml.cs @@ -0,0 +1,5 @@ +using Avalonia.Controls.Primitives; + +namespace StabilityMatrix.Avalonia.Controls; + +public class FreeUCard : TemplatedControl { } diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 1f538779d..7a5e9b0da 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -8,6 +8,7 @@ using System.Text; using AvaloniaEdit.Utils; using Microsoft.Extensions.DependencyInjection; +using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.TagCompletion; @@ -539,6 +540,8 @@ public static PackageManagerViewModel PackageManagerViewModel public static ImageFolderCardViewModel ImageFolderCardViewModel => DialogFactory.Get(); + public static FreeUCardViewModel FreeUCardViewModel => DialogFactory.Get(); + public static PromptCardViewModel PromptCardViewModel => DialogFactory.Get(vm => { diff --git a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs index e0fc820c0..92e732560 100644 --- a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs +++ b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs @@ -39,7 +39,8 @@ public static void SetupBaseSampler( SamplerCardViewModel samplerCardViewModel, PromptCardViewModel promptCardViewModel, ModelCardViewModel modelCardViewModel, - IModelIndexService modelIndexService + IModelIndexService modelIndexService, + Action? postModelLoad = null ) { // Load base checkpoint @@ -51,11 +52,12 @@ IModelIndexService modelIndexService ) ); + builder.Connections.BaseModel = checkpointLoader.GetOutput(0); + builder.Connections.BaseClip = checkpointLoader.GetOutput(1); builder.Connections.BaseVAE = checkpointLoader.GetOutput(2); - // Define model and clip for connections for chaining - var modelSource = checkpointLoader.GetOutput(0); - var clipSource = checkpointLoader.GetOutput(1); + // Run post model load action + postModelLoad?.Invoke(builder); // Load prompts var prompt = promptCardViewModel.GetPrompt(); @@ -69,25 +71,28 @@ IModelIndexService modelIndexService // Convert to local file names var lorasGroup = builder.Group_LoraLoadMany( "Loras", - modelSource, - clipSource, + builder.Connections.BaseModel, + builder.Connections.BaseClip, prompt.GetExtraNetworksAsLocalModels(modelIndexService) ); // Set as source - modelSource = lorasGroup.Output1; - clipSource = lorasGroup.Output2; + builder.Connections.BaseModel = lorasGroup.Output1; + builder.Connections.BaseClip = lorasGroup.Output2; } - builder.Connections.BaseModel = modelSource; // Clips var positiveClip = builder.Nodes.AddNamedNode( - ComfyNodeBuilder.ClipTextEncode("PositiveCLIP", clipSource, prompt.ProcessedText) + ComfyNodeBuilder.ClipTextEncode( + "PositiveCLIP", + builder.Connections.BaseClip, + prompt.ProcessedText + ) ); var negativeClip = builder.Nodes.AddNamedNode( ComfyNodeBuilder.ClipTextEncode( "NegativeCLIP", - clipSource, + builder.Connections.BaseClip, negativePrompt.ProcessedText ) ); @@ -103,7 +108,7 @@ IModelIndexService modelIndexService var sampler = builder.Nodes.AddNamedNode( ComfyNodeBuilder.KSampler( "Sampler", - modelSource, + builder.Connections.BaseModel, Convert.ToUInt64(seedCardViewModel.Seed), samplerCardViewModel.Steps, samplerCardViewModel.CfgScale, @@ -129,7 +134,7 @@ IModelIndexService modelIndexService var sampler = builder.Nodes.AddNamedNode( ComfyNodeBuilder.KSamplerAdvanced( "Sampler", - modelSource, + builder.Connections.BaseModel, true, Convert.ToUInt64(seedCardViewModel.Seed), totalSteps, @@ -157,7 +162,8 @@ public static void SetupRefinerSampler( SamplerCardViewModel samplerCardViewModel, PromptCardViewModel promptCardViewModel, ModelCardViewModel modelCardViewModel, - IModelIndexService modelIndexService + IModelIndexService modelIndexService, + Action? postModelLoad = null ) { // Load refiner checkpoint @@ -169,11 +175,12 @@ IModelIndexService modelIndexService ) ); + builder.Connections.RefinerModel = checkpointLoader.GetOutput(0); + builder.Connections.RefinerClip = checkpointLoader.GetOutput(1); builder.Connections.RefinerVAE = checkpointLoader.GetOutput(2); - // Define model and clip for connections for chaining - var modelSource = checkpointLoader.GetOutput(0); - var clipSource = checkpointLoader.GetOutput(1); + // Run post model load action + postModelLoad?.Invoke(builder); // Load prompts var prompt = promptCardViewModel.GetPrompt(); @@ -187,29 +194,28 @@ IModelIndexService modelIndexService // Convert to local file names var lorasGroup = builder.Group_LoraLoadMany( "Refiner_Loras", - modelSource, - clipSource, + builder.Connections.RefinerModel, + builder.Connections.RefinerClip, prompt.GetExtraNetworksAsLocalModels(modelIndexService) ); // Set as source - modelSource = lorasGroup.Output1; - clipSource = lorasGroup.Output2; + builder.Connections.RefinerModel = lorasGroup.Output1; + builder.Connections.RefinerClip = lorasGroup.Output2; } - builder.Connections.RefinerModel = modelSource; // Clips var positiveClip = builder.Nodes.AddNamedNode( ComfyNodeBuilder.ClipTextEncode( "Refiner_PositiveCLIP", - clipSource, + builder.Connections.RefinerClip, prompt.ProcessedText ) ); var negativeClip = builder.Nodes.AddNamedNode( ComfyNodeBuilder.ClipTextEncode( "Refiner_NegativeCLIP", - clipSource, + builder.Connections.RefinerClip, negativePrompt.ProcessedText ) ); @@ -224,7 +230,7 @@ IModelIndexService modelIndexService var sampler = builder.Nodes.AddNamedNode( ComfyNodeBuilder.KSamplerAdvanced( "Refiner_Sampler", - modelSource, + builder.Connections.RefinerModel, false, Convert.ToUInt64(seedCardViewModel.Seed), totalSteps, diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/FreeUCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/FreeUCardViewModel.cs new file mode 100644 index 000000000..059975d38 --- /dev/null +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/FreeUCardViewModel.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; +using CommunityToolkit.Mvvm.ComponentModel; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.ViewModels.Inference; + +[View(typeof(FreeUCard))] +public partial class FreeUCardViewModel : LoadableViewModelBase +{ + [ObservableProperty] + [Required] + [Range(0D, 10D)] + private double b1 = 1.1; + + [ObservableProperty] + [Required] + [Range(0D, 10D)] + private double b2 = 1.2; + + [ObservableProperty] + [Required] + [Range(0D, 10D)] + private double s1 = 0.9; + + [ObservableProperty] + [Required] + [Range(0D, 10D)] + private double s2 = 0.2; +} diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs index f69a44356..64f4c62f3 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs @@ -55,24 +55,33 @@ public class InferenceTextToImageViewModel [JsonPropertyName("HiresUpscaler")] public UpscalerCardViewModel HiresUpscalerCardViewModel { get; } + [JsonPropertyName("FreeU")] + public FreeUCardViewModel FreeUCardViewModel { get; } + [JsonPropertyName("BatchSize")] public BatchSizeCardViewModel BatchSizeCardViewModel { get; } [JsonPropertyName("Seed")] public SeedCardViewModel SeedCardViewModel { get; } - public bool IsHiresFixEnabled + public bool IsFreeUEnabled { get => StackCardViewModel.GetCard().IsEnabled; set => StackCardViewModel.GetCard().IsEnabled = value; } - public bool IsUpscaleEnabled + public bool IsHiresFixEnabled { get => StackCardViewModel.GetCard(1).IsEnabled; set => StackCardViewModel.GetCard(1).IsEnabled = value; } + public bool IsUpscaleEnabled + { + get => StackCardViewModel.GetCard(2).IsEnabled; + set => StackCardViewModel.GetCard(2).IsEnabled = value; + } + public InferenceTextToImageViewModel( INotificationService notificationService, IInferenceClientManager inferenceClientManager, @@ -106,6 +115,7 @@ IModelIndexService modelIndexService }); HiresUpscalerCardViewModel = vmFactory.Get(); UpscalerCardViewModel = vmFactory.Get(); + FreeUCardViewModel = vmFactory.Get(); BatchSizeCardViewModel = vmFactory.Get(); StackCardViewModel = vmFactory.Get(); @@ -115,6 +125,12 @@ IModelIndexService modelIndexService { ModelCardViewModel, SamplerCardViewModel, + // Free U + vmFactory.Get(stackExpander => + { + stackExpander.Title = "FreeU"; + stackExpander.AddCards(new LoadableViewModelBase[] { FreeUCardViewModel }); + }), // Hires Fix vmFactory.Get(stackExpander => { @@ -166,7 +182,25 @@ protected override void BuildPrompt(BuildPromptEventArgs args) SamplerCardViewModel, PromptCardViewModel, ModelCardViewModel, - modelIndexService + modelIndexService, + postModelLoad: x => + { + if (IsFreeUEnabled) + { + builder.Connections.BaseModel = nodes + .AddNamedNode( + ComfyNodeBuilder.FreeU( + "FreeU", + x.Connections.BaseModel!, + FreeUCardViewModel.B1, + FreeUCardViewModel.B2, + FreeUCardViewModel.S1, + FreeUCardViewModel.S2 + ) + ) + .Output; + } + } ); // Setup refiner stage if enabled @@ -180,7 +214,25 @@ ModelCardViewModel is SamplerCardViewModel, PromptCardViewModel, ModelCardViewModel, - modelIndexService + modelIndexService, + postModelLoad: x => + { + if (IsFreeUEnabled) + { + builder.Connections.RefinerModel = nodes + .AddNamedNode( + ComfyNodeBuilder.FreeU( + "Refiner_FreeU", + x.Connections.RefinerModel!, + FreeUCardViewModel.B1, + FreeUCardViewModel.B2, + FreeUCardViewModel.S1, + FreeUCardViewModel.S2 + ) + ) + .Output; + } + } ); } diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs index ecdb3dcea..6e9d30923 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs @@ -240,6 +240,29 @@ string modelName }; } + public static NamedComfyNode FreeU( + string name, + ModelNodeConnection model, + double b1, + double b2, + double s1, + double s2 + ) + { + return new NamedComfyNode(name) + { + ClassType = "FreeU", + Inputs = new Dictionary + { + ["model"] = model.Data, + ["b1"] = b1, + ["b2"] = b2, + ["s1"] = s1, + ["s2"] = s2 + } + }; + } + public static NamedComfyNode ClipTextEncode( string name, ClipNodeConnection clip, @@ -611,12 +634,14 @@ public class NodeBuilderConnections { public ModelNodeConnection? BaseModel { get; set; } public VAENodeConnection? BaseVAE { get; set; } + public ClipNodeConnection? BaseClip { get; set; } public ConditioningNodeConnection? BaseConditioning { get; set; } public ConditioningNodeConnection? BaseNegativeConditioning { get; set; } public ModelNodeConnection? RefinerModel { get; set; } public VAENodeConnection? RefinerVAE { get; set; } + public ClipNodeConnection? RefinerClip { get; set; } public ConditioningNodeConnection? RefinerConditioning { get; set; } public ConditioningNodeConnection? RefinerNegativeConditioning { get; set; } From 10e18334fdaf33b3bea4507e19131085b761cd71 Mon Sep 17 00:00:00 2001 From: Ionite Date: Fri, 29 Sep 2023 19:43:49 -0400 Subject: [PATCH 430/474] 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 d5c33ac0d..e69408b90 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.0-pre.2 + 2.5.0-pre.3 $(Version) true true From 38be8da27f1f84ccadc99dd6e02ca42ae5292dc0 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 30 Sep 2023 16:22:39 -0400 Subject: [PATCH 431/474] Remove double logging in DataStoreLoggerTarget --- .../LogViewer/DataStoreLoggerTarget.cs | 4 ---- StabilityMatrix.Avalonia/App.axaml.cs | 9 ++++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs index 22646b047..da8f501a7 100644 --- a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs +++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs @@ -71,10 +71,6 @@ protected override void Write(LogEventInfo logEvent) Color = _config!.Colors[logLevel], } ); - - Debug.WriteLine( - $"--- [{logLevel.ToString()[..3]}] {message} - {logEvent.Exception?.Message ?? "no error"}" - ); } #endregion diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index 36a56e931..bab40548b 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -699,7 +699,14 @@ private static LoggingConfiguration ConfigureLogging() { var debugTarget = builder .ForTarget("console") - .WriteTo(new DebuggerTarget { Layout = "${message}" }) + .WriteTo( + new DebuggerTarget + { + Layout = + "[${level:format=TriLetter}] " + + "${callsite:includeNamespace=false:captureStackTrace=false}: ${message}" + } + ) .WithAsync(); var fileTarget = builder From cd8d7816c39bb775b153d49ebdada000f52e3b34 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 30 Sep 2023 16:34:30 -0400 Subject: [PATCH 432/474] Ignore websocket errors for sentry --- StabilityMatrix.Avalonia/Program.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/StabilityMatrix.Avalonia/Program.cs b/StabilityMatrix.Avalonia/Program.cs index b8c0cd007..16c0c3fdf 100644 --- a/StabilityMatrix.Avalonia/Program.cs +++ b/StabilityMatrix.Avalonia/Program.cs @@ -176,6 +176,19 @@ private static void ConfigureSentry() #if DEBUG o.Environment = "Development"; #endif + // Filters + o.SetBeforeSend( + (sentryEvent, _) => + { + // Ignore websocket errors from ComfyClient + if (sentryEvent.Logger == "Websocket.Client.WebsocketClient") + { + return null; + } + + return sentryEvent; + } + ); }); } From 66a394b31d6c2b8b4d47a9b56eabb8e44ab02a2f Mon Sep 17 00:00:00 2001 From: Ionite Date: Sat, 30 Sep 2023 16:35:52 -0400 Subject: [PATCH 433/474] Some cleanup --- StabilityMatrix.Avalonia/App.axaml.cs | 17 ----------------- StabilityMatrix.Core/Inference/ComfyClient.cs | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index bab40548b..2633c3f0b 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -17,9 +17,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Controls.Primitives; using Avalonia.Input.Platform; -using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.Media.Imaging; using Avalonia.Platform; @@ -42,7 +40,6 @@ using Refit; using Sentry; using StabilityMatrix.Avalonia.Controls; -using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.DesignData; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; @@ -65,7 +62,6 @@ using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Database; -using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; @@ -81,7 +77,6 @@ using StabilityMatrix.Core.Updater; using Application = Avalonia.Application; using DrawingColor = System.Drawing.Color; -using InferenceTextToImageView = StabilityMatrix.Avalonia.Views.Inference.InferenceTextToImageView; using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace StabilityMatrix.Avalonia; @@ -762,18 +757,6 @@ private static LoggingConfiguration ConfigureLogging() o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug; // Error and higher is sent as event (default is Error) o.MinimumEventLevel = NLog.LogLevel.Error; - // Filters - o.SetBeforeSend( - (sentryEvent, _) => - { - if (sentryEvent.Logger == "Websocket.Client.WebsocketClient") - { - return null; - } - - return sentryEvent; - } - ); }); } diff --git a/StabilityMatrix.Core/Inference/ComfyClient.cs b/StabilityMatrix.Core/Inference/ComfyClient.cs index f4b21b6ac..15433b5d8 100644 --- a/StabilityMatrix.Core/Inference/ComfyClient.cs +++ b/StabilityMatrix.Core/Inference/ComfyClient.cs @@ -83,7 +83,7 @@ public ComfyClient(IApiFactory apiFactory, Uri baseAddress) webSocketClient = new WebsocketClient(wsUri) { - Name = "ComfyClient", + Name = nameof(ComfyClient), ReconnectTimeout = TimeSpan.FromSeconds(30) }; From 8f4536ef4c16abfc1d08b2ac080327b8a0d439c0 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 1 Oct 2023 03:02:51 -0400 Subject: [PATCH 434/474] Add Italian language option --- .../Languages/Cultures.cs | 3 +- .../Languages/Resources.it-it.resx | 683 ++++++++++++++++++ 2 files changed, 685 insertions(+), 1 deletion(-) create mode 100644 StabilityMatrix.Avalonia/Languages/Resources.it-it.resx diff --git a/StabilityMatrix.Avalonia/Languages/Cultures.cs b/StabilityMatrix.Avalonia/Languages/Cultures.cs index e31d4695d..b512d2e89 100644 --- a/StabilityMatrix.Avalonia/Languages/Cultures.cs +++ b/StabilityMatrix.Avalonia/Languages/Cultures.cs @@ -21,7 +21,8 @@ public static class Cultures ["en-US"] = Default, ["ja-JP"] = new("ja-JP"), ["zh-Hans"] = new("zh-Hans"), - ["zh-Hant"] = new("zh-Hant") + ["zh-Hant"] = new("zh-Hant"), + ["it-IT"] = new("it-IT") }; public static IReadOnlyList SupportedCultures => diff --git a/StabilityMatrix.Avalonia/Languages/Resources.it-it.resx b/StabilityMatrix.Avalonia/Languages/Resources.it-it.resx new file mode 100644 index 000000000..cadda8ef1 --- /dev/null +++ b/StabilityMatrix.Avalonia/Languages/Resources.it-it.resx @@ -0,0 +1,683 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Avvia + + + Esci + + + Salva + + + Cancella + + + Lingua + + + E' richiesto il riavvio per rendere effettivi i cambiamenti + + + Riavvia + + + Riavvia Dopo + + + Riavvio Richiesto + + + Pacchetto Sconosciuto + + + Importa + + + Tipologia Pacchetto + + + Versione + + + Tipologia Versione + + + Releases + + + Branches + + + Trascina qui i checkpoint per importarli + + + Accentuazione + + + Attenuazione + + + Embeddings / Inversione Testuale + I'm not sure I'll translate this term because it is specifically contextual to Stable Diffusion, and it may confuse several users who are already using it. + + + Reti (Lora / LyCORIS) + + + Commenti + + + Mostra la griglia di pixel a livelli di zoom elevati + + + Steps + + + Steps - Base + + + Steps - Refiner + + + Scala CFG + + + Valore di Denoising + + + Larghezza + + + Altezza + + + Refiner + + + VAE + + + Modello + + + Connetti + + + In connessione... + + + Chiudi + + + In attesa della connessione... + + + Aggiornamento Disponibile + + + Diventa un Patreon + + + Unisciti al Server Discord + + + Scaricamenti + + + Installa + + + Salta la configurazione iniziale + + + Si è verificato un errore imprevisto + + + Esci dall'Applicazione + + + Nome da Visualizzare + + + Esiste già un'installazione con questo nome. + + + Scegli un nome diverso o seleziona un percorso di installazione diverso. + + + Opzioni Avanzate + + + Commit + + + Strategia Cartella Modello Condiviso + + + Versione PyTorch + + + Chiudi la finestra di dialogo al termine + + + Cartella dei Dati + + + Qui vengono installati i modelli checkpoint, le LORA, l'interfaccia utente Web, le impostazioni ecc. + + + Potresti riscontrare errori quando utilizzi un'unità FAT32 o exFAT. Seleziona un'unità diversa per un'esperienza più fluida. + + + Modalità Portable + + + In Modalità Portable, tutti i dati e le impostazioni verranno archiviati nella stessa directory dell'applicazione. Potrai spostare l'applicazione con la sua cartella 'Dati' in una posizione o computer diversi. + + + Continua + + + Immagine Precedente + + + Immagine Successiva + + + Descrizione Modello + + + E' disponibile una nuova versione di Stability Matrix! + Maybe a variable with the {appname} would be a better solution? + + + Importa più Recente - + + + Tutte le Versioni + + + Cerca modelli, #tags, o @utenti + + + Cerca + + + Ordina + + + Periodo + + + Tipo di Modello + + + Modello Base + + + Mostra Contenuti NSFW + + + Dati forniti da CivitAI + + + Pagina + + + Prima Pagina + + + Pagina Precedente + + + Pagina Successiva + + + Ultima Pagina + + + Rinomina + + + Cancella + + + Apri su CivitAI + + + Modello Collegato + + + Modello Locale + + + Mostra in Explorer + + + Nuovo + + + Cartella + + + Rilascia il file qui per importarlo + + + Recupera i metadati durante l'importazione + + + Cerca metadati collegati sulle nuove importazioni locali + + + Indicizzazione... + + + Cartella Modelli + + + Categorie + + + Iniziamo + + + Ho letto e acconsento al + + + Contratto di Licenza. + + + Trova i Metadati Collegati + + + Mostra Immagini del Modello + + + Aspetto + + + Tema + + + Gestione Checkpoint + + + Rimuovi allo spegnimento i collegamenti simbolici alla directory dei checkpoint condivisi + + + Seleziona questa opzione se riscontri problemi nello spostamento di Stability Matrix su un'altra unità + + + Reimposta la Cache dei Checkpoint + + + Ricostruisci la cache dei checkpoint installati. Da utilizzare se i checkpoint sono etichettati in modo errato nel Browser Modello + + + Ambiente del Pacchetto + + + Modifica + + + Variabili Ambiente + + + Python Incorporato + + + Controlla Versione + + + Integrazioni + + + Discord Rich Presence + + + Sistema + + + Aggiungi Stability Matrix al Menù Start + + + Utilizza la posizione corrente, puoi eseguirla di nuovo se sposti l'app + + + Disponibile solo su Windows + + + Aggiungi per l'Utente Corrente + + + Aggiungi per Tutti gli Utenti + + + Seleziona una nuova Cartella Dati + + + Non sposta i dati esistenti + + + Seleziona Cartella + + + Informazioni + + + Stability Matrix + + + Informazioni sulla Licenza e Open Source + + + Fai clic su Avvia per iniziare! + + + Ferma + + + Manda Input + + + Input + + + Invia + + + Input richiesto + + + Confermi? + + + Si + + + No + + + Apri la Web UI + + + Benvenuto su Stability Matrix! + + + Scegli la tua interfaccia preferita e clicca su Installa per inziare + + + Installazione in corso + + + Procediamo alla pagina di Avvio + + + Download del pacchetto in corso... + + + Download completato + + + Installazione completata + + + Installazione dei prerequisiti... + + + Installazione requisiti del pacchetto... + + + Apri in Explorer + + + Apri sul Finder + + + Disinstalla + + + Controlla gli Aggiornamenti + + + Aggiorna + + + Aggiungi un Pacchetto + + + Aggiungi un pacchetto per iniziare! + + + Nome + + + Valore + + + Rimuovi + + + Dettagli + + + Stack di chiamate + + + Eccezione interna + + + Cerca... + + + OK + + + Riprova + + + Informazioni Versione Python + + + Riavvia + + + Conferma Cancellazione + + + Ciò eliminerà la cartella del pacchetto e tutto il suo contenuto, comprese eventuali immagini e file generati che potresti aver aggiunto. + + + Disinstallazione del pacchetto in corso... + + + Pacchetto Disinstallato + + + Impossibile eliminare alcuni file. Chiudi tutti i file aperti nella directory del pacchetto e riprova. + + + Tipo di pacchetto non valido + + + Aggiornamento {0} + + + Aggiornamento Completato + + + {0} è stato aggiornato all'ultima versione + + + Errore aggiornamento {0} + + + Aggiornamento fallito + + + Apri nel Browser + + + Errore installazione pacchetto + + + Branch + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 6b8b8ded289c71f71985287952f9c0a67c5a7e78 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 1 Oct 2023 18:46:00 -0400 Subject: [PATCH 435/474] Handle errors in interrupt prompt --- .../Base/InferenceGenerationViewModelBase.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs index c17bde51f..0b56b8c21 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -103,19 +103,22 @@ CancellationToken cancellationToken // Connect preview image handler client.PreviewImageReceived += OnPreviewImageReceived; + // Register to interrupt if user cancels + await using var promptInterrupt = cancellationToken.Register(() => + { + Logger.Info("Cancelling prompt"); + client + .InterruptPromptAsync(new CancellationTokenSource(5000).Token) + .SafeFireAndForget(ex => + { + Logger.Warn(ex, "Error while interrupting prompt"); + }); + }); + ComfyTask? promptTask = null; try { - // Register to interrupt if user cancels - cancellationToken.Register(() => - { - Logger.Info("Cancelling prompt"); - client - .InterruptPromptAsync(new CancellationTokenSource(5000).Token) - .SafeFireAndForget(); - }); - try { promptTask = await client.QueuePromptAsync(nodes, cancellationToken); @@ -140,6 +143,9 @@ CancellationToken cancellationToken cancellationToken ); + // Disable cancellation + await promptInterrupt.DisposeAsync(); + ImageGalleryCardViewModel.ImageSources.Clear(); if ( From 3b5691c90f7c03ab373a2221af8534e50764b1e0 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 1 Oct 2023 18:47:25 -0400 Subject: [PATCH 436/474] Allow spaces in extra network tokens --- StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json b/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json index d06b1efc8..fdf0964e3 100644 --- a/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json +++ b/StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json @@ -133,7 +133,7 @@ "name": "meta.structure.network.prompt", "patterns": [ { - "match": "(?<=\\<)([^,:\\<\\> ]+)(:)([^,:\\<\\> ]+)(:)([-+]?\\d+(?:\\.\\d+)?)", + "match": "(?<=\\<)([^,:\\<\\>]+)(:)([^,:\\<\\>]+)(:)([-+]?\\d+(?:\\.\\d+)?)", "captures": { "1": { "name": "meta.embedded.network.type.prompt" @@ -153,7 +153,7 @@ } }, { - "match": "(?<=\\<)([^,:\\<\\> ]+)(:)([^,:\\<\\> ]+)?", + "match": "(?<=\\<)([^,:\\<\\>]+)(:)([^,:\\<\\>]+)?", "captures": { "1": { "name": "meta.embedded.network.type.prompt" @@ -167,7 +167,7 @@ } }, { - "match": "(?<=\\<)([^,:\\<\\> ]+)", + "match": "(?<=\\<)([^,:\\<\\>]+)", "captures": { "1": { "name": "meta.embedded.network.type.prompt" From ed31e7646312c77b07e847ad287d3206505e968b Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 1 Oct 2023 19:21:21 -0400 Subject: [PATCH 437/474] Move registration dispose to finally --- .../ViewModels/Base/InferenceGenerationViewModelBase.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs index 0b56b8c21..6fec5b7f9 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -104,7 +104,7 @@ CancellationToken cancellationToken client.PreviewImageReceived += OnPreviewImageReceived; // Register to interrupt if user cancels - await using var promptInterrupt = cancellationToken.Register(() => + var promptInterrupt = cancellationToken.Register(() => { Logger.Info("Cancelling prompt"); client @@ -173,6 +173,7 @@ CancellationToken cancellationToken // Cleanup tasks promptTask?.Dispose(); + await promptInterrupt.DisposeAsync(); } } From af88003829c98a9a67a1110a8bf3a7c28973610b Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 1 Oct 2023 21:04:00 -0400 Subject: [PATCH 438/474] Check inferenceoutput link before making --- StabilityMatrix.Core/Models/Packages/ComfyUI.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index 7e1e4e58c..a7a3126eb 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -449,6 +449,18 @@ public async Task SetupInferenceOutputFolderLinks(DirectoryPath installDirectory var sharedInferenceDir = SettingsManager.ImagesInferenceDirectory; + if (sharedInferenceDir.IsSymbolicLink) + { + if (sharedInferenceDir.Info.ResolveLinkTarget(true) == sharedInferenceDir.Info) + { + // Already valid link, skip + return; + } + + // Otherwise delete so we don't have to move files + await sharedInferenceDir.DeleteAsync(false).ConfigureAwait(false); + } + await Task.Run(() => { Helper.SharedFolders.CreateLinkOrJunctionWithMove(sharedInferenceDir, inferenceDir); From b13f9e68e06b7006f5bf6033ed0aa752112bb839 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 1 Oct 2023 21:04:33 -0400 Subject: [PATCH 439/474] Fix directory check target --- StabilityMatrix.Core/Models/Packages/ComfyUI.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index a7a3126eb..6a7e5940a 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -451,7 +451,7 @@ public async Task SetupInferenceOutputFolderLinks(DirectoryPath installDirectory if (sharedInferenceDir.IsSymbolicLink) { - if (sharedInferenceDir.Info.ResolveLinkTarget(true) == sharedInferenceDir.Info) + if (inferenceDir.Info.ResolveLinkTarget(true) == sharedInferenceDir.Info) { // Already valid link, skip return; From 14bfb9ecdc6328ed963d616c14cac8887813dd32 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 01:17:32 -0400 Subject: [PATCH 440/474] Fix symlink logic --- StabilityMatrix.Core/Models/Packages/ComfyUI.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index 6a7e5940a..0e773cb38 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -449,9 +449,9 @@ public async Task SetupInferenceOutputFolderLinks(DirectoryPath installDirectory var sharedInferenceDir = SettingsManager.ImagesInferenceDirectory; - if (sharedInferenceDir.IsSymbolicLink) + if (inferenceDir.IsSymbolicLink) { - if (inferenceDir.Info.ResolveLinkTarget(true) == sharedInferenceDir.Info) + if (inferenceDir.Info.ResolveLinkTarget(true)?.FullName == sharedInferenceDir.FullPath) { // Already valid link, skip return; From dd39428c14a406623e9a4bb20b057ab22692e18f Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 01:17:50 -0400 Subject: [PATCH 441/474] Add progress reporting for running node changed events --- .../Base/InferenceGenerationViewModelBase.cs | 20 +++++++++++++ StabilityMatrix.Core/Inference/ComfyTask.cs | 29 ++++++++++++++----- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs index 6fec5b7f9..1b5ac8fc2 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -340,6 +340,26 @@ ComfyProgressUpdateEventArgs args }); } + /// + /// Handles the node executing updates received event from the websocket. + /// + protected virtual void OnRunningNodeChanged(object? sender, string? nodeName) + { + // Ignore if regular progress updates started + if (sender is not ComfyTask { LastProgressUpdate: null }) + { + return; + } + + Dispatcher.UIThread.Post(() => + { + OutputProgress.IsIndeterminate = true; + OutputProgress.Value = 100; + OutputProgress.Maximum = 100; + OutputProgress.Text = nodeName; + }); + } + public class ImageGenerationEventArgs : EventArgs { public required ComfyClient Client { get; init; } diff --git a/StabilityMatrix.Core/Inference/ComfyTask.cs b/StabilityMatrix.Core/Inference/ComfyTask.cs index 0fbd613eb..95bfcb7cb 100644 --- a/StabilityMatrix.Core/Inference/ComfyTask.cs +++ b/StabilityMatrix.Core/Inference/ComfyTask.cs @@ -1,4 +1,4 @@ -using System.Reactive; +using System.ComponentModel; using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; namespace StabilityMatrix.Core.Inference; @@ -6,24 +6,39 @@ namespace StabilityMatrix.Core.Inference; public class ComfyTask : TaskCompletionSource, IDisposable { public string Id { get; set; } - - public string? RunningNode { get; set; } + + private string? runningNode; + public string? RunningNode + { + get => runningNode; + set + { + runningNode = value; + RunningNodeChanged?.Invoke(this, value); + } + } + + public ComfyProgressUpdateEventArgs? LastProgressUpdate { get; private set; } public EventHandler? ProgressUpdate; - + + public event EventHandler? RunningNodeChanged; + public ComfyTask(string id) { Id = id; } - + /// /// Handler for progress updates /// public void OnProgressUpdate(ComfyWebSocketProgressData update) { - ProgressUpdate?.Invoke(this, new ComfyProgressUpdateEventArgs(update.Value, update.Max, Id, RunningNode)); + var args = new ComfyProgressUpdateEventArgs(update.Value, update.Max, Id, RunningNode); + ProgressUpdate?.Invoke(this, args); + LastProgressUpdate = args; } - + /// public void Dispose() { From b2f03299bd77ae38814cb538913efe0f031d7c54 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 01:26:26 -0400 Subject: [PATCH 442/474] Actually subscribe the event handler --- .../ViewModels/Base/InferenceGenerationViewModelBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs index 1b5ac8fc2..f32a93b8b 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -132,6 +132,7 @@ CancellationToken cancellationToken // Register progress handler promptTask.ProgressUpdate += OnProgressUpdateReceived; + promptTask.RunningNodeChanged += OnRunningNodeChanged; // Wait for prompt to finish await promptTask.Task.WaitAsync(cancellationToken); From e68a8512efb0818c816ad946881f66baf66f38ad Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 01:28:20 -0400 Subject: [PATCH 443/474] Add indeterminate bindings to progress bars --- .../Views/Inference/InferenceImageUpscaleView.axaml | 1 + .../Views/Inference/InferenceTextToImageView.axaml | 1 + 2 files changed, 2 insertions(+) diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml index c4e460ba7..5e63c01ed 100644 --- a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml +++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageUpscaleView.axaml @@ -148,6 +148,7 @@ Spacing="4"> From 48b81afc6816fb0ce137ab141a409de3aa5cb884 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 01:59:38 -0400 Subject: [PATCH 444/474] Fix interrupt handler disposed too early --- .../Base/InferenceGenerationViewModelBase.cs | 18 ++++++++++++++---- .../ViewModels/Base/ProgressViewModel.cs | 11 +++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs index f32a93b8b..7ee884e25 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -132,7 +132,19 @@ CancellationToken cancellationToken // Register progress handler promptTask.ProgressUpdate += OnProgressUpdateReceived; - promptTask.RunningNodeChanged += OnRunningNodeChanged; + + // Delay attaching running node change handler to not show indeterminate progress + // if progress updates are received before the prompt starts + Task.Run( + async () => + { + await Task.Delay(200, cancellationToken); + // ReSharper disable once AccessToDisposedClosure + promptTask.RunningNodeChanged += OnRunningNodeChanged; + }, + cancellationToken + ) + .SafeFireAndForget(); // Wait for prompt to finish await promptTask.Task.WaitAsync(cancellationToken); @@ -166,15 +178,13 @@ CancellationToken cancellationToken client.PreviewImageReceived -= OnPreviewImageReceived; // Clear progress - OutputProgress.Value = 0; - OutputProgress.Text = ""; + OutputProgress.ClearProgress(); ImageGalleryCardViewModel.PreviewImage?.Dispose(); ImageGalleryCardViewModel.PreviewImage = null; ImageGalleryCardViewModel.IsPreviewOverlayEnabled = false; // Cleanup tasks promptTask?.Dispose(); - await promptInterrupt.DisposeAsync(); } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/ProgressViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Base/ProgressViewModel.cs index cc0466e38..40375dfaf 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/ProgressViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/ProgressViewModel.cs @@ -12,16 +12,23 @@ public partial class ProgressViewModel : ViewModelBase [ObservableProperty] private string? description; - + [ObservableProperty, NotifyPropertyChangedFor(nameof(IsProgressVisible))] private double value; [ObservableProperty] private double maximum = 100; - + [ObservableProperty, NotifyPropertyChangedFor(nameof(IsProgressVisible))] private bool isIndeterminate; public virtual bool IsProgressVisible => Value > 0 || IsIndeterminate; public virtual bool IsTextVisible => !string.IsNullOrWhiteSpace(Text); + + public void ClearProgress() + { + Value = 0; + Text = null; + IsIndeterminate = false; + } } From 026e9bd19ab85999e6719e1dc5152d80e6e01958 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 16:13:14 -0400 Subject: [PATCH 445/474] Fix shared folder link ending early --- StabilityMatrix.Core/Helper/SharedFolders.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Core/Helper/SharedFolders.cs b/StabilityMatrix.Core/Helper/SharedFolders.cs index e74fd7203..c68ccc0a5 100644 --- a/StabilityMatrix.Core/Helper/SharedFolders.cs +++ b/StabilityMatrix.Core/Helper/SharedFolders.cs @@ -174,7 +174,7 @@ DirectoryPath installDirectory Logger.Info( $"Skipped updating matching folder link ({destinationDir} -> ({sourceDir})" ); - return; + continue; } // Otherwise delete the link From 626e3436ef001fdae9375ed11e3c626a3bfcdf41 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 16:33:17 -0400 Subject: [PATCH 446/474] Formatting cleanup --- StabilityMatrix.Core/Helper/FileTransfers.cs | 116 +++++++++++++------ StabilityMatrix.Core/Helper/SharedFolders.cs | 7 +- 2 files changed, 83 insertions(+), 40 deletions(-) diff --git a/StabilityMatrix.Core/Helper/FileTransfers.cs b/StabilityMatrix.Core/Helper/FileTransfers.cs index b233e5d8d..8316c07f5 100644 --- a/StabilityMatrix.Core/Helper/FileTransfers.cs +++ b/StabilityMatrix.Core/Helper/FileTransfers.cs @@ -11,21 +11,22 @@ namespace StabilityMatrix.Core.Helper; public static class FileTransfers { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - + /// /// Determines suitable buffer size based on stream length. /// /// /// - public static ulong GetBufferSize(ulong totalBytes) => totalBytes switch - { - < Size.MiB => 8 * Size.KiB, - < 100 * Size.MiB => 16 * Size.KiB, - < 500 * Size.MiB => Size.MiB, - < Size.GiB => 16 * Size.MiB, - _ => 32 * Size.MiB - }; - + public static ulong GetBufferSize(ulong totalBytes) => + totalBytes switch + { + < Size.MiB => 8 * Size.KiB, + < 100 * Size.MiB => 16 * Size.KiB, + < 500 * Size.MiB => Size.MiB, + < Size.GiB => 16 * Size.MiB, + _ => 32 * Size.MiB + }; + /// /// Copy all files and subfolders using a dictionary of source and destination file paths. /// Non-existing directories within the paths will be created. @@ -42,40 +43,73 @@ public static class FileTransfers /// /// Optional (total) progress. /// - public static async Task CopyFiles(Dictionary files, IProgress? fileProgress = default, IProgress? totalProgress = default) + public static async Task CopyFiles( + Dictionary files, + IProgress? fileProgress = default, + IProgress? totalProgress = default + ) { var totalFiles = files.Count; var completedFiles = 0; var totalSize = Convert.ToUInt64(files.Keys.Select(x => new FileInfo(x).Length).Sum()); var totalRead = 0ul; - foreach(var (sourcePath, destPath) in files) + foreach (var (sourcePath, destPath) in files) { var totalReadForFile = 0ul; - await using var outStream = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.Read); - await using var inStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read); - var fileSize = (ulong) inStream.Length; + await using var outStream = new FileStream( + destPath, + FileMode.Create, + FileAccess.Write, + FileShare.Read + ); + await using var inStream = new FileStream( + sourcePath, + FileMode.Open, + FileAccess.Read, + FileShare.Read + ); + var fileSize = (ulong)inStream.Length; var fileName = Path.GetFileName(sourcePath); completedFiles++; - await CopyStream(inStream , outStream, fileReadBytes => - { - var lastRead = totalReadForFile; - totalReadForFile = Convert.ToUInt64(fileReadBytes); - totalRead += totalReadForFile - lastRead; - fileProgress?.Report(new ProgressReport(totalReadForFile, fileSize, fileName, $"{completedFiles}/{totalFiles}")); - totalProgress?.Report(new ProgressReport(totalRead, totalSize, fileName, $"{completedFiles}/{totalFiles}")); - } ); + await CopyStream( + inStream, + outStream, + fileReadBytes => + { + var lastRead = totalReadForFile; + totalReadForFile = Convert.ToUInt64(fileReadBytes); + totalRead += totalReadForFile - lastRead; + fileProgress?.Report( + new ProgressReport( + totalReadForFile, + fileSize, + fileName, + $"{completedFiles}/{totalFiles}" + ) + ); + totalProgress?.Report( + new ProgressReport( + totalRead, + totalSize, + fileName, + $"{completedFiles}/{totalFiles}" + ) + ); + } + ) + .ConfigureAwait(false); } } private static async Task CopyStream(Stream from, Stream to, Action progress) { var shared = ArrayPool.Shared; - var bufferSize = (int) GetBufferSize((ulong) from.Length); + var bufferSize = (int)GetBufferSize((ulong)from.Length); var buffer = shared.Rent(bufferSize); var totalRead = 0L; - + try { while (totalRead < from.Length) @@ -104,27 +138,33 @@ public static async Task MoveAllFilesAndDirectories( DirectoryPath sourceDir, DirectoryPath destinationDir, bool overwrite = false, - bool overwriteIfHashMatches = false) + bool overwriteIfHashMatches = false + ) { // Create the destination directory if it doesn't exist if (!destinationDir.Exists) { destinationDir.Create(); } - + // First move files await MoveAllFiles(sourceDir, destinationDir, overwrite, overwriteIfHashMatches); - + // Then move directories foreach (var subDir in sourceDir.Info.EnumerateDirectories()) { var destinationSubDir = destinationDir.JoinDir(subDir.Name); // Recursively move sub directories - await MoveAllFilesAndDirectories(subDir, destinationSubDir, - overwrite, overwriteIfHashMatches); + await MoveAllFilesAndDirectories( + subDir, + destinationSubDir, + overwrite, + overwriteIfHashMatches + ) + .ConfigureAwait(false); } } - + /// /// Move all files within the source directory to the destination directory. /// If the destination contains a file with the same name, we'll check if the hashes match. @@ -134,10 +174,11 @@ await MoveAllFilesAndDirectories(subDir, destinationSubDir, /// If moving files results in name collision with different hashes. /// public static async Task MoveAllFiles( - DirectoryPath sourceDir, + DirectoryPath sourceDir, DirectoryPath destinationDir, bool overwrite = false, - bool overwriteIfHashMatches = false) + bool overwriteIfHashMatches = false + ) { foreach (var file in sourceDir.Info.EnumerateFiles()) { @@ -150,7 +191,7 @@ public static async Task MoveAllFiles( { destinationFile.Delete(); } - + if (overwriteIfHashMatches) { // Check if files hashes are the same @@ -160,13 +201,14 @@ public static async Task MoveAllFiles( if (sourceHash == destinationHash) { Logger.Info( - $"Deleted source file {file.Name} as it already exists in {destinationDir}." + - $" Matching Blake3 hash: {sourceHash}"); + $"Deleted source file {file.Name} as it already exists in {destinationDir}." + + $" Matching Blake3 hash: {sourceHash}" + ); sourceFile.Delete(); continue; } } - + throw new FileTransferExistsException(sourceFile, destinationFile); } // Move the file diff --git a/StabilityMatrix.Core/Helper/SharedFolders.cs b/StabilityMatrix.Core/Helper/SharedFolders.cs index c68ccc0a5..5cc494462 100644 --- a/StabilityMatrix.Core/Helper/SharedFolders.cs +++ b/StabilityMatrix.Core/Helper/SharedFolders.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using NLog; +using NLog; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models; @@ -54,7 +53,8 @@ DirectoryPath destinationDir Logger.Info($"Creating junction source {sourceDir}"); sourceDir.Create(); } - // Delete the destination folder if it exists + + // Destination folder exists, move files to source then delete if (destinationDir.Exists) { // Copy all files from destination to source @@ -76,6 +76,7 @@ DirectoryPath destinationDir Logger.Info($"Deleting junction target {destinationDir}"); destinationDir.Delete(true); } + Logger.Info($"Creating junction link from {sourceDir} to {destinationDir}"); CreateLinkOrJunction(destinationDir, sourceDir, true); } From 98b50be62dae5233730de86cba94fff7def8cf47 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 16:38:38 -0400 Subject: [PATCH 447/474] Fix progress total captured variable --- StabilityMatrix.Core/Helper/FileTransfers.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Core/Helper/FileTransfers.cs b/StabilityMatrix.Core/Helper/FileTransfers.cs index 8316c07f5..c8a4fdc58 100644 --- a/StabilityMatrix.Core/Helper/FileTransfers.cs +++ b/StabilityMatrix.Core/Helper/FileTransfers.cs @@ -70,9 +70,12 @@ public static async Task CopyFiles( FileAccess.Read, FileShare.Read ); + var fileSize = (ulong)inStream.Length; var fileName = Path.GetFileName(sourcePath); completedFiles++; + var currentCompletedFiles = completedFiles; + await CopyStream( inStream, outStream, @@ -86,7 +89,7 @@ await CopyStream( totalReadForFile, fileSize, fileName, - $"{completedFiles}/{totalFiles}" + $"{currentCompletedFiles}/{totalFiles}" ) ); totalProgress?.Report( @@ -94,7 +97,7 @@ await CopyStream( totalRead, totalSize, fileName, - $"{completedFiles}/{totalFiles}" + $"{currentCompletedFiles}/{totalFiles}" ) ); } From 3ddadbb8f40e37dd3bd6a61135031791b9b3ba1d Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 17:47:19 -0400 Subject: [PATCH 448/474] Refactor nullability --- StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs | 4 ++-- StabilityMatrix.Core/Services/DownloadService.cs | 5 +++-- StabilityMatrix.Core/Services/IDownloadService.cs | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs b/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs index 727c9c902..b1e08d8b1 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockDownloadService.cs @@ -43,8 +43,8 @@ public Task GetFileSizeAsync( return Task.FromResult(0L); } - public Task GetImageStreamFromUrl(string url) + public Task GetImageStreamFromUrl(string url) { - return Task.FromResult(new MemoryStream(new byte[24]) as Stream); + return Task.FromResult(new MemoryStream(new byte[24]) as Stream)!; } } diff --git a/StabilityMatrix.Core/Services/DownloadService.cs b/StabilityMatrix.Core/Services/DownloadService.cs index 9d0989d7f..7e0be15a3 100644 --- a/StabilityMatrix.Core/Services/DownloadService.cs +++ b/StabilityMatrix.Core/Services/DownloadService.cs @@ -256,7 +256,7 @@ var delay in Backoff.DecorrelatedJitterBackoffV2( return contentLength; } - public async Task GetImageStreamFromUrl(string url) + public async Task GetImageStreamFromUrl(string url) { using var client = httpClientFactory.CreateClient(); client.Timeout = TimeSpan.FromSeconds(10); @@ -270,7 +270,8 @@ public async Task GetImageStreamFromUrl(string url) } catch (Exception e) { - return default; + logger.LogError(e, "Failed to get image stream from url {Url}", url); + return null; } } } diff --git a/StabilityMatrix.Core/Services/IDownloadService.cs b/StabilityMatrix.Core/Services/IDownloadService.cs index 40ee0d219..883b1542a 100644 --- a/StabilityMatrix.Core/Services/IDownloadService.cs +++ b/StabilityMatrix.Core/Services/IDownloadService.cs @@ -27,5 +27,5 @@ Task GetFileSizeAsync( CancellationToken cancellationToken = default ); - Task GetImageStreamFromUrl(string url); + Task GetImageStreamFromUrl(string url); } From 50a91040fe60e156666ef92ba91bb830748e3ffd Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 17:50:22 -0400 Subject: [PATCH 449/474] Simplify and improve safety of shared folder links --- StabilityMatrix.Core/Helper/FileTransfers.cs | 30 +-- StabilityMatrix.Core/Helper/SharedFolders.cs | 181 ++++++------------ .../Models/Packages/BaseGitPackage.cs | 16 +- .../Models/Packages/ComfyUI.cs | 8 +- .../Models/SharedFoldersTests.cs | 87 ++++++--- 5 files changed, 143 insertions(+), 179 deletions(-) diff --git a/StabilityMatrix.Core/Helper/FileTransfers.cs b/StabilityMatrix.Core/Helper/FileTransfers.cs index c8a4fdc58..69cb05132 100644 --- a/StabilityMatrix.Core/Helper/FileTransfers.cs +++ b/StabilityMatrix.Core/Helper/FileTransfers.cs @@ -117,8 +117,9 @@ private static async Task CopyStream(Stream from, Stream to, Action progre { while (totalRead < from.Length) { - var read = await from.ReadAsync(buffer.AsMemory(0, bufferSize)); - await to.WriteAsync(buffer.AsMemory(0, read)); + var read = await from.ReadAsync(buffer.AsMemory(0, bufferSize)) + .ConfigureAwait(false); + await to.WriteAsync(buffer.AsMemory(0, read)).ConfigureAwait(false); totalRead += read; progress(totalRead); } @@ -151,7 +152,8 @@ public static async Task MoveAllFilesAndDirectories( } // First move files - await MoveAllFiles(sourceDir, destinationDir, overwrite, overwriteIfHashMatches); + await MoveAllFiles(sourceDir, destinationDir, overwrite, overwriteIfHashMatches) + .ConfigureAwait(false); // Then move directories foreach (var subDir in sourceDir.Info.EnumerateDirectories()) @@ -190,16 +192,15 @@ public static async Task MoveAllFiles( if (destinationFile.Exists) { - if (overwrite) - { - destinationFile.Delete(); - } - if (overwriteIfHashMatches) { // Check if files hashes are the same - var sourceHash = await FileHash.GetBlake3Async(sourceFile); - var destinationHash = await FileHash.GetBlake3Async(destinationFile); + var sourceHash = await FileHash + .GetBlake3Async(sourceFile) + .ConfigureAwait(false); + var destinationHash = await FileHash + .GetBlake3Async(destinationFile) + .ConfigureAwait(false); // For same hash, just delete original file if (sourceHash == destinationHash) { @@ -208,14 +209,15 @@ public static async Task MoveAllFiles( + $" Matching Blake3 hash: {sourceHash}" ); sourceFile.Delete(); - continue; } } - - throw new FileTransferExistsException(sourceFile, destinationFile); + else if (!overwrite) + { + throw new FileTransferExistsException(sourceFile, destinationFile); + } } // Move the file - await sourceFile.MoveToAsync(destinationFile); + await sourceFile.MoveToAsync(destinationFile).ConfigureAwait(false); } } } diff --git a/StabilityMatrix.Core/Helper/SharedFolders.cs b/StabilityMatrix.Core/Helper/SharedFolders.cs index 5cc494462..aeb67c29b 100644 --- a/StabilityMatrix.Core/Helper/SharedFolders.cs +++ b/StabilityMatrix.Core/Helper/SharedFolders.cs @@ -21,8 +21,10 @@ public SharedFolders(ISettingsManager settingsManager, IPackageFactory packageFa this.packageFactory = packageFactory; } - // Platform redirect for junctions / symlinks - public static void CreateLinkOrJunction(string junctionDir, string targetDir, bool overwrite) + /// + /// Platform redirect for junctions / symlinks + /// + private static void CreateLinkOrJunction(string junctionDir, string targetDir, bool overwrite) { if (Compat.IsWindows) { @@ -37,14 +39,16 @@ public static void CreateLinkOrJunction(string junctionDir, string targetDir, bo } /// - /// Creates a junction link from the source to the destination. + /// Creates or updates junction link from the source to the destination. /// Moves destination files to source if they exist. /// /// Shared source (i.e. "Models/") /// Destination (i.e. "webui/models/lora") - public static void CreateLinkOrJunctionWithMove( + /// Whether to overwrite the destination if it exists + public static async Task CreateOrUpdateLink( DirectoryPath sourceDir, - DirectoryPath destinationDir + DirectoryPath destinationDir, + bool overwrite = false ) { // Create source folder if it doesn't exist @@ -54,102 +58,71 @@ DirectoryPath destinationDir sourceDir.Create(); } - // Destination folder exists, move files to source then delete if (destinationDir.Exists) { - // Copy all files from destination to source - Logger.Info($"Copying files from {destinationDir} to {sourceDir}"); - foreach (var file in destinationDir.Info.EnumerateFiles()) + // Existing dest is a link + if (destinationDir.IsSymbolicLink) { - var sourceFile = sourceDir + file; - var destinationFile = destinationDir + file; - // Skip name collisions - if (File.Exists(sourceFile)) + // If link is already the same, just skip + if (destinationDir.Info.LinkTarget == sourceDir) { - Logger.Warn( - $"Skipping file {file.FullName} because it already exists in {sourceDir}" + Logger.Info( + $"Skipped updating matching folder link ({destinationDir} -> ({sourceDir})" ); - continue; + return; } - destinationFile.Info.MoveTo(sourceFile); - } - Logger.Info($"Deleting junction target {destinationDir}"); - destinationDir.Delete(true); - } - Logger.Info($"Creating junction link from {sourceDir} to {destinationDir}"); - CreateLinkOrJunction(destinationDir, sourceDir, true); - } - - public static void SetupLinks( - Dictionary> definitions, - DirectoryPath modelsDirectory, - DirectoryPath installDirectory - ) - { - foreach (var (folderType, relativePaths) in definitions) - { - foreach (var relativePath in relativePaths) + // Otherwise delete the link + Logger.Info($"Deleting existing junction at target {destinationDir}"); + await destinationDir.DeleteAsync(false).ConfigureAwait(false); + } + else { - var sourceDir = new DirectoryPath(modelsDirectory, folderType.GetStringValue()); - var destinationDir = new DirectoryPath(installDirectory, relativePath); - // Create source folder if it doesn't exist - if (!sourceDir.Exists) - { - Logger.Info($"Creating junction source {sourceDir}"); - sourceDir.Create(); - } - // Delete the destination folder if it exists - if (destinationDir.Exists) + // Move all files if not empty + if (destinationDir.Info.EnumerateFileSystemInfos().Any()) { - // Copy all files from destination to source - Logger.Info($"Copying files from {destinationDir} to {sourceDir}"); - foreach (var file in destinationDir.Info.EnumerateFiles()) - { - var sourceFile = sourceDir + file; - var destinationFile = destinationDir + file; - // Skip name collisions - if (File.Exists(sourceFile)) - { - Logger.Warn( - $"Skipping file {file.FullName} because it already exists in {sourceDir}" - ); - continue; - } - destinationFile.Info.MoveTo(sourceFile); - } - Logger.Info($"Deleting junction target {destinationDir}"); - destinationDir.Delete(true); + Logger.Info($"Moving files from {destinationDir} to {sourceDir}"); + await FileTransfers + .MoveAllFilesAndDirectories( + destinationDir, + sourceDir, + overwriteIfHashMatches: true, + overwrite: overwrite + ) + .ConfigureAwait(false); } - Logger.Info($"Creating junction link from {sourceDir} to {destinationDir}"); - CreateLinkOrJunction(destinationDir, sourceDir, true); + + Logger.Info($"Deleting existing empty folder at target {destinationDir}"); + await destinationDir.DeleteAsync(false).ConfigureAwait(false); } } + + Logger.Info($"Updating junction link from {sourceDir} to {destinationDir}"); + CreateLinkOrJunction(destinationDir, sourceDir, true); } + [Obsolete("Use static methods instead")] public void SetupLinksForPackage(BasePackage basePackage, DirectoryPath installDirectory) { var modelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory); var sharedFolders = basePackage.SharedFolders; if (sharedFolders == null) return; - SetupLinks(sharedFolders, modelsDirectory, installDirectory); + UpdateLinksForPackage(sharedFolders, modelsDirectory, installDirectory) + .GetAwaiter() + .GetResult(); } /// - /// Deletes junction links and remakes them. Unlike SetupLinksForPackage, - /// this will not copy files from the destination to the source. + /// 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( - BasePackage basePackage, + Dictionary> sharedFolders, DirectoryPath modelsDirectory, DirectoryPath installDirectory ) { - var sharedFolders = basePackage.SharedFolders; - if (sharedFolders is null) - return; - foreach (var (folderType, relativePaths) in sharedFolders) { foreach (var relativePath in relativePaths) @@ -157,53 +130,7 @@ DirectoryPath installDirectory var sourceDir = new DirectoryPath(modelsDirectory, folderType.GetStringValue()); var destinationDir = installDirectory.JoinDir(relativePath); - // Create source folder if it doesn't exist - if (!sourceDir.Exists) - { - Logger.Info($"Creating junction source {sourceDir}"); - sourceDir.Create(); - } - - if (destinationDir.Exists) - { - // Existing dest is a link - if (destinationDir.IsSymbolicLink) - { - // If link is already the same, just skip - if (destinationDir.Info.LinkTarget == sourceDir) - { - Logger.Info( - $"Skipped updating matching folder link ({destinationDir} -> ({sourceDir})" - ); - continue; - } - - // Otherwise delete the link - Logger.Info($"Deleting existing junction at target {destinationDir}"); - await destinationDir.DeleteAsync(false).ConfigureAwait(false); - } - else - { - // Move all files if not empty - if (destinationDir.Info.EnumerateFileSystemInfos().Any()) - { - Logger.Info($"Moving files from {destinationDir} to {sourceDir}"); - await FileTransfers - .MoveAllFilesAndDirectories( - destinationDir, - sourceDir, - overwriteIfHashMatches: true - ) - .ConfigureAwait(false); - } - - Logger.Info($"Deleting existing empty folder at target {destinationDir}"); - await destinationDir.DeleteAsync(false).ConfigureAwait(false); - } - } - - Logger.Info($"Updating junction link from {sourceDir} to {destinationDir}"); - CreateLinkOrJunction(destinationDir, sourceDir, true); + await CreateOrUpdateLink(sourceDir, destinationDir).ConfigureAwait(false); } } } @@ -236,16 +163,16 @@ public void RemoveLinksForAllPackages() var packages = settingsManager.Settings.InstalledPackages; foreach (var package in packages) { - if (package.PackageName == null) - continue; - var basePackage = packageFactory[package.PackageName]; - if (basePackage == null) - continue; - if (package.LibraryPath == null) - continue; - try { + if ( + packageFactory.FindPackageByName(package.PackageName) is not { } basePackage + || package.FullPath is null + ) + { + continue; + } + var sharedFolderMethod = package.PreferredSharedFolderMethod ?? basePackage.RecommendedSharedFolderMethod; diff --git a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs index 35c5a9158..dab562cc7 100644 --- a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs @@ -338,7 +338,7 @@ SharedFolderMethod sharedFolderMethod { if (sharedFolderMethod == SharedFolderMethod.Symlink && SharedFolders is { } folders) { - StabilityMatrix.Core.Helper.SharedFolders.SetupLinks( + return StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage( folders, SettingsManager.ModelsDirectory, installDirectory @@ -348,17 +348,21 @@ SharedFolderMethod sharedFolderMethod return Task.CompletedTask; } - public override async Task UpdateModelFolders( + public override Task UpdateModelFolders( DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod ) { - if (SharedFolders is not null && sharedFolderMethod == SharedFolderMethod.Symlink) + if (sharedFolderMethod == SharedFolderMethod.Symlink && SharedFolders is { } sharedFolders) { - await StabilityMatrix.Core.Helper.SharedFolders - .UpdateLinksForPackage(this, SettingsManager.ModelsDirectory, installDirectory) - .ConfigureAwait(false); + return StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage( + sharedFolders, + SettingsManager.ModelsDirectory, + installDirectory + ); } + + return Task.CompletedTask; } public override Task RemoveModelFolderLinks( diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index 0e773cb38..38b1bdf97 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Drawing; using System.Text.RegularExpressions; using NLog; using StabilityMatrix.Core.Helper; @@ -12,7 +11,6 @@ using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -using YamlDotNet.Serialization.TypeInspectors; namespace StabilityMatrix.Core.Models.Packages; @@ -461,10 +459,8 @@ public async Task SetupInferenceOutputFolderLinks(DirectoryPath installDirectory await sharedInferenceDir.DeleteAsync(false).ConfigureAwait(false); } - await Task.Run(() => - { - Helper.SharedFolders.CreateLinkOrJunctionWithMove(sharedInferenceDir, inferenceDir); - }) + await Helper.SharedFolders + .CreateOrUpdateLink(sharedInferenceDir, inferenceDir) .ConfigureAwait(false); } } diff --git a/StabilityMatrix.Tests/Models/SharedFoldersTests.cs b/StabilityMatrix.Tests/Models/SharedFoldersTests.cs index d5eec3c4d..8b44241c0 100644 --- a/StabilityMatrix.Tests/Models/SharedFoldersTests.cs +++ b/StabilityMatrix.Tests/Models/SharedFoldersTests.cs @@ -10,13 +10,14 @@ public class SharedFoldersTests private string tempFolder = string.Empty; private string TempModelsFolder => Path.Combine(tempFolder, "models"); private string TempPackageFolder => Path.Combine(tempFolder, "package"); - - private readonly Dictionary sampleDefinitions = new() - { - [SharedFolderType.StableDiffusion] = "models/Stable-diffusion", - [SharedFolderType.ESRGAN] = "models/ESRGAN", - [SharedFolderType.TextualInversion] = "embeddings", - }; + + private readonly Dictionary sampleDefinitions = + new() + { + [SharedFolderType.StableDiffusion] = "models/Stable-diffusion", + [SharedFolderType.ESRGAN] = "models/ESRGAN", + [SharedFolderType.TextualInversion] = "embeddings", + }; [TestInitialize] public void Initialize() @@ -29,7 +30,8 @@ public void Initialize() [TestCleanup] public void Cleanup() { - if (string.IsNullOrEmpty(tempFolder)) return; + if (string.IsNullOrEmpty(tempFolder)) + return; TempFiles.DeleteDirectory(tempFolder); } @@ -37,29 +39,42 @@ private void CreateSampleJunctions() { var definitions = new Dictionary> { - [SharedFolderType.StableDiffusion] = new[] {"models/Stable-diffusion"}, - [SharedFolderType.ESRGAN] = new[] {"models/ESRGAN"}, - [SharedFolderType.TextualInversion] = new[] {"embeddings"}, + [SharedFolderType.StableDiffusion] = new[] { "models/Stable-diffusion" }, + [SharedFolderType.ESRGAN] = new[] { "models/ESRGAN" }, + [SharedFolderType.TextualInversion] = new[] { "embeddings" }, }; - SharedFolders.SetupLinks(definitions, TempModelsFolder, TempPackageFolder); + SharedFolders + .UpdateLinksForPackage(definitions, TempModelsFolder, TempPackageFolder) + .GetAwaiter() + .GetResult(); } [TestMethod] public void SetupLinks_CreatesJunctions() { CreateSampleJunctions(); - + // Check model folders foreach (var (folderType, relativePath) in sampleDefinitions) { var packagePath = Path.Combine(TempPackageFolder, relativePath); var modelFolder = Path.Combine(TempModelsFolder, folderType.GetStringValue()); // Should exist and be a junction - Assert.IsTrue(Directory.Exists(packagePath), $"Package folder {packagePath} does not exist."); + Assert.IsTrue( + Directory.Exists(packagePath), + $"Package folder {packagePath} does not exist." + ); var info = new DirectoryInfo(packagePath); - Assert.IsTrue(info.Attributes.HasFlag(FileAttributes.ReparsePoint), $"Package folder {packagePath} is not a junction."); + Assert.IsTrue( + info.Attributes.HasFlag(FileAttributes.ReparsePoint), + $"Package folder {packagePath} is not a junction." + ); // Check junction target should be in models folder - Assert.AreEqual(modelFolder, info.LinkTarget, $"Package folder {packagePath} does not point to {modelFolder}."); + Assert.AreEqual( + modelFolder, + info.LinkTarget, + $"Package folder {packagePath} does not point to {modelFolder}." + ); } } @@ -67,21 +82,41 @@ public void SetupLinks_CreatesJunctions() public void SetupLinks_CanDeleteJunctions() { CreateSampleJunctions(); - - var modelFolder = Path.Combine(tempFolder, "models", SharedFolderType.StableDiffusion.GetStringValue()); - var packagePath = Path.Combine(tempFolder, "package", sampleDefinitions[SharedFolderType.StableDiffusion]); - + + var modelFolder = Path.Combine( + tempFolder, + "models", + SharedFolderType.StableDiffusion.GetStringValue() + ); + var packagePath = Path.Combine( + tempFolder, + "package", + sampleDefinitions[SharedFolderType.StableDiffusion] + ); + // Write a file to a model folder File.Create(Path.Combine(modelFolder, "AFile")).Close(); - Assert.IsTrue(File.Exists(Path.Combine(modelFolder, "AFile")), $"File should exist in {modelFolder}."); + Assert.IsTrue( + File.Exists(Path.Combine(modelFolder, "AFile")), + $"File should exist in {modelFolder}." + ); // Should exist in the package folder - Assert.IsTrue(File.Exists(Path.Combine(packagePath, "AFile")), $"File should exist in {packagePath}."); - + Assert.IsTrue( + File.Exists(Path.Combine(packagePath, "AFile")), + $"File should exist in {packagePath}." + ); + // Now delete the junction Directory.Delete(packagePath, false); - Assert.IsFalse(Directory.Exists(packagePath), $"Package folder {packagePath} should not exist."); - + Assert.IsFalse( + Directory.Exists(packagePath), + $"Package folder {packagePath} should not exist." + ); + // The file should still exist in the model folder - Assert.IsTrue(File.Exists(Path.Combine(modelFolder, "AFile")), $"File should exist in {modelFolder}."); + Assert.IsTrue( + File.Exists(Path.Combine(modelFolder, "AFile")), + $"File should exist in {modelFolder}." + ); } } From be46508296547c2710ccfb180c8c80a516470bb7 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 19:45:44 -0400 Subject: [PATCH 450/474] Update README images and translations --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0253f7054..44d214d36 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Multi-Platform Package Manager for Stable Diffusion - Automatically imports to the associated model folder depending on the model type - Downloads relevant metadata files and preview image -![header](https://github.com/LykosAI/StabilityMatrix/assets/13956642/a9c5f925-8561-49ba-855b-1b7bf57d7c0d) +![header](https://cdn.lykos.ai/static/sm-banner-rounded.webp) [![Release](https://img.shields.io/github/v/release/LykosAI/StabilityMatrix?label=Latest%20Release&link=https%3A%2F%2Fgithub.com%2FLykosAI%2FStabilityMatrix%2Freleases%2Flatest)][release] @@ -61,13 +61,16 @@ Multi-Platform Package Manager for Stable Diffusion ### Shared model directory for all your packages - Import local models by simple drag and drop -- Option to find CivitAI metadata and preview thumbnails for new local imports -- Toggle visibility of categories like LoRA, VAE, CLIP, etc. +- Option to automatically find CivitAI metadata and preview thumbnails for new local imports

+- Find connected metadata for existing models +

+ +

## Localization Stability Matrix is now available in the following languages, thanks to our community contributors: @@ -76,6 +79,8 @@ Stability Matrix is now available in the following languages, thanks to our comm - kgmkm_mkgm - 🇨🇳 中文(简体,繁体) - jimlovewine +- 🇮🇹 Italiano + - Marco Capelli If you would like to contribute a translation, please create an issue or contact us on Discord. Include an email where we'll send an invite to our [POEditor](https://poeditor.com/) project. From 717f722a899c0be8c939768480499ac295e3446c Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 21:31:19 -0400 Subject: [PATCH 451/474] Improve progress change behavior --- .../Base/InferenceGenerationViewModelBase.cs | 24 ++++++++++++++++--- StabilityMatrix.Core/Inference/ComfyClient.cs | 2 +- StabilityMatrix.Core/Inference/ComfyTask.cs | 16 +++++++++---- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs index 7ee884e25..2ddd96365 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json.Serialization; @@ -119,6 +120,8 @@ CancellationToken cancellationToken try { + var timer = Stopwatch.StartNew(); + try { promptTask = await client.QueuePromptAsync(nodes, cancellationToken); @@ -138,9 +141,13 @@ CancellationToken cancellationToken Task.Run( async () => { - await Task.Delay(200, cancellationToken); + var delayTime = 250 - (int)timer.ElapsedMilliseconds; + if (delayTime > 0) + { + await Task.Delay(delayTime, cancellationToken); + } // ReSharper disable once AccessToDisposedClosure - promptTask.RunningNodeChanged += OnRunningNodeChanged; + AttachRunningNodeChangedHandler(promptTask); }, cancellationToken ) @@ -351,13 +358,24 @@ ComfyProgressUpdateEventArgs args }); } + private void AttachRunningNodeChangedHandler(ComfyTask comfyTask) + { + // Do initial update + if (comfyTask.RunningNodesHistory.TryPeek(out var lastNode)) + { + OnRunningNodeChanged(comfyTask, lastNode); + } + + comfyTask.RunningNodeChanged += OnRunningNodeChanged; + } + /// /// Handles the node executing updates received event from the websocket. /// protected virtual void OnRunningNodeChanged(object? sender, string? nodeName) { // Ignore if regular progress updates started - if (sender is not ComfyTask { LastProgressUpdate: null }) + if (sender is not ComfyTask { HasProgressUpdateStarted: false }) { return; } diff --git a/StabilityMatrix.Core/Inference/ComfyClient.cs b/StabilityMatrix.Core/Inference/ComfyClient.cs index 15433b5d8..0dfbe741e 100644 --- a/StabilityMatrix.Core/Inference/ComfyClient.cs +++ b/StabilityMatrix.Core/Inference/ComfyClient.cs @@ -282,7 +282,7 @@ public async Task QueuePromptAsync( // Add task to dictionary and set it as the current task var task = new ComfyTask(result.PromptId); - PromptTasks[result.PromptId] = task; + PromptTasks.TryAdd(result.PromptId, task); currentPromptTask = task; return task; diff --git a/StabilityMatrix.Core/Inference/ComfyTask.cs b/StabilityMatrix.Core/Inference/ComfyTask.cs index 95bfcb7cb..04c0be213 100644 --- a/StabilityMatrix.Core/Inference/ComfyTask.cs +++ b/StabilityMatrix.Core/Inference/ComfyTask.cs @@ -1,11 +1,10 @@ -using System.ComponentModel; -using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; +using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData; namespace StabilityMatrix.Core.Inference; public class ComfyTask : TaskCompletionSource, IDisposable { - public string Id { get; set; } + public string Id { get; } private string? runningNode; public string? RunningNode @@ -15,11 +14,19 @@ public string? RunningNode { runningNode = value; RunningNodeChanged?.Invoke(this, value); + if (value != null) + { + RunningNodesHistory.Push(value); + } } } + public Stack RunningNodesHistory { get; } = new(); + public ComfyProgressUpdateEventArgs? LastProgressUpdate { get; private set; } + public bool HasProgressUpdateStarted => LastProgressUpdate != null; + public EventHandler? ProgressUpdate; public event EventHandler? RunningNodeChanged; @@ -34,7 +41,8 @@ public ComfyTask(string id) ///
public void OnProgressUpdate(ComfyWebSocketProgressData update) { - var args = new ComfyProgressUpdateEventArgs(update.Value, update.Max, Id, RunningNode); + RunningNodesHistory.TryPeek(out var lastNode); + var args = new ComfyProgressUpdateEventArgs(update.Value, update.Max, Id, lastNode); ProgressUpdate?.Invoke(this, args); LastProgressUpdate = args; } From d5b1d622f1e5fad9757d8040cd49c0d114f76b80 Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 22:01:15 -0400 Subject: [PATCH 452/474] Add auto download of default tag completion sources --- StabilityMatrix.Avalonia/Assets.cs | 19 ++++++-- .../DesignData/MockCompletionProvider.cs | 6 +++ .../TagCompletion/CompletionProvider.cs | 47 ++++++++++++++++++- .../TagCompletion/ICompletionProvider.cs | 5 ++ .../Services/InferenceClientManager.cs | 14 +++++- .../ViewModels/SettingsViewModel.cs | 4 +- .../Models/Settings/Settings.cs | 4 +- 7 files changed, 90 insertions(+), 9 deletions(-) diff --git a/StabilityMatrix.Avalonia/Assets.cs b/StabilityMatrix.Avalonia/Assets.cs index cf8a09319..db08322e1 100644 --- a/StabilityMatrix.Avalonia/Assets.cs +++ b/StabilityMatrix.Avalonia/Assets.cs @@ -32,7 +32,7 @@ internal static class Assets public static AvaloniaResource ThemeMatrixDarkJson => new("avares://StabilityMatrix.Avalonia/Assets/ThemeMatrixDark.json"); - private const UnixFileMode unix755 = + private const UnixFileMode Unix755 = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute @@ -54,14 +54,14 @@ internal static class Assets PlatformKind.Linux | PlatformKind.X64, new AvaloniaResource( "avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs", - unix755 + Unix755 ) ), ( PlatformKind.MacOS | PlatformKind.Arm, new AvaloniaResource( "avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz", - unix755 + Unix755 ) ) ); @@ -136,6 +136,19 @@ internal static class Assets ) ); + public static IReadOnlyList DefaultCompletionTags { get; } = + new[] + { + new RemoteResource( + new Uri("https://cdn.lykos.ai/tags/danbooru.csv"), + "b84a879f1d9c47bf4758d66542598faa565b1571122ae12e7b145da8e7a4c1c6" + ), + new RemoteResource( + new Uri("https://cdn.lykos.ai/tags/e621.csv"), + "ef7ea148ad865ad936d0c1ee57f0f83de723b43056c70b07fd67dbdbb89cae35" + ) + }; + public static Uri DiscordServerUrl { get; } = new("https://discord.com/invite/TUrgfECxHz"); public static Uri PatreonUrl { get; } = new("https://patreon.com/StabilityMatrix"); diff --git a/StabilityMatrix.Avalonia/DesignData/MockCompletionProvider.cs b/StabilityMatrix.Avalonia/DesignData/MockCompletionProvider.cs index d6702f2d4..1b22b1eb6 100644 --- a/StabilityMatrix.Avalonia/DesignData/MockCompletionProvider.cs +++ b/StabilityMatrix.Avalonia/DesignData/MockCompletionProvider.cs @@ -15,6 +15,12 @@ public class MockCompletionProvider : ICompletionProvider /// public Func? PrepareInsertionText { get; } = data => data.Text; + /// + public Task Setup() + { + return Task.CompletedTask; + } + /// public Task LoadFromFile(FilePath path, bool recreate = false) { diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs index cc9889dae..a2bd34dca 100644 --- a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs @@ -36,6 +36,7 @@ public partial class CompletionProvider : ICompletionProvider private readonly ISettingsManager settingsManager; private readonly INotificationService notificationService; private readonly IModelIndexService modelIndexService; + private readonly IDownloadService downloadService; private readonly AsyncLock loadLock = new(); private readonly Dictionary entries = new(); @@ -61,12 +62,14 @@ public CompletionType AvailableCompletionTypes public CompletionProvider( ISettingsManager settingsManager, INotificationService notificationService, - IModelIndexService modelIndexService + IModelIndexService modelIndexService, + IDownloadService downloadService ) { this.settingsManager = settingsManager; this.notificationService = notificationService; this.modelIndexService = modelIndexService; + this.downloadService = downloadService; PrepareInsertionText = PrepareInsertionText_Process; @@ -148,6 +151,48 @@ public void BackgroundLoadFromFile(FilePath path, bool recreate = false) ); } + /// + public async Task Setup() + { + var tagsDir = settingsManager.TagsDirectory; + tagsDir.Create(); + + // If tagsDir is empty and no selected, download defaults + if ( + !tagsDir.Info.EnumerateFiles().Any() + && settingsManager.Settings.TagCompletionCsv is null + ) + { + foreach (var remoteCsv in Assets.DefaultCompletionTags) + { + var fileName = remoteCsv.Url.Segments.Last(); + Logger.Info( + "Downloading default tag source {Name} [{Hash}]", + fileName, + remoteCsv.HashSha256[..7] + ); + await downloadService.DownloadToFileAsync( + remoteCsv.Url.ToString(), + tagsDir.JoinFile(fileName) + ); + } + + var defaultFile = tagsDir.JoinFile("danbooru.csv"); + if (!defaultFile.Exists) + { + Logger.Warn("Failed to download default tag source"); + return; + } + + // Set default file as selected + settingsManager.Settings.TagCompletionCsv = defaultFile.Name; + Logger.Debug("Tag completion source set to {Name}", defaultFile.Name); + + // Load default file + BackgroundLoadFromFile(defaultFile); + } + } + /// public async Task LoadFromFile(FilePath path, bool recreate = false) { diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/ICompletionProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/ICompletionProvider.cs index 3e091c948..2e7ed85af 100644 --- a/StabilityMatrix.Avalonia/Models/TagCompletion/ICompletionProvider.cs +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/ICompletionProvider.cs @@ -18,6 +18,11 @@ public interface ICompletionProvider ///
Func? PrepareInsertionText => null; + /// + /// Downloads default tags and selects one if required. + /// + Task Setup(); + /// /// Load the completion provider from a file. /// diff --git a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs index 439e815c9..3e71a8773 100644 --- a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging; using SkiaSharp; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Inference; @@ -32,6 +33,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient private readonly IApiFactory apiFactory; private readonly IModelIndexService modelIndexService; private readonly ISettingsManager settingsManager; + private readonly ICompletionProvider completionProvider; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsConnected), nameof(CanUserConnect))] @@ -86,13 +88,15 @@ public InferenceClientManager( ILogger logger, IApiFactory apiFactory, IModelIndexService modelIndexService, - ISettingsManager settingsManager + ISettingsManager settingsManager, + ICompletionProvider completionProvider ) { this.logger = logger; this.apiFactory = apiFactory; this.modelIndexService = modelIndexService; this.settingsManager = settingsManager; + this.completionProvider = completionProvider; modelsSource .Connect() @@ -355,6 +359,14 @@ public async Task ConnectAsync( throw new ArgumentException("Base package is not ComfyUI", nameof(packagePair)); } + // Setup completion provider + completionProvider + .Setup() + .SafeFireAndForget(ex => + { + logger.LogError(ex, "Error setting up completion provider"); + }); + // Setup image folder links await comfyPackage.SetupInferenceOutputFolderLinks( packagePair.InstalledPackage.FullPath diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index 6053e1e0a..27ec36c35 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -95,7 +95,7 @@ public partial class SettingsViewModel : PageViewModelBase // Inference UI section [ObservableProperty] - private bool isPromptCompletionEnabled; + private bool isPromptCompletionEnabled = true; [ObservableProperty] private IReadOnlyList availableTagCompletionCsvs = Array.Empty(); @@ -104,7 +104,7 @@ public partial class SettingsViewModel : PageViewModelBase private string? selectedTagCompletionCsv; [ObservableProperty] - private bool isCompletionRemoveUnderscoresEnabled; + private bool isCompletionRemoveUnderscoresEnabled = true; [ObservableProperty] private bool isImageViewerPixelGridEnabled = true; diff --git a/StabilityMatrix.Core/Models/Settings/Settings.cs b/StabilityMatrix.Core/Models/Settings/Settings.cs index 38f2d1b6f..cb01f14eb 100644 --- a/StabilityMatrix.Core/Models/Settings/Settings.cs +++ b/StabilityMatrix.Core/Models/Settings/Settings.cs @@ -58,7 +58,7 @@ public InstalledPackage? ActiveInstalledPackage /// /// Whether prompt auto completion is enabled /// - public bool IsPromptCompletionEnabled { get; set; } + public bool IsPromptCompletionEnabled { get; set; } = true; /// /// Relative path to the tag completion CSV file from 'LibraryDir/Tags' @@ -68,7 +68,7 @@ public InstalledPackage? ActiveInstalledPackage /// /// Whether to remove underscores from completions /// - public bool IsCompletionRemoveUnderscoresEnabled { get; set; } + public bool IsCompletionRemoveUnderscoresEnabled { get; set; } = true; /// /// Whether the Inference Image Viewer shows pixel grids at high zoom levels From 9ca11f872c50c980d14f3083122473acc875880f Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 22:10:02 -0400 Subject: [PATCH 453/474] Add completion setup on settings load --- .../Models/TagCompletion/CompletionProvider.cs | 5 ++++- StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs index a2bd34dca..1b4395a49 100644 --- a/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs +++ b/StabilityMatrix.Avalonia/Models/TagCompletion/CompletionProvider.cs @@ -160,7 +160,10 @@ public async Task Setup() // If tagsDir is empty and no selected, download defaults if ( !tagsDir.Info.EnumerateFiles().Any() - && settingsManager.Settings.TagCompletionCsv is null + && ( + settingsManager.Settings.TagCompletionCsv is null + || !tagsDir.JoinFile(settingsManager.Settings.TagCompletionCsv).Exists + ) ) { foreach (var remoteCsv in Assets.DefaultCompletionTags) diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs index 27ec36c35..eee148fa8 100644 --- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs @@ -11,6 +11,7 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using AsyncAwaitBestPractices; using Avalonia; using Avalonia.Controls.Notifications; using Avalonia.Controls.Primitives; @@ -215,9 +216,11 @@ IModelIndexService modelIndexService } /// - public override void OnLoaded() + public override async Task OnLoadedAsync() { - base.OnLoaded(); + await base.OnLoadedAsync(); + + await notificationService.TryAsync(completionProvider.Setup()); UpdateAvailableTagCompletionCsvs(); } From d568c837b25d070fb7323a8c90c29fba3c388a8c Mon Sep 17 00:00:00 2001 From: Ionite Date: Mon, 2 Oct 2023 22:31:43 -0400 Subject: [PATCH 454/474] Fix light theme in first time setup --- CHANGELOG.md | 1 + StabilityMatrix.Avalonia/App.axaml | 2 +- .../Views/FirstLaunchSetupWindow.axaml.cs | 10 ++++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea2bfab8a..5d626e2e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Fixed model index startup errors when `./Models` contains unknown custom folder names - Fixed ストップ button being cut off in Japanese translation - Fixed update progress freezing in some cases +- Fixed light theme being default in first time setup window ## v2.4.6 ### Added diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml index 76cfbbf8c..0258b36b2 100644 --- a/StabilityMatrix.Avalonia/App.axaml +++ b/StabilityMatrix.Avalonia/App.axaml @@ -4,7 +4,7 @@ xmlns:local="using:StabilityMatrix.Avalonia" xmlns:idcr="using:Dock.Avalonia.Controls.Recycling" xmlns:styling="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia" - RequestedThemeVariant="Default"> + RequestedThemeVariant="Dark"> diff --git a/StabilityMatrix.Avalonia/Views/FirstLaunchSetupWindow.axaml.cs b/StabilityMatrix.Avalonia/Views/FirstLaunchSetupWindow.axaml.cs index 3bcf1ef47..f02ea2542 100644 --- a/StabilityMatrix.Avalonia/Views/FirstLaunchSetupWindow.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/FirstLaunchSetupWindow.axaml.cs @@ -1,4 +1,6 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using FluentAvalonia.UI.Controls; @@ -9,7 +11,7 @@ namespace StabilityMatrix.Avalonia.Views; public partial class FirstLaunchSetupWindow : AppWindowBase { public ContentDialogResult Result { get; private set; } - + public FirstLaunchSetupWindow() { InitializeComponent(); @@ -19,14 +21,14 @@ private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - + [SuppressMessage("ReSharper", "UnusedParameter.Local")] private void ContinueButton_OnClick(object? sender, RoutedEventArgs e) { Result = ContentDialogResult.Primary; Close(); } - + [SuppressMessage("ReSharper", "UnusedParameter.Local")] private void QuitButton_OnClick(object? sender, RoutedEventArgs e) { From dfe1a59b09d8ed01b27c0205d5f1bcec71e90ecd Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 3 Oct 2023 00:19:21 -0400 Subject: [PATCH 455/474] Enable PublishReadyToRun for releases --- .github/workflows/release.yml | 3 ++- StabilityMatrix.Avalonia.pupnet.conf | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80d6cdd12..b7d84081c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -142,6 +142,7 @@ jobs: -o out -c Release -r ${{ env.platform-id }} -p:Version=$env:RELEASE_VERSION -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true + -p:PublishReadyToRun=true -p:SentryOrg=${{ secrets.SENTRY_ORG }} -p:SentryProject=${{ secrets.SENTRY_PROJECT }} -p:SentryUploadSymbols=true -p:SentryUploadSources=true @@ -197,4 +198,4 @@ jobs: tag_name: v${{ github.event.inputs.version }} body: ${{ steps.release_notes.outputs.release_notes }} draft: ${{ github.event.inputs.github-release-draft == 'true' }} - prerelease: ${{ github.event.inputs.github-release-prerelease == 'true' }} \ No newline at end of file + prerelease: ${{ github.event.inputs.github-release-prerelease == 'true' }} diff --git a/StabilityMatrix.Avalonia.pupnet.conf b/StabilityMatrix.Avalonia.pupnet.conf index fc75bf12e..f663cbd9c 100644 --- a/StabilityMatrix.Avalonia.pupnet.conf +++ b/StabilityMatrix.Avalonia.pupnet.conf @@ -138,7 +138,7 @@ DotnetProjectPath = StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj # '-p:DebugType=None -p:DebugSymbols=false -p:PublishSingleFile=true -p:PublishReadyToRun=true # -p:PublishTrimmed=true -p:TrimMode=link'. Note. This value may use macro variables. Use 'pupnet --help macro' # for reference. See: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish -DotnetPublishArgs = -p:Version=${APP_VERSION} -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false --self-contained +DotnetPublishArgs = -p:Version=${APP_VERSION} -p:PublishReadyToRun=true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=None -p:DebugSymbols=false --self-contained # Post-publish (or standalone build) command on Linux (ignored on Windows). It is called after dotnet # publish, but before the final output is built. This could, for example, be a script which copies From 76220173c07c10daff20302ec2fde69f647d0998 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 3 Oct 2023 03:45:10 -0400 Subject: [PATCH 456/474] Add changelog for 026e9bd --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d626e2e4..c739ad8ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Fixed ストップ button being cut off in Japanese translation - Fixed update progress freezing in some cases - Fixed light theme being default in first time setup window +- Fixed shared folder links not recreating fully when partially missing ## v2.4.6 ### Added From e6c1968b43762b6616521b0c6edb167db43025f7 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 3 Oct 2023 03:53:34 -0400 Subject: [PATCH 457/474] Fix extra proportional dock splitters causing errors --- .../Views/Inference/InferenceTextToImageView.axaml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml b/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml index 35c536154..bba97d30c 100644 --- a/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml +++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml @@ -37,8 +37,6 @@ Id="MainLayout" Orientation="Horizontal"> - - - + - + - + - - From 18354c77a2d7a1d016fd7e819a3178482bc6b2a4 Mon Sep 17 00:00:00 2001 From: Ionite Date: Tue, 3 Oct 2023 19:37:53 -0400 Subject: [PATCH 458/474] Update README with Inference info --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 44d214d36..ab15881c6 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ Multi-Platform Package Manager for Stable Diffusion +### ✨ New in 2.5 - [Inference](#inference), a built-in Stable Diffusion interface powered by ComfyUI + ### 🖱️ One click install and update for Stable Diffusion Web UI Packages - Supports [Automatic 1111][auto1111], [Comfy UI][comfy], [SD.Next (Vladmandic)][sdnext], [VoltaML][voltaml], [InvokeAI][invokeai], [Fooocus][fooocus], and [Fooocus-MRE][fooocus-mre] - Embedded Git and Python dependencies, with no need for either to be globally installed @@ -46,6 +48,18 @@ Multi-Platform Package Manager for Stable Diffusion > macOS builds are currently pending: [#45][download-macos] +### Inference - A reimagined built-in Stable Diffusion experience +- Powerful auto-completion and syntax highlighting using a formal language grammar +- Workspaces open in tabs that save and load from `.smproj` project files + +![](https://cdn.lykos.ai/static/sm-banner-inference-rounded.webp) + +- Customizable dockable and float panels +- Generated images contain Inference Project, ComfyUI Nodes, and A1111-compatible metadata +- Drag and drop gallery images or files to load states + +![](https://cdn.lykos.ai/static/sc-inference-drag-load-2.gif) + ### Searchable launch options

From 044ec5ecf83007badcdca8299851730ce2b3061b Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 00:14:28 -0400 Subject: [PATCH 459/474] Add PipInstallFromRequirements --- StabilityMatrix.Core/Python/PyVenvRunner.cs | 32 ++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/StabilityMatrix.Core/Python/PyVenvRunner.cs b/StabilityMatrix.Core/Python/PyVenvRunner.cs index d38474d63..4e88f22f2 100644 --- a/StabilityMatrix.Core/Python/PyVenvRunner.cs +++ b/StabilityMatrix.Core/Python/PyVenvRunner.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Text; using NLog; using Salaros.Configuration; @@ -241,6 +242,35 @@ public async Task PipInstall(string args, Action? outputDataRecei } } + ///

+ /// Pip install from a requirements.txt file. + /// + public async Task PipInstallFromRequirements( + FilePath file, + Action? outputDataReceived = null, + IEnumerable? excludes = null + ) + { + var requirementsText = await file.ReadAllTextAsync().ConfigureAwait(false); + var requirements = requirementsText + .Split(Environment.NewLine) + .Where(s => !string.IsNullOrWhiteSpace(s)); + + if (excludes is not null) + { + var excludesFilter = excludes.ToImmutableHashSet( + StringComparer.InvariantCultureIgnoreCase + ); + + requirements = requirements.Where(s => !excludesFilter.Contains(s)); + } + + var pipArgs = string.Join(' ', requirements); + + Logger.Info("Installing {FileName} ({PipArgs})", file.Name, pipArgs); + await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); + } + /// /// Run a custom install command. Waits for the process to exit. /// workingDirectory defaults to RootPath. From 0b6e2f125989dc2a9d6c1bc9dc79354e8adbf3bc Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 00:14:45 -0400 Subject: [PATCH 460/474] Improve image indexing speed --- .../Services/ImageIndexService.cs | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/StabilityMatrix.Core/Services/ImageIndexService.cs b/StabilityMatrix.Core/Services/ImageIndexService.cs index c009e8ee4..c7ee7739b 100644 --- a/StabilityMatrix.Core/Services/ImageIndexService.cs +++ b/StabilityMatrix.Core/Services/ImageIndexService.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System.Collections.Concurrent; +using System.Diagnostics; using System.Text.Json; using AsyncAwaitBestPractices; using DynamicData; @@ -73,33 +74,22 @@ public async Task RefreshIndex(IndexCollection indexColl var stopwatch = Stopwatch.StartNew(); logger.LogInformation("Refreshing images index at {ImagesDir}...", imagesDir); - var added = 0; - var toAdd = new Queue(); + var toAdd = new ConcurrentBag(); await Task.Run(() => { - 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); - - if (string.IsNullOrEmpty(relativePath)) + var files = imagesDir.Info + .EnumerateFiles("*.*", SearchOption.AllDirectories) + .Where(info => LocalImageFile.SupportedImageExtensions.Contains(info.Extension)) + .Select(info => new FilePath(info)); + + Parallel.ForEach( + files, + f => { - continue; - }*/ - - var localImage = LocalImageFile.FromPath(file); - - toAdd.Enqueue(localImage); - - added++; - } + toAdd.Add(LocalImageFile.FromPath(f)); + } + ); }) .ConfigureAwait(false); @@ -114,7 +104,7 @@ var file in imagesDir.Info logger.LogInformation( "Image index updated for {Prefix} with {Entries} files, took {IndexDuration:F1}ms ({EditDuration:F1}ms edit)", subPath, - added, + toAdd.Count, indexElapsed.TotalMilliseconds, editElapsed.TotalMilliseconds ); From 27e2e827fc7466c17531d22d0e297566c59e9348 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 00:15:05 -0400 Subject: [PATCH 461/474] Fix comfy requirements install --- StabilityMatrix.Core/Models/Packages/ComfyUI.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index 38b1bdf97..4996fc594 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -178,12 +178,20 @@ await InstallDirectMlTorch(venvRunner, progress, onConsoleOutput) throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null); } - // Install requirements file + // Install requirements file (skip torch) progress?.Report( new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true) ); - Logger.Info("Installing requirements.txt"); - await venvRunner.PipInstall($"-r requirements.txt", onConsoleOutput).ConfigureAwait(false); + + var requirementsFile = new FilePath(installLocation, "requirements.txt"); + + await venvRunner + .PipInstallFromRequirements( + requirementsFile, + onConsoleOutput, + excludes: new[] { "torch" } + ) + .ConfigureAwait(false); progress?.Report( new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false) From 13c2ed9b9713754188f1a9e2cf2bac391dcee2bf Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 01:25:54 -0400 Subject: [PATCH 462/474] Update Resources.ja-JP.resx --- .../Languages/Resources.ja-JP.resx | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx index 86dcb5b7e..63d37d58a 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx @@ -171,11 +171,11 @@ checkpoint / embedding / LoRA are often used in the same way as the above words on Japanese information websites, so it is easier to understand them without translation. - Emphasis + プロンプトの強調 The word has not been translated because it is not possible to guess what part of the UI it is used in. It is a little difficult to translate the word into Japanese, so I want to be careful not to change the meaning of the word. - Deemphasis + プロンプトの縮小 Emebeddings / Textual Inversion @@ -455,7 +455,7 @@ 統合 - Discord Rich Presence + Stability Matrix利用中、Discordステータス欄に表示 システム @@ -605,7 +605,7 @@ リスタート - 削除の再確認 + 削除する これにより、生成された画像や追加したファイルを含め、パッケージフォルダとそのすべてのコンテンツが削除されます。 @@ -629,7 +629,7 @@ 更新完了 - {0}が最新バージョンに更新されました + {0}が最新版に更新されました {0}の更新エラー @@ -647,4 +647,37 @@ Branch For Japanese engineers who use git on a daily basis, it is easier to understand terms used in git as they are in English. - + + ライセンス + + + データフォルダを選択してください + + + データフォルダの名前 + + + 現在のフォルダ: + + + アップデート完了後に再起動します + + + また後で + + + Install Now + + + リリースノート + + + プロジェクトファイルを開く + + + 名前をつけて保存 + + + レイアウトを初期状態に戻す + + \ No newline at end of file From 9b3cfde987876bfb407d3a6c78a59d1d35c611ea Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 14:41:30 -0400 Subject: [PATCH 463/474] Fix PipInstallFromRequirements --- StabilityMatrix.Core/Python/PyVenvRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Core/Python/PyVenvRunner.cs b/StabilityMatrix.Core/Python/PyVenvRunner.cs index 4e88f22f2..fa115468f 100644 --- a/StabilityMatrix.Core/Python/PyVenvRunner.cs +++ b/StabilityMatrix.Core/Python/PyVenvRunner.cs @@ -268,7 +268,7 @@ public async Task PipInstallFromRequirements( var pipArgs = string.Join(' ', requirements); Logger.Info("Installing {FileName} ({PipArgs})", file.Name, pipArgs); - await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); + await PipInstall(pipArgs, outputDataReceived).ConfigureAwait(false); } /// From f2c78caa4805cf8ea501b60ea1edc93ce9bfff77 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 14:51:06 -0400 Subject: [PATCH 464/474] Update requirements install for A3, Foocus, Mre --- StabilityMatrix.Core/Models/Packages/A3WebUI.cs | 5 ++++- StabilityMatrix.Core/Models/Packages/Fooocus.cs | 7 +++---- StabilityMatrix.Core/Models/Packages/FooocusMre.cs | 7 +++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs index 5a0ab9464..37472b9f9 100644 --- a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs +++ b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs @@ -4,6 +4,7 @@ using NLog; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; @@ -197,8 +198,10 @@ await InstallDirectMlTorch(venvRunner, progress, onConsoleOutput) new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true) ); Logger.Info("Installing requirements_versions.txt"); + + var requirements = new FilePath(installLocation, "requirements_versions.txt"); await venvRunner - .PipInstall($"-r requirements_versions.txt", onConsoleOutput) + .PipInstallFromRequirements(requirements, onConsoleOutput) .ConfigureAwait(false); progress?.Report( diff --git a/StabilityMatrix.Core/Models/Packages/Fooocus.cs b/StabilityMatrix.Core/Models/Packages/Fooocus.cs index c564d3f8a..429c62a9a 100644 --- a/StabilityMatrix.Core/Models/Packages/Fooocus.cs +++ b/StabilityMatrix.Core/Models/Packages/Fooocus.cs @@ -2,6 +2,7 @@ using System.Text.RegularExpressions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; @@ -125,11 +126,9 @@ await venvRunner ) .ConfigureAwait(false); - progress?.Report( - new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true) - ); + var requirements = new FilePath(installLocation, "requirements_versions.txt"); await venvRunner - .PipInstall("-r requirements_versions.txt", onConsoleOutput) + .PipInstallFromRequirements(requirements, onConsoleOutput) .ConfigureAwait(false); } diff --git a/StabilityMatrix.Core/Models/Packages/FooocusMre.cs b/StabilityMatrix.Core/Models/Packages/FooocusMre.cs index 5fd3e0430..1492a3a8d 100644 --- a/StabilityMatrix.Core/Models/Packages/FooocusMre.cs +++ b/StabilityMatrix.Core/Models/Packages/FooocusMre.cs @@ -2,6 +2,7 @@ using System.Text.RegularExpressions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Services; @@ -129,11 +130,9 @@ await venvRunner ) .ConfigureAwait(false); - progress?.Report( - new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true) - ); + var requirements = new FilePath(installLocation, "requirements_versions.txt"); await venvRunner - .PipInstall("-r requirements_versions.txt", onConsoleOutput) + .PipInstallFromRequirements(requirements, onConsoleOutput) .ConfigureAwait(false); } From 1ece86e2dbfb285f2595fe182d63e8dcab0e3d22 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 16:18:40 -0400 Subject: [PATCH 465/474] Cleanup async tasks to task returns --- StabilityMatrix.Core/Models/Packages/BasePackage.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/StabilityMatrix.Core/Models/Packages/BasePackage.cs b/StabilityMatrix.Core/Models/Packages/BasePackage.cs index 98d1ac7a0..2c224ad3e 100644 --- a/StabilityMatrix.Core/Models/Packages/BasePackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BasePackage.cs @@ -181,7 +181,7 @@ await venvRunner await venvRunner.PipInstall("xformers", onConsoleOutput).ConfigureAwait(false); } - protected async Task InstallDirectMlTorch( + protected Task InstallDirectMlTorch( PyVenvRunner venvRunner, IProgress? progress = null, Action? onConsoleOutput = null @@ -191,12 +191,10 @@ protected async Task InstallDirectMlTorch( new ProgressReport(-1f, "Installing PyTorch for DirectML", isIndeterminate: true) ); - await venvRunner - .PipInstall(PyVenvRunner.TorchPipInstallArgsDirectML, onConsoleOutput) - .ConfigureAwait(false); + return venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsDirectML, onConsoleOutput); } - protected async Task InstallCpuTorch( + protected Task InstallCpuTorch( PyVenvRunner venvRunner, IProgress? progress = null, Action? onConsoleOutput = null @@ -206,8 +204,6 @@ protected async Task InstallCpuTorch( new ProgressReport(-1f, "Installing PyTorch for CPU", isIndeterminate: true) ); - await venvRunner - .PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, onConsoleOutput) - .ConfigureAwait(false); + return venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, onConsoleOutput); } } From d2e201ecf7257d90955de36799c57b095d24a5a6 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 16:41:40 -0400 Subject: [PATCH 466/474] Add `SplitLines` string extension --- StabilityMatrix.Core/Extensions/StringExtensions.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/StabilityMatrix.Core/Extensions/StringExtensions.cs b/StabilityMatrix.Core/Extensions/StringExtensions.cs index 986f7b214..87e222911 100644 --- a/StabilityMatrix.Core/Extensions/StringExtensions.cs +++ b/StabilityMatrix.Core/Extensions/StringExtensions.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.RegularExpressions; namespace StabilityMatrix.Core.Extensions; @@ -84,4 +85,16 @@ public static string StripStart(this string str, string subString) var index = str.IndexOf(subString, StringComparison.Ordinal); return index < 0 ? str : str.Remove(index, subString.Length); } + + /// + /// Splits lines by \n and \r\n + /// + // ReSharper disable once ReturnTypeCanBeEnumerable.Global + public static string[] SplitLines( + this string str, + StringSplitOptions options = StringSplitOptions.None + ) + { + return str.Split(new[] { "\r\n", "\n" }, options); + } } From c9065dbb62060000a743bd8efa726419807fe707 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 16:42:15 -0400 Subject: [PATCH 467/474] Fix line splitting in PipInstallFromRequirements --- StabilityMatrix.Core/Models/Packages/A3WebUI.cs | 2 +- StabilityMatrix.Core/Models/Packages/ComfyUI.cs | 6 +----- StabilityMatrix.Core/Models/Packages/Fooocus.cs | 2 +- StabilityMatrix.Core/Models/Packages/FooocusMre.cs | 2 +- StabilityMatrix.Core/Python/PyVenvRunner.cs | 14 ++++++-------- 5 files changed, 10 insertions(+), 16 deletions(-) diff --git a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs index 37472b9f9..5d0a275d0 100644 --- a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs +++ b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs @@ -201,7 +201,7 @@ await InstallDirectMlTorch(venvRunner, progress, onConsoleOutput) var requirements = new FilePath(installLocation, "requirements_versions.txt"); await venvRunner - .PipInstallFromRequirements(requirements, onConsoleOutput) + .PipInstallFromRequirements(requirements, onConsoleOutput, excludes: "torch") .ConfigureAwait(false); progress?.Report( diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index 4996fc594..af3881fbe 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -186,11 +186,7 @@ await InstallDirectMlTorch(venvRunner, progress, onConsoleOutput) var requirementsFile = new FilePath(installLocation, "requirements.txt"); await venvRunner - .PipInstallFromRequirements( - requirementsFile, - onConsoleOutput, - excludes: new[] { "torch" } - ) + .PipInstallFromRequirements(requirementsFile, onConsoleOutput, excludes: "torch") .ConfigureAwait(false); progress?.Report( diff --git a/StabilityMatrix.Core/Models/Packages/Fooocus.cs b/StabilityMatrix.Core/Models/Packages/Fooocus.cs index 429c62a9a..424f35533 100644 --- a/StabilityMatrix.Core/Models/Packages/Fooocus.cs +++ b/StabilityMatrix.Core/Models/Packages/Fooocus.cs @@ -128,7 +128,7 @@ await venvRunner var requirements = new FilePath(installLocation, "requirements_versions.txt"); await venvRunner - .PipInstallFromRequirements(requirements, onConsoleOutput) + .PipInstallFromRequirements(requirements, onConsoleOutput, excludes: "torch") .ConfigureAwait(false); } diff --git a/StabilityMatrix.Core/Models/Packages/FooocusMre.cs b/StabilityMatrix.Core/Models/Packages/FooocusMre.cs index 1492a3a8d..fb65ce288 100644 --- a/StabilityMatrix.Core/Models/Packages/FooocusMre.cs +++ b/StabilityMatrix.Core/Models/Packages/FooocusMre.cs @@ -132,7 +132,7 @@ await venvRunner var requirements = new FilePath(installLocation, "requirements_versions.txt"); await venvRunner - .PipInstallFromRequirements(requirements, onConsoleOutput) + .PipInstallFromRequirements(requirements, onConsoleOutput, excludes: "torch") .ConfigureAwait(false); } diff --git a/StabilityMatrix.Core/Python/PyVenvRunner.cs b/StabilityMatrix.Core/Python/PyVenvRunner.cs index fa115468f..dbd922223 100644 --- a/StabilityMatrix.Core/Python/PyVenvRunner.cs +++ b/StabilityMatrix.Core/Python/PyVenvRunner.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Text; +using System.Text.RegularExpressions; using NLog; using Salaros.Configuration; using StabilityMatrix.Core.Exceptions; @@ -15,7 +16,6 @@ namespace StabilityMatrix.Core.Python; /// /// Python runner using a subprocess, mainly for venv support. /// -[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class PyVenvRunner : IDisposable, IAsyncDisposable { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); @@ -248,21 +248,19 @@ public async Task PipInstall(string args, Action? outputDataRecei public async Task PipInstallFromRequirements( FilePath file, Action? outputDataReceived = null, - IEnumerable? excludes = null + [StringSyntax(StringSyntaxAttribute.Regex)] string? excludes = null ) { var requirementsText = await file.ReadAllTextAsync().ConfigureAwait(false); var requirements = requirementsText - .Split(Environment.NewLine) - .Where(s => !string.IsNullOrWhiteSpace(s)); + .SplitLines(StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .AsEnumerable(); if (excludes is not null) { - var excludesFilter = excludes.ToImmutableHashSet( - StringComparer.InvariantCultureIgnoreCase - ); + var excludeRegex = new Regex(excludes); - requirements = requirements.Where(s => !excludesFilter.Contains(s)); + requirements = requirements.Where(s => !excludeRegex.IsMatch(s)); } var pipArgs = string.Join(' ', requirements); From 41f08017abf58a3a9641ce6a2dc434f02c3c924c Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 16:46:18 -0400 Subject: [PATCH 468/474] Version bump to release --- 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 e69408b90..e07e8f30c 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.0-pre.3 + 2.5.0 $(Version) true true From 6432edb8478a6750ca01c7fe9222ccdf1da792e7 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 16:59:34 -0400 Subject: [PATCH 469/474] Fix torch install to pin 2.0.1 --- StabilityMatrix.Core/Python/PyVenvRunner.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/StabilityMatrix.Core/Python/PyVenvRunner.cs b/StabilityMatrix.Core/Python/PyVenvRunner.cs index dbd922223..538fea148 100644 --- a/StabilityMatrix.Core/Python/PyVenvRunner.cs +++ b/StabilityMatrix.Core/Python/PyVenvRunner.cs @@ -20,17 +20,19 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private const string TorchPipInstallArgs = "torch==2.0.1 torchvision==2.0.1"; + public const string TorchPipInstallArgsCuda = - "torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu118"; - public const string TorchPipInstallArgsCpu = "torch torchvision torchaudio"; + $"{TorchPipInstallArgs} --extra-index-url https://download.pytorch.org/whl/cu118"; + public const string TorchPipInstallArgsCpu = TorchPipInstallArgs; public const string TorchPipInstallArgsDirectML = "torch-directml"; public const string TorchPipInstallArgsRocm511 = - "torch torchvision --extra-index-url https://download.pytorch.org/whl/rocm5.1.1"; + $"{TorchPipInstallArgs} --extra-index-url https://download.pytorch.org/whl/rocm5.1.1"; public const string TorchPipInstallArgsRocm542 = - "torch torchvision --extra-index-url https://download.pytorch.org/whl/rocm5.4.2"; + $"{TorchPipInstallArgs} --extra-index-url https://download.pytorch.org/whl/rocm5.4.2"; public const string TorchPipInstallArgsRocmNightly56 = - "--pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/rocm5.6"; + $"--pre {TorchPipInstallArgs} --index-url https://download.pytorch.org/whl/nightly/rocm5.6"; /// /// Relative path to the site-packages folder from the venv root. From e01ac46c612465582e39033caec0871b1016fa25 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 17:00:47 -0400 Subject: [PATCH 470/474] Formatting --- StabilityMatrix.Core/Python/PyVenvRunner.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/StabilityMatrix.Core/Python/PyVenvRunner.cs b/StabilityMatrix.Core/Python/PyVenvRunner.cs index 538fea148..395b966a7 100644 --- a/StabilityMatrix.Core/Python/PyVenvRunner.cs +++ b/StabilityMatrix.Core/Python/PyVenvRunner.cs @@ -1,5 +1,4 @@ -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.RegularExpressions; using NLog; From 7b8cc5b89806c59c28cddded79729db5f1225815 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 17:28:57 -0400 Subject: [PATCH 471/474] Fix torchvision version specifier --- StabilityMatrix.Core/Python/PyVenvRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Core/Python/PyVenvRunner.cs b/StabilityMatrix.Core/Python/PyVenvRunner.cs index 395b966a7..41f389c0d 100644 --- a/StabilityMatrix.Core/Python/PyVenvRunner.cs +++ b/StabilityMatrix.Core/Python/PyVenvRunner.cs @@ -19,7 +19,7 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - private const string TorchPipInstallArgs = "torch==2.0.1 torchvision==2.0.1"; + private const string TorchPipInstallArgs = "torch==2.0.1 torchvision"; public const string TorchPipInstallArgsCuda = $"{TorchPipInstallArgs} --extra-index-url https://download.pytorch.org/whl/cu118"; From e0df26e2e3f10bd85137c9be462d2a344c1308da Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 17:36:41 -0400 Subject: [PATCH 472/474] Fix match regex --- StabilityMatrix.Core/Python/PyVenvRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StabilityMatrix.Core/Python/PyVenvRunner.cs b/StabilityMatrix.Core/Python/PyVenvRunner.cs index 41f389c0d..f35b66efc 100644 --- a/StabilityMatrix.Core/Python/PyVenvRunner.cs +++ b/StabilityMatrix.Core/Python/PyVenvRunner.cs @@ -259,7 +259,7 @@ public async Task PipInstallFromRequirements( if (excludes is not null) { - var excludeRegex = new Regex(excludes); + var excludeRegex = new Regex($"^{excludes}$"); requirements = requirements.Where(s => !excludeRegex.IsMatch(s)); } From 388f36dcbd2ee177914e1b52e291b0947fd0c302 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 17:47:41 -0400 Subject: [PATCH 473/474] Add exists check to IsSymbolic link Fix issue where IsSymbolicLink can be true when the directory doesn't exist --- .../Models/FileInterfaces/DirectoryPath.cs | 83 ++++++++++--------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/StabilityMatrix.Core/Models/FileInterfaces/DirectoryPath.cs b/StabilityMatrix.Core/Models/FileInterfaces/DirectoryPath.cs index c125e7a12..8cab7af5d 100644 --- a/StabilityMatrix.Core/Models/FileInterfaces/DirectoryPath.cs +++ b/StabilityMatrix.Core/Models/FileInterfaces/DirectoryPath.cs @@ -9,6 +9,7 @@ namespace StabilityMatrix.Core.Models.FileInterfaces; public class DirectoryPath : FileSystemPath, IPathObject { private DirectoryInfo? info; + // ReSharper disable once MemberCanBePrivate.Global [JsonIgnore] public DirectoryInfo Info => info ??= new DirectoryInfo(FullPath); @@ -19,53 +20,49 @@ public bool IsSymbolicLink get { Info.Refresh(); - return Info.Attributes.HasFlag(FileAttributes.ReparsePoint); + return Info.Exists && Info.Attributes.HasFlag(FileAttributes.ReparsePoint); } } - + /// /// Gets a value indicating whether the directory exists. /// [JsonIgnore] public bool Exists => Info.Exists; - + /// [JsonIgnore] public string Name => Info.Name; - + /// /// Get the parent directory. /// [JsonIgnore] - public DirectoryPath? Parent => Info.Parent == null - ? null : new DirectoryPath(Info.Parent); + public DirectoryPath? Parent => Info.Parent == null ? null : new DirectoryPath(Info.Parent); - public DirectoryPath(string path) : base(path) - { - } - - public DirectoryPath(FileSystemPath path) : base(path) - { - } - - public DirectoryPath(DirectoryInfo info) : base(info.FullName) + public DirectoryPath(string path) + : base(path) { } + + public DirectoryPath(FileSystemPath path) + : base(path) { } + + public DirectoryPath(DirectoryInfo info) + : base(info.FullName) { // Additionally set the info field this.info = info; } - - public DirectoryPath(params string[] paths) : base(paths) - { - } + + public DirectoryPath(params string[] paths) + : base(paths) { } /// public long GetSize() { Info.Refresh(); - return Info.EnumerateFiles("*", SearchOption.AllDirectories) - .Sum(file => file.Length); + return Info.EnumerateFiles("*", SearchOption.AllDirectories).Sum(file => file.Length); } - + /// /// Gets the size of the directory. /// @@ -74,15 +71,18 @@ public long GetSize() /// public long GetSize(bool includeSymbolicLinks) { - if (includeSymbolicLinks) return GetSize(); - + if (includeSymbolicLinks) + return GetSize(); + Info.Refresh(); var files = Info.GetFiles() .Where(file => !file.Attributes.HasFlag(FileAttributes.ReparsePoint)) .Sum(file => file.Length); var subDirs = Info.GetDirectories() .Where(dir => !dir.Attributes.HasFlag(FileAttributes.ReparsePoint)) - .Sum(dir => dir.EnumerateFiles("*", SearchOption.AllDirectories).Sum(file => file.Length)); + .Sum( + dir => dir.EnumerateFiles("*", SearchOption.AllDirectories).Sum(file => file.Length) + ); return files + subDirs; } @@ -96,7 +96,7 @@ public Task GetSizeAsync(bool includeSymbolicLinks) { return Task.Run(() => GetSize(includeSymbolicLinks)); } - + /// /// Creates the directory. /// @@ -106,7 +106,7 @@ public Task GetSizeAsync(bool includeSymbolicLinks) /// Deletes the directory. /// public void Delete() => Directory.Delete(FullPath); - + /// Deletes the directory asynchronously. public Task DeleteAsync() => Task.Run(Delete); @@ -120,38 +120,43 @@ public Task GetSizeAsync(bool includeSymbolicLinks) /// Deletes the directory asynchronously. /// public Task DeleteAsync(bool recursive) => Task.Run(() => Delete(recursive)); - + /// /// Join with other paths to form a new directory path. /// - public DirectoryPath JoinDir(params DirectoryPath[] paths) => + public DirectoryPath JoinDir(params DirectoryPath[] paths) => new(Path.Combine(FullPath, Path.Combine(paths.Select(path => path.FullPath).ToArray()))); - + /// /// Join with other paths to form a new file path. /// - public FilePath JoinFile(params FilePath[] paths) => + public FilePath JoinFile(params FilePath[] paths) => new(Path.Combine(FullPath, Path.Combine(paths.Select(path => path.FullPath).ToArray()))); public override string ToString() => FullPath; // DirectoryPath + DirectoryPath = DirectoryPath - public static DirectoryPath operator +(DirectoryPath path, DirectoryPath other) => new(Path.Combine(path, other.FullPath)); - + public static DirectoryPath operator +(DirectoryPath path, DirectoryPath other) => + new(Path.Combine(path, other.FullPath)); + // DirectoryPath + FilePath = FilePath - public static FilePath operator +(DirectoryPath path, FilePath other) => new(Path.Combine(path, other.FullPath)); - + public static FilePath operator +(DirectoryPath path, FilePath other) => + new(Path.Combine(path, other.FullPath)); + // DirectoryPath + FileInfo = FilePath - public static FilePath operator +(DirectoryPath path, FileInfo other) => new(Path.Combine(path, other.FullName)); - + public static FilePath operator +(DirectoryPath path, FileInfo other) => + new(Path.Combine(path, other.FullName)); + // DirectoryPath + string = string public static string operator +(DirectoryPath path, string other) => Path.Combine(path, other); - + // Implicit conversions to and from string public static implicit operator string(DirectoryPath path) => path.FullPath; + public static implicit operator DirectoryPath(string path) => new(path); - + // Implicit conversions to and from DirectoryInfo public static implicit operator DirectoryInfo(DirectoryPath path) => path.Info; + public static implicit operator DirectoryPath(DirectoryInfo path) => new(path); } From 72c149a64af0736a7c7cde15a6a44bdb742d2530 Mon Sep 17 00:00:00 2001 From: Ionite Date: Wed, 4 Oct 2023 18:13:34 -0400 Subject: [PATCH 474/474] fix missing continue --- StabilityMatrix.Core/Helper/FileTransfers.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/StabilityMatrix.Core/Helper/FileTransfers.cs b/StabilityMatrix.Core/Helper/FileTransfers.cs index 69cb05132..d2f84db8a 100644 --- a/StabilityMatrix.Core/Helper/FileTransfers.cs +++ b/StabilityMatrix.Core/Helper/FileTransfers.cs @@ -209,6 +209,7 @@ public static async Task MoveAllFiles( + $" Matching Blake3 hash: {sourceHash}" ); sourceFile.Delete(); + continue; } } else if (!overwrite)