Browse Source

Merge branch 'dev' into inference-modules

pull/333/head
Ionite 1 year ago committed by GitHub
parent
commit
8a1b03a3f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 1
      StabilityMatrix.Avalonia/App.axaml
  3. 7
      StabilityMatrix.Avalonia/App.axaml.cs
  4. 51
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml
  5. 25
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs
  6. 26
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  7. 117
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  8. 41
      StabilityMatrix.Avalonia/Languages/Resources.resx
  9. 28
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
  10. 2
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  11. 19
      StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs
  12. 151
      StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
  13. 41
      StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml
  14. 64
      StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml
  15. 7
      StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml.cs
  16. 190
      StabilityMatrix.Avalonia/Views/OutputsPage.axaml
  17. 33
      StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml
  18. 2
      StabilityMatrix.Core/Models/Packages/BasePackage.cs
  19. 289
      StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs

4
CHANGELOG.md

@ -11,8 +11,12 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
- This will link the package's output folders to the relevant subfolders in the "Outputs" directory
- When a package only has a generic "outputs" folder, all generated images from that package will be linked to the "Outputs\Text2Img" folder when this option is enabled
- Added "Outputs" page for viewing generated images from any package, or the shared output folder
- Added [Stable Diffusion WebUI/UX](https://github.com/anapnoe/stable-diffusion-webui-ux) package
### Changed
- If ComfyUI for Inference is chosen during the One-Click Installer, the Inference page will be opened after installation instead of the Launch page
### Fixed
- Fixed crash when clicking Inference gallery image after the image is deleted externally in file explorer
- Fixed Inference popup Install button not working on One-Click Installer
## v2.5.3
### Added

1
StabilityMatrix.Avalonia/App.axaml

@ -63,6 +63,7 @@
<StyleInclude Source="Controls/SharpenCard.axaml"/>
<StyleInclude Source="Controls/FreeUCard.axaml"/>
<StyleInclude Source="Controls/Paginator.axaml"/>
<StyleInclude Source="Controls/SelectableImageCard/SelectableImageButton.axaml"/>
<Style Selector="DockControl">
<Setter Property="(DockProperties.ControlRecycling)" Value="{StaticResource ControlRecyclingKey}" />

7
StabilityMatrix.Avalonia/App.axaml.cs

@ -436,12 +436,13 @@ public sealed class App : Application
internal static void ConfigurePackages(IServiceCollection services)
{
services.AddSingleton<BasePackage, A3WebUI>();
services.AddSingleton<BasePackage, Fooocus>();
services.AddSingleton<BasePackage, StableDiffusionUx>();
services.AddSingleton<BasePackage, VladAutomatic>();
services.AddSingleton<BasePackage, InvokeAI>();
services.AddSingleton<BasePackage, Fooocus>();
services.AddSingleton<BasePackage, FooocusMre>();
services.AddSingleton<BasePackage, ComfyUI>();
services.AddSingleton<BasePackage, VoltaML>();
services.AddSingleton<BasePackage, FooocusMre>();
services.AddSingleton<BasePackage, InvokeAI>();
}
private static IServiceCollection ConfigureServices()

51
StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml

@ -0,0 +1,51 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
<selectableImageCard:SelectableImageButton
Source="https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg" />
</Border>
</Design.PreviewWith>
<!-- Add Styles Here -->
<Style Selector="selectableImageCard|SelectableImageButton">
<Setter Property="Template">
<ControlTemplate>
<Grid>
<CheckBox VerticalAlignment="Top"
HorizontalAlignment="Right"
Margin="14,8"
Padding="0"
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
ZIndex="100">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5" />
</CheckBox.RenderTransform>
<CheckBox.Styles>
<Style Selector="CheckBox">
<Setter Property="CornerRadius" Value="16" />
</Style>
</CheckBox.Styles>
</CheckBox>
<Button
Margin="0"
Padding="4"
CornerRadius="12"
Command="{TemplateBinding Command}"
CommandParameter="{TemplateBinding CommandParameter}">
<controls:BetterAdvancedImage
Width="300"
Height="300"
Stretch="UniformToFill"
CornerRadius="8"
ContextFlyout="{TemplateBinding ContextFlyout}"
Source="{TemplateBinding Source}" />
</Button>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>

25
StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs

@ -0,0 +1,25 @@
using Avalonia;
using Avalonia.Controls;
namespace StabilityMatrix.Avalonia.Controls.SelectableImageCard;
public class SelectableImageButton : Button
{
public static readonly StyledProperty<bool?> IsSelectedProperty =
CheckBox.IsCheckedProperty.AddOwner<SelectableImageButton>();
public static readonly StyledProperty<string?> SourceProperty =
BetterAdvancedImage.SourceProperty.AddOwner<SelectableImageButton>();
public bool? IsSelected
{
get => GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}
public string? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
}

26
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -7,6 +7,7 @@ using System.IO;
using System.Net.Http;
using System.Text;
using AvaloniaEdit.Utils;
using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Controls.CodeCompletion;
@ -20,6 +21,7 @@ using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Progress;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Avalonia.ViewModels.Settings;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database;
@ -29,6 +31,7 @@ using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
@ -336,8 +339,27 @@ public static class DesignData
public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>();
public static OutputsPageViewModel OutputsPageViewModel =>
Services.GetRequiredService<OutputsPageViewModel>();
public static OutputsPageViewModel OutputsPageViewModel
{
get
{
var vm = Services.GetRequiredService<OutputsPageViewModel>();
vm.Outputs = new ObservableCollectionExtended<OutputImageViewModel>
{
new(
new LocalImageFile
{
AbsolutePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
RelativePath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg",
ImageType = LocalImageFileType.TextToImage
}
)
};
return vm;
}
}
public static PackageManagerViewModel PackageManagerViewModel
{

117
StabilityMatrix.Avalonia/Languages/Resources.Designer.cs generated

@ -113,6 +113,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Clear Selection.
/// </summary>
public static string Action_ClearSelection {
get {
return ResourceManager.GetString("Action_ClearSelection", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Close.
/// </summary>
@ -257,6 +266,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Open in Image Viewer.
/// </summary>
public static string Action_OpenInViewer {
get {
return ResourceManager.GetString("Action_OpenInViewer", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open on CivitAI.
/// </summary>
@ -392,6 +410,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Select All.
/// </summary>
public static string Action_SelectAll {
get {
return ResourceManager.GetString("Action_SelectAll", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select Directory.
/// </summary>
@ -419,6 +446,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Send to Inference.
/// </summary>
public static string Action_SendToInference {
get {
return ResourceManager.GetString("Action_SendToInference", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show in Explorer.
/// </summary>
@ -896,6 +932,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Image to Image.
/// </summary>
public static string Label_ImageToImage {
get {
return ResourceManager.GetString("Label_ImageToImage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Import as Connected.
/// </summary>
@ -941,6 +986,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Inpainting.
/// </summary>
public static string Label_Inpainting {
get {
return ResourceManager.GetString("Label_Inpainting", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Input.
/// </summary>
@ -1157,6 +1211,24 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to {0} images selected.
/// </summary>
public static string Label_NumImagesSelected {
get {
return ResourceManager.GetString("Label_NumImagesSelected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to 1 image selected.
/// </summary>
public static string Label_OneImageSelected {
get {
return ResourceManager.GetString("Label_OneImageSelected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Only available on Windows.
/// </summary>
@ -1166,6 +1238,33 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Output Folder.
/// </summary>
public static string Label_OutputFolder {
get {
return ResourceManager.GetString("Label_OutputFolder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output Browser.
/// </summary>
public static string Label_OutputsPageTitle {
get {
return ResourceManager.GetString("Label_OutputsPageTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output Type.
/// </summary>
public static string Label_OutputType {
get {
return ResourceManager.GetString("Label_OutputType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Package Environment.
/// </summary>
@ -1490,6 +1589,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Text to Image.
/// </summary>
public static string Label_TextToImage {
get {
return ResourceManager.GetString("Label_TextToImage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Theme.
/// </summary>
@ -1535,6 +1643,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Upscale.
/// </summary>
public static string Label_Upscale {
get {
return ResourceManager.GetString("Label_Upscale", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output Sharing.
/// </summary>

41
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -687,4 +687,43 @@
<data name="Action_Copy" xml:space="preserve">
<value>Copy</value>
</data>
</root>
<data name="Action_OpenInViewer" xml:space="preserve">
<value>Open in Image Viewer</value>
</data>
<data name="Label_NumImagesSelected" xml:space="preserve">
<value>{0} images selected</value>
</data>
<data name="Label_OutputFolder" xml:space="preserve">
<value>Output Folder</value>
</data>
<data name="Label_OutputType" xml:space="preserve">
<value>Output Type</value>
</data>
<data name="Action_ClearSelection" xml:space="preserve">
<value>Clear Selection</value>
</data>
<data name="Action_SelectAll" xml:space="preserve">
<value>Select All</value>
</data>
<data name="Action_SendToInference" xml:space="preserve">
<value>Send to Inference</value>
</data>
<data name="Label_TextToImage" xml:space="preserve">
<value>Text to Image</value>
</data>
<data name="Label_ImageToImage" xml:space="preserve">
<value>Image to Image</value>
</data>
<data name="Label_Inpainting" xml:space="preserve">
<value>Inpainting</value>
</data>
<data name="Label_Upscale" xml:space="preserve">
<value>Upscale</value>
</data>
<data name="Label_OutputsPageTitle" xml:space="preserve">
<value>Output Browser</value>
</data>
<data name="Label_OneImageSelected" xml:space="preserve">
<value>1 image selected</value>
</data>
</root>

28
StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs

@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
@ -26,6 +27,7 @@ public partial class OneClickInstallViewModel : ViewModelBase
private readonly ILogger<OneClickInstallViewModel> logger;
private readonly IPyRunner pyRunner;
private readonly ISharedFolders sharedFolders;
private readonly INavigationService navigationService;
private const string DefaultPackageName = "stable-diffusion-webui";
[ObservableProperty]
@ -55,13 +57,16 @@ public partial class OneClickInstallViewModel : ViewModelBase
public bool IsProgressBarVisible => OneClickInstallProgress > 0 || IsIndeterminate;
private bool isInferenceInstall;
public OneClickInstallViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
IPrerequisiteHelper prerequisiteHelper,
ILogger<OneClickInstallViewModel> logger,
IPyRunner pyRunner,
ISharedFolders sharedFolders
ISharedFolders sharedFolders,
INavigationService navigationService
)
{
this.settingsManager = settingsManager;
@ -70,6 +75,7 @@ public partial class OneClickInstallViewModel : ViewModelBase
this.logger = logger;
this.pyRunner = pyRunner;
this.sharedFolders = sharedFolders;
this.navigationService = navigationService;
HeaderText = Resources.Text_WelcomeToStabilityMatrix;
SubHeaderText = Resources.Text_OneClickInstaller_SubHeader;
@ -95,6 +101,18 @@ public partial class OneClickInstallViewModel : ViewModelBase
return Task.CompletedTask;
}
[RelayCommand]
private async Task InstallComfyForInference()
{
var comfyPackage = AllPackages.FirstOrDefault(x => x is ComfyUI);
if (comfyPackage != null)
{
SelectedPackage = comfyPackage;
isInferenceInstall = true;
await InstallCommand.ExecuteAsync(null);
}
}
private async Task DoInstall()
{
HeaderText = $"{Resources.Label_Installing} {SelectedPackage.DisplayName}";
@ -171,14 +189,18 @@ public partial class OneClickInstallViewModel : ViewModelBase
SubSubHeaderText = string.Empty;
OneClickInstallProgress = 100;
for (var i = 0; i < 3; i++)
for (var i = 3; i > 0; i--)
{
SubHeaderText = $"{Resources.Text_ProceedingToLaunchPage} ({i + 1}s)";
SubHeaderText = $"{Resources.Text_ProceedingToLaunchPage} ({i}s)";
await Task.Delay(1000);
}
// should close dialog
EventManager.Instance.OnOneClickInstallFinished(false);
if (isInferenceInstall)
{
navigationService.NavigateTo<InferenceViewModel>();
}
}
private async Task DownloadPackage(

2
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

@ -108,7 +108,7 @@ public partial class MainWindowViewModel : ViewModelBase
var startupTime = CodeTimer.FormatTime(Program.StartupTimer.Elapsed);
Logger.Info($"App started ({startupTime})");
if (Program.Args.DebugOneClickInstall || !settingsManager.Settings.InstalledPackages.Any())
if (Program.Args.DebugOneClickInstall || settingsManager.Settings.InstalledPackages.Any())
{
var viewModel = dialogFactory.Get<OneClickInstallViewModel>();
var dialog = new BetterContentDialog

19
StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs

@ -0,0 +1,19 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Models.Database;
namespace StabilityMatrix.Avalonia.ViewModels.OutputsPage;
public partial class OutputImageViewModel : ViewModelBase
{
public LocalImageFile ImageFile { get; }
[ObservableProperty]
private bool isSelected;
public OutputImageViewModel(LocalImageFile imageFile)
{
ImageFile = imageFile;
}
}

151
StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs

@ -1,24 +1,30 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
@ -32,21 +38,23 @@ using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(OutputsPage))]
[View(typeof(Views.OutputsPage))]
public partial class OutputsPageViewModel : PageViewModelBase
{
private readonly ISettingsManager settingsManager;
private readonly INotificationService notificationService;
private readonly INavigationService navigationService;
public override string Title => "Outputs";
private readonly ILogger<OutputsPageViewModel> logger;
public override string Title => Resources.Label_OutputsPageTitle;
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true };
public SourceCache<LocalImageFile, string> OutputsCache { get; } = new(p => p.AbsolutePath);
public SourceCache<OutputImageViewModel, string> OutputsCache { get; } =
new(p => p.ImageFile.AbsolutePath);
public IObservableCollection<LocalImageFile> Outputs { get; } =
new ObservableCollectionExtended<LocalImageFile>();
public IObservableCollection<OutputImageViewModel> Outputs { get; set; } =
new ObservableCollectionExtended<OutputImageViewModel>();
public IEnumerable<SharedOutputType> OutputTypes { get; } = Enum.GetValues<SharedOutputType>();
@ -60,25 +68,41 @@ public partial class OutputsPageViewModel : PageViewModelBase
[ObservableProperty]
private SharedOutputType selectedOutputType;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(NumImagesSelected))]
private int numItemsSelected;
public bool CanShowOutputTypes => SelectedCategory.Name.Equals("Shared Output Folder");
public string NumImagesSelected =>
NumItemsSelected == 1
? Resources.Label_OneImageSelected
: string.Format(Resources.Label_NumImagesSelected, NumItemsSelected);
public OutputsPageViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
INotificationService notificationService,
INavigationService navigationService
INavigationService navigationService,
ILogger<OutputsPageViewModel> logger
)
{
this.settingsManager = settingsManager;
this.notificationService = notificationService;
this.navigationService = navigationService;
this.logger = logger;
OutputsCache
.Connect()
.DeferUntilLoaded()
.SortBy(x => x.CreatedAt, SortDirection.Descending)
.SortBy(x => x.ImageFile.CreatedAt, SortDirection.Descending)
.ObserveOn(SynchronizationContext.Current)
.Bind(Outputs)
.Subscribe();
.WhenPropertyChanged(p => p.IsSelected)
.Subscribe(_ =>
{
NumItemsSelected = Outputs.Count(o => o.IsSelected);
});
if (!settingsManager.IsLibraryDirSet || Design.IsDesignMode)
return;
@ -152,16 +176,29 @@ public partial class OutputsPageViewModel : PageViewModelBase
GetOutputs(path);
}
public async Task OnImageClick(LocalImageFile item)
public async Task OnImageClick(OutputImageViewModel item)
{
// Select image if we're in "select mode"
if (NumItemsSelected > 0)
{
item.IsSelected = !item.IsSelected;
}
else
{
await ShowImageDialog(item);
}
}
public async Task ShowImageDialog(OutputImageViewModel item)
{
var currentIndex = Outputs.IndexOf(item);
var image = new ImageSource(new FilePath(item.AbsolutePath));
var image = new ImageSource(new FilePath(item.ImageFile.AbsolutePath));
// Preload
await image.GetBitmapAsync();
var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item };
var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item.ImageFile };
using var onNext = Observable
.FromEventPattern<DirectionalNavigationEventArgs>(
@ -180,14 +217,14 @@ public partial class OutputsPageViewModel : PageViewModelBase
{
var newImage = Outputs[newIndex];
var newImageSource = new ImageSource(
new FilePath(newImage.AbsolutePath)
new FilePath(newImage.ImageFile.AbsolutePath)
);
// Preload
await newImageSource.GetBitmapAsync();
sender.ImageSource = newImageSource;
sender.LocalImageFile = newImage;
sender.LocalImageFile = newImage.ImageFile;
currentIndex = newIndex;
}
@ -207,9 +244,22 @@ public partial class OutputsPageViewModel : PageViewModelBase
public async Task OpenImage(string imagePath) => await ProcessRunner.OpenFileBrowser(imagePath);
public async Task DeleteImage(LocalImageFile? item)
public async Task DeleteImage(OutputImageViewModel? item)
{
if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
var confirmationDialog = new BetterContentDialog
{
Title = "Are you sure you want to delete this image?",
Content = "This action cannot be undone.",
PrimaryButtonText = Resources.Action_Delete,
SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary,
IsSecondaryButtonEnabled = true,
};
var dialogResult = await confirmationDialog.ShowAsync();
if (dialogResult != ContentDialogResult.Primary)
return;
if (item?.ImageFile.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
{
return;
}
@ -223,7 +273,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
return;
}
Outputs.Remove(item);
OutputsCache.Remove(item);
// Invalidate cache
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
@ -244,6 +294,67 @@ public partial class OutputsPageViewModel : PageViewModelBase
EventManager.Instance.OnInferenceUpscaleRequested(image);
}
public void ClearSelection()
{
foreach (var output in Outputs)
{
output.IsSelected = false;
}
}
public void SelectAll()
{
foreach (var output in Outputs)
{
output.IsSelected = true;
}
}
public async Task DeleteAllSelected()
{
var confirmationDialog = new BetterContentDialog
{
Title = $"Are you sure you want to delete {NumItemsSelected} images?",
Content = "This action cannot be undone.",
PrimaryButtonText = Resources.Action_Delete,
SecondaryButtonText = Resources.Action_Cancel,
DefaultButton = ContentDialogButton.Primary,
IsSecondaryButtonEnabled = true,
};
var dialogResult = await confirmationDialog.ShowAsync();
if (dialogResult != ContentDialogResult.Primary)
return;
var selected = Outputs.Where(o => o.IsSelected).ToList();
Debug.Assert(selected.Count == NumItemsSelected);
foreach (var output in selected)
{
if (output?.ImageFile.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
{
continue;
}
// Delete the file
var imageFile = new FilePath(imagePath);
var result = await notificationService.TryAsync(imageFile.DeleteAsync());
if (!result.IsSuccessful)
{
continue;
}
OutputsCache.Remove(output);
// Invalidate cache
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
loader.RemoveAllNamesFromCache(imageFile.Name);
}
}
NumItemsSelected = 0;
ClearSelection();
}
private void GetOutputs(string directory)
{
if (!settingsManager.IsLibraryDirSet)
@ -257,9 +368,9 @@ public partial class OutputsPageViewModel : PageViewModelBase
var list = Directory
.EnumerateFiles(directory, "*.png", SearchOption.AllDirectories)
.Select(file => LocalImageFile.FromPath(file))
.OrderByDescending(f => f.CreatedAt);
.Select(file => new OutputImageViewModel(LocalImageFile.FromPath(file)))
.OrderByDescending(f => f.ImageFile.CreatedAt);
OutputsCache.EditDiff(list, (x, y) => x.AbsolutePath == y.AbsolutePath);
OutputsCache.EditDiff(list, (x, y) => x.ImageFile.AbsolutePath == y.ImageFile.AbsolutePath);
}
}

41
StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml

@ -26,6 +26,7 @@
FontSize="24"
Margin="0, 16, 0, 4" />
<TextBlock Grid.Row="1" Text="{Binding SelectedPackage.Blurb}"
TextWrapping="Wrap"
Margin="0, 0, 0, 4" />
<TextBlock Grid.Row="2" Text="{Binding SelectedPackage.Disclaimer}"
Margin="0, 0, 0, 4"
@ -206,24 +207,28 @@
</UniformGrid>
<!-- Available Packages -->
<ListBox Grid.Column="1" Grid.Row="0"
Margin="8, 16"
ItemsSource="{Binding AvailablePackages}"
SelectedItem="{Binding SelectedPackage}">
<ListBox.Template>
<ControlTemplate>
<ItemsPresenter />
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type packages:BasePackage}">
<StackPanel Margin="8">
<TextBlock Text="{Binding DisplayName}" />
<TextBlock Text="{Binding ByAuthor}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ScrollViewer Grid.Column="1" Grid.Row="0"
Margin="8, 16"
MaxHeight="400"
VerticalScrollBarVisibility="Visible">
<ListBox
ItemsSource="{Binding AvailablePackages}"
SelectedItem="{Binding SelectedPackage}">
<ListBox.Template>
<ControlTemplate>
<ItemsPresenter />
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type packages:BasePackage}">
<StackPanel Margin="8">
<TextBlock Text="{Binding DisplayName}" />
<TextBlock Text="{Binding ByAuthor}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
</Grid>
</controls:UserControlBase>

64
StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml

@ -37,8 +37,10 @@
Title="Use ComfyUI with Inference"
Subtitle="A new built-in native Stable Diffusion experience, powered by ComfyUI"
ActionButtonContent="{x:Static lang:Resources.Action_Install}"
ActionButtonCommand="{Binding InstallComfyForInferenceCommand}"
CloseButtonContent="{x:Static lang:Resources.Action_Close}"
PreferredPlacement="RightTop"
Margin="8,0,0,0"
PlacementMargin="0,0,0,0"
TailVisibility="Auto">
<ui:TeachingTip.HeroContent>
@ -68,6 +70,7 @@
FontSize="24"
Margin="16, 16, 0, 4"/>
<TextBlock Text="{Binding SelectedPackage.Blurb}"
TextWrapping="Wrap"
Margin="16, 0, 0, 8"/>
<TextBlock Text="{Binding SelectedPackage.Disclaimer}"
Padding="4"
@ -124,34 +127,39 @@
</StackPanel>
<ListBox Grid.Column="1" Grid.Row="1"
Name="PackagesListBox"
Margin="8, 16"
IsVisible="{Binding ShowInstallButton}"
ItemsSource="{Binding AllPackages}"
SelectedItem="{Binding SelectedPackage}">
<ListBox.Template>
<ControlTemplate>
<ItemsPresenter/>
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type packages:BasePackage}">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Margin="8">
<TextBlock Text="{Binding DisplayName}"/>
<TextBlock Text="{Binding ByAuthor}"/>
</StackPanel>
<ui:InfoBadge
IsVisible="{Binding IsInferenceCompatible}"
Grid.Column="1"
Margin="16,0,0,0"
Classes="Success Dot"
VerticalAlignment="Center" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ScrollViewer Grid.Column="1" Grid.Row="1"
MaxHeight="400"
VerticalScrollBarVisibility="Visible"
Margin="0, 16">
<ListBox
Name="PackagesListBox"
Margin="8,0"
IsVisible="{Binding ShowInstallButton}"
ItemsSource="{Binding AllPackages}"
SelectedItem="{Binding SelectedPackage}">
<ListBox.Template>
<ControlTemplate>
<ItemsPresenter/>
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type packages:BasePackage}">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Margin="8">
<TextBlock Text="{Binding DisplayName}"/>
<TextBlock Text="{Binding ByAuthor}"/>
</StackPanel>
<ui:InfoBadge
IsVisible="{Binding IsInferenceCompatible}"
Grid.Column="1"
Margin="8,0,0,0"
Classes="Success Dot"
VerticalAlignment="Center" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
</Grid>
</UserControl>

7
StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml.cs

@ -30,7 +30,12 @@ public partial class OneClickInstallDialog : UserControl
var teachingTip =
this.FindControl<TeachingTip>("InferenceTeachingTip")
?? throw new InvalidOperationException("TeachingTip not found");
;
teachingTip.ActionButtonClick += (_, _) =>
{
teachingTip.IsOpen = false;
};
// Find ComfyUI listbox item
var listBox = this.FindControl<ListBox>("PackagesListBox");

190
StabilityMatrix.Avalonia/Views/OutputsPage.axaml

@ -11,9 +11,10 @@
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:models1="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:database="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core"
xmlns:fluentAvalonia="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:outputsPage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.OutputsPage"
xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
d:DataContext="{x:Static mocks:DesignData.OutputsPageViewModel}"
d:DesignHeight="450"
d:DesignWidth="700"
@ -21,41 +22,95 @@
x:DataType="vm:OutputsPageViewModel"
mc:Ignorable="d">
<Grid RowDefinitions="Auto, *" Margin="16">
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,16"
HorizontalAlignment="Left">
<Grid RowDefinitions="Auto, *">
<TextBlock Text="Output Folder" Margin="4"/>
<ComboBox Grid.Row="1" ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
MinWidth="150">
<ComboBox.Styles>
<Style
Selector="ComboBox /template/ ContentControl#ContentPresenter &gt; StackPanel &gt; TextBlock:nth-child(2)">
<Setter Property="IsVisible" Value="False" />
</Style>
</ComboBox.Styles>
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type models1:PackageOutputCategory}">
<StackPanel>
<TextBlock
Margin="0,4,0,4"
Text="{Binding Name, Mode=OneWay}" />
<TextBlock Text="{Binding Path, Mode=OneWay}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
<Grid RowDefinitions="Auto, *" Margin="8,0"
IsVisible="{Binding CanShowOutputTypes}">
<TextBlock Text="Output Type" Margin="4"/>
<ComboBox Grid.Row="1" ItemsSource="{Binding OutputTypes}"
SelectedItem="{Binding SelectedOutputType}"
MinWidth="150"
VerticalAlignment="Stretch"
VerticalContentAlignment="Center"/>
</Grid>
</StackPanel>
<Grid Grid.Row="0" Margin="0,0,0,16"
HorizontalAlignment="Stretch"
RowDefinitions="Auto, Auto"
ColumnDefinitions="Auto, Auto, Auto, *, Auto, Auto">
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="{x:Static lang:Resources.Label_OutputFolder}"
Margin="4" />
<ComboBox Grid.Column="0"
Grid.Row="1" ItemsSource="{Binding Categories}"
CornerRadius="8"
Margin="4,0"
SelectedItem="{Binding SelectedCategory}"
VerticalAlignment="Stretch"
MinWidth="150">
<ComboBox.Styles>
<Style
Selector="ComboBox /template/ ContentControl#ContentPresenter &gt; StackPanel &gt; TextBlock:nth-child(2)">
<Setter Property="IsVisible" Value="False" />
</Style>
</ComboBox.Styles>
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type models1:PackageOutputCategory}">
<StackPanel>
<TextBlock
Margin="0,4,0,4"
Text="{Binding Name, Mode=OneWay}" />
<TextBlock Text="{Binding Path, Mode=OneWay}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Row="0"
Grid.Column="1"
Text="{x:Static lang:Resources.Label_OutputType}"
Margin="4"
IsVisible="{Binding CanShowOutputTypes}" />
<ComboBox Grid.Column="1" Grid.Row="1" ItemsSource="{Binding OutputTypes}"
IsVisible="{Binding CanShowOutputTypes}"
CornerRadius="8"
SelectedItem="{Binding SelectedOutputType}"
MinWidth="150"
Margin="4,0"
VerticalAlignment="Stretch"
VerticalContentAlignment="Center" />
<TextBlock Grid.Row="1"
Grid.Column="3"
IsVisible="{Binding !!NumItemsSelected}"
FontSize="16"
Margin="8, 0"
VerticalAlignment="Center"
TextAlignment="Center"
HorizontalAlignment="Right"
Text="{Binding NumImagesSelected, FallbackValue=1234 images selected}" />
<Button Grid.Row="1"
Grid.Column="4"
VerticalAlignment="Bottom"
CornerRadius="8"
Padding="12, 0"
Height="40"
Classes="danger"
Command="{Binding DeleteAllSelected}"
IsVisible="{Binding !!NumItemsSelected}" >
<avalonia:Icon Value="fa-solid fa-trash-can"/>
</Button>
<Button Grid.Row="1"
Grid.Column="5"
Content="{x:Static lang:Resources.Action_ClearSelection}"
VerticalAlignment="Bottom"
CornerRadius="8"
Margin="8, 0"
Height="40"
Command="{Binding ClearSelection}"
IsVisible="{Binding !!NumItemsSelected}" />
<Button Grid.Row="1"
Grid.Column="5"
Content="{x:Static lang:Resources.Action_SelectAll}"
VerticalAlignment="Bottom"
Classes="accent"
CornerRadius="8"
Margin="8, 0"
Height="40"
Command="{Binding SelectAll}"
IsVisible="{Binding !NumItemsSelected}" />
</Grid>
<ScrollViewer Grid.Row="1">
<ItemsRepeater
@ -65,65 +120,66 @@
<UniformGridLayout MinColumnSpacing="16" MinRowSpacing="16" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type database:LocalImageFile}">
<Button
Margin="0"
Padding="4"
<DataTemplate DataType="{x:Type outputsPage:OutputImageViewModel}">
<selectableImageCard:SelectableImageButton
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).OnImageClick}"
CommandParameter="{Binding }">
<controls:BetterAdvancedImage
Width="300"
Height="300"
Stretch="UniformToFill"
Source="{Binding AbsolutePath}" />
<Button.ContextFlyout>
CommandParameter="{Binding }"
IsSelected="{Binding IsSelected}"
Source="{Binding ImageFile.AbsolutePath}">
<selectableImageCard:SelectableImageButton.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="{x:Static lang:Resources.Action_Delete}"
IconSource="Delete"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).DeleteImage}"
CommandParameter="{Binding }" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="{x:Static lang:Resources.Action_Copy}"
IconSource="Copy"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).CopyImage}"
CommandParameter="{Binding AbsolutePath}" />
CommandParameter="{Binding ImageFile.AbsolutePath}" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="{x:Static lang:Resources.Action_OpenInExplorer}"
IconSource="Folder"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).OpenImage}"
CommandParameter="{Binding AbsolutePath}" />
CommandParameter="{Binding ImageFile.AbsolutePath}" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="{x:Static lang:Resources.Action_OpenInViewer}"
IconSource="Image"
IsVisible="{Binding !!$parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).NumItemsSelected}"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).ShowImageDialog}"
CommandParameter="{Binding }" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="{x:Static lang:Resources.Action_Delete}"
IconSource="Delete"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).DeleteImage}"
CommandParameter="{Binding }" />
<ui:MenuFlyoutSeparator
IsVisible="{Binding GenerationParameters, Converter={x:Static ObjectConverters.IsNotNull}}" />
IsVisible="{Binding ImageFile.GenerationParameters, Converter={x:Static ObjectConverters.IsNotNull}}" />
<ui:MenuFlyoutSubItem Text="Send to Inference" IconSource="Share"
IsVisible="{Binding GenerationParameters, Converter={x:Static ObjectConverters.IsNotNull}}">
<ui:MenuFlyoutSubItem Text="{x:Static lang:Resources.Action_SendToInference}"
IconSource="Share"
IsVisible="{Binding ImageFile.GenerationParameters, Converter={x:Static ObjectConverters.IsNotNull}}">
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="Text to Image"
Text="{x:Static lang:Resources.Label_TextToImage}"
IconSource="FullScreenMaximize"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).SendToTextToImage}"
CommandParameter="{Binding }" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="Image to Image"
Text="{x:Static lang:Resources.Label_ImageToImage}"
IsEnabled="False"
IconSource="ImageCopy"
CommandParameter="{Binding }" />
<ui:MenuFlyoutItem
Text="Inpainting"
Text="{x:Static lang:Resources.Label_Inpainting}"
IconSource="ImageEdit"
IsEnabled="False"
HotKey="{x:Null}"
CommandParameter="{Binding }" />
<ui:MenuFlyoutItem
Text="Upscale"
Text="{x:Static lang:Resources.Label_Upscale}"
HotKey="{x:Null}"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).SendToUpscale}"
CommandParameter="{Binding }">
@ -135,8 +191,8 @@
</ui:MenuFlyoutItem>
</ui:MenuFlyoutSubItem>
</ui:FAMenuFlyout>
</Button.ContextFlyout>
</Button>
</selectableImageCard:SelectableImageButton.ContextFlyout>
</selectableImageCard:SelectableImageButton>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>

33
StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml

@ -79,20 +79,41 @@
<MenuItem Header="Symlink"
Command="{Binding ToggleSharedModelSymlink}">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding IsSharedModelSymlink}"/>
<CheckBox IsChecked="{Binding IsSharedModelSymlink}"
Margin="8,0,0,0"
Padding="0"
Width="28" Height="28">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5"/>
</CheckBox.RenderTransform>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Config"
Command="{Binding ToggleSharedModelConfig}"
IsVisible="{Binding CanUseConfigMethod}">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding IsSharedModelConfig}"/>
<CheckBox Margin="8,0,0,0"
Padding="0"
Width="28" Height="28"
IsChecked="{Binding IsSharedModelConfig}">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5"/>
</CheckBox.RenderTransform>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="None"
Command="{Binding ToggleSharedModelNone}">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding IsSharedModelDisabled}"/>
<CheckBox IsChecked="{Binding IsSharedModelDisabled}"
Margin="8,0,0,0"
Padding="0"
Width="28" Height="28">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5"/>
</CheckBox.RenderTransform>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<!-- ReSharper enable Xaml.RedundantResource -->
@ -104,7 +125,11 @@
<CheckBox Margin="8,0,0,0"
Padding="0"
Width="28" Height="28"
IsChecked="{Binding UseSharedOutput}"/>
IsChecked="{Binding UseSharedOutput}">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5"/>
</CheckBox.RenderTransform>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<Separator />

2
StabilityMatrix.Core/Models/Packages/BasePackage.cs

@ -187,7 +187,7 @@ public abstract class BasePackage
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsCuda, onConsoleOutput)
.ConfigureAwait(false);
await venvRunner.PipInstall("xformers", onConsoleOutput).ConfigureAwait(false);
await venvRunner.PipInstall("xformers==0.0.20", onConsoleOutput).ConfigureAwait(false);
}
protected Task InstallDirectMlTorch(

289
StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs

@ -0,0 +1,289 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
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;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.Packages;
public class StableDiffusionUx : BaseGitPackage
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
public override string Name => "stable-diffusion-webui-ux";
public override string DisplayName { get; set; } = "Stable Diffusion Web UI-UX";
public override string Author => "anapnoe";
public override string LicenseType => "AGPL-3.0";
public override string LicenseUrl =>
"https://github.com/anapnoe/stable-diffusion-webui-ux/blob/master/LICENSE.txt";
public override string Blurb =>
"A pixel perfect design, mobile friendly, customizable interface that adds accessibility, "
+ "ease of use and extended functionallity to the stable diffusion web ui.";
public override string LaunchCommand => "launch.py";
public override Uri PreviewImageUri =>
new(
"https://user-images.githubusercontent.com/124302297/227973574-6003142d-0c7c-41c6-9966-0792a94549e9.png"
);
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public StableDiffusionUx(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{
[SharedFolderType.StableDiffusion] = new[] { "models/Stable-diffusion" },
[SharedFolderType.ESRGAN] = new[] { "models/ESRGAN" },
[SharedFolderType.RealESRGAN] = new[] { "models/RealESRGAN" },
[SharedFolderType.SwinIR] = new[] { "models/SwinIR" },
[SharedFolderType.Lora] = new[] { "models/Lora" },
[SharedFolderType.LyCORIS] = new[] { "models/LyCORIS" },
[SharedFolderType.ApproxVAE] = new[] { "models/VAE-approx" },
[SharedFolderType.VAE] = new[] { "models/VAE" },
[SharedFolderType.DeepDanbooru] = new[] { "models/deepbooru" },
[SharedFolderType.Karlo] = new[] { "models/karlo" },
[SharedFolderType.TextualInversion] = new[] { "embeddings" },
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" },
[SharedFolderType.ControlNet] = new[] { "models/ControlNet" },
[SharedFolderType.Codeformer] = new[] { "models/Codeformer" },
[SharedFolderType.LDSR] = new[] { "models/LDSR" },
[SharedFolderType.AfterDetailer] = new[] { "models/adetailer" }
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new()
{
[SharedOutputType.Extras] = new[] { "outputs/extras-images" },
[SharedOutputType.Saved] = new[] { "log/images" },
[SharedOutputType.Img2Img] = new[] { "outputs/img2img-images" },
[SharedOutputType.Text2Img] = new[] { "outputs/text2img-images" },
[SharedOutputType.Img2ImgGrids] = new[] { "outputs/img2img-grids" },
[SharedOutputType.Text2ImgGrids] = new[] { "outputs/text2img-grids" }
};
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
{
new()
{
Name = "Host",
Type = LaunchOptionType.String,
DefaultValue = "localhost",
Options = new() { "--server-name" }
},
new()
{
Name = "Port",
Type = LaunchOptionType.String,
DefaultValue = "7860",
Options = new() { "--port" }
},
new()
{
Name = "VRAM",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper
.IterGpuInfo()
.Select(gpu => gpu.MemoryLevel)
.Max() switch
{
Level.Low => "--lowvram",
Level.Medium => "--medvram",
_ => null
},
Options = new() { "--lowvram", "--medvram", "--medvram-sdxl" }
},
new()
{
Name = "Xformers",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.HasNvidiaGpu(),
Options = new() { "--xformers" }
},
new()
{
Name = "API",
Type = LaunchOptionType.Bool,
InitialValue = true,
Options = new() { "--api" }
},
new()
{
Name = "Auto Launch Web UI",
Type = LaunchOptionType.Bool,
InitialValue = false,
Options = new() { "--autolaunch" }
},
new()
{
Name = "Skip Torch CUDA Check",
Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu(),
Options = new() { "--skip-torch-cuda-test" }
},
new()
{
Name = "Skip Python Version Check",
Type = LaunchOptionType.Bool,
InitialValue = true,
Options = new() { "--skip-python-version-check" }
},
new()
{
Name = "No Half",
Type = LaunchOptionType.Bool,
Description = "Do not switch the model to 16-bit floats",
InitialValue = HardwareHelper.HasAmdGpu(),
Options = new() { "--no-half" }
},
new()
{
Name = "Skip SD Model Download",
Type = LaunchOptionType.Bool,
InitialValue = false,
Options = new() { "--no-download-sd-model" }
},
new()
{
Name = "Skip Install",
Type = LaunchOptionType.Bool,
Options = new() { "--skip-install" }
},
LaunchOptionDefinition.Extras
};
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm };
public override Task<string> GetLatestVersion() => Task.FromResult("master");
public override bool ShouldIgnoreReleases => true;
public override string OutputFolderName => "outputs";
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true));
// Setup venv
await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
venvRunner.WorkingDirectory = installLocation;
await venvRunner.Setup(true, onConsoleOutput).ConfigureAwait(false);
switch (torchVersion)
{
case TorchVersion.Cpu:
await InstallCpuTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
break;
case TorchVersion.Cuda:
await InstallCudaTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
break;
case TorchVersion.Rocm:
await InstallRocmTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
break;
case TorchVersion.DirectMl:
await InstallDirectMlTorch(venvRunner, progress, onConsoleOutput)
.ConfigureAwait(false);
break;
default:
throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null);
}
// Install requirements file
progress?.Report(
new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true)
);
Logger.Info("Installing requirements_versions.txt");
var requirements = new FilePath(installLocation, "requirements_versions.txt");
await venvRunner
.PipInstallFromRequirements(requirements, onConsoleOutput, excludes: "torch")
.ConfigureAwait(false);
progress?.Report(
new ProgressReport(1f, "Installing Package Requirements", isIndeterminate: false)
);
progress?.Report(new ProgressReport(-1f, "Updating configuration", isIndeterminate: true));
// // Create and add {"show_progress_type": "TAESD"} to config.json
// // Only add if the file doesn't exist
// var configPath = Path.Combine(installLocation, "config.json");
// if (!File.Exists(configPath))
// {
// var config = new JsonObject { { "show_progress_type", "TAESD" } };
// await File.WriteAllTextAsync(configPath, config.ToString()).ConfigureAwait(false);
// }
progress?.Report(new ProgressReport(1f, "Install complete", isIndeterminate: false));
}
public override async Task RunPackage(
string installedPackagePath,
string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
)
{
await SetupVenv(installedPackagePath).ConfigureAwait(false);
void HandleConsoleOutput(ProcessOutput s)
{
onConsoleOutput?.Invoke(s);
if (!s.Text.Contains("Running on", StringComparison.OrdinalIgnoreCase))
return;
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)");
var match = regex.Match(s.Text);
if (!match.Success)
return;
WebUrl = match.Value;
OnStartupComplete(WebUrl);
}
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}";
VenvRunner.RunDetached(args.TrimEnd(), HandleConsoleOutput, OnExit);
}
private async Task InstallRocmTorch(
PyVenvRunner venvRunner,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true)
);
await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false);
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm511, onConsoleOutput)
.ConfigureAwait(false);
}
}
Loading…
Cancel
Save