Browse Source

Merge branch 'dev' into downmerge

pull/240/head
Ionite 1 year ago committed by GitHub
parent
commit
9c138ee436
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 24
      CHANGELOG.md
  2. 1
      StabilityMatrix.Avalonia/App.axaml
  3. 9
      StabilityMatrix.Avalonia/App.axaml.cs
  4. 38
      StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs
  5. 6
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs
  6. 22
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs
  7. 8
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs
  8. 206
      StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml
  9. 190
      StabilityMatrix.Avalonia/Controls/ModelCard.axaml
  10. 51
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.axaml
  11. 25
      StabilityMatrix.Avalonia/Controls/SelectableImageCard/SelectableImageButton.cs
  12. 30
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  13. 68
      StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs
  14. 12
      StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs
  15. 14
      StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs
  16. 47
      StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs
  17. 11
      StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs
  18. 6
      StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs
  19. 152
      StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs
  20. 135
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  21. 47
      StabilityMatrix.Avalonia/Languages/Resources.resx
  22. 73
      StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs
  23. 16
      StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs
  24. 192
      StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs
  25. 8
      StabilityMatrix.Avalonia/Models/Inference/FileNameFormatVar.cs
  26. 7
      StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs
  27. 13
      StabilityMatrix.Avalonia/Program.cs
  28. 45
      StabilityMatrix.Avalonia/Services/InferenceClientManager.cs
  29. 3
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  30. 6
      StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs
  31. 194
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
  32. 3
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
  33. 98
      StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs
  34. 170
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
  35. 51
      StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs
  36. 4
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs
  37. 26
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
  38. 6
      StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
  39. 2
      StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs
  40. 78
      StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
  41. 19
      StabilityMatrix.Avalonia/ViewModels/OutputsPage/OutputImageViewModel.cs
  42. 402
      StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
  43. 55
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  44. 4
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  45. 6
      StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs
  46. 17
      StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs
  47. 64
      StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
  48. 14
      StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml
  49. 178
      StabilityMatrix.Avalonia/Views/Dialogs/InstallerDialog.axaml
  50. 190
      StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml
  51. 7
      StabilityMatrix.Avalonia/Views/Dialogs/OneClickInstallDialog.axaml.cs
  52. 5
      StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml
  53. 213
      StabilityMatrix.Avalonia/Views/OutputsPage.axaml
  54. 14
      StabilityMatrix.Avalonia/Views/OutputsPage.axaml.cs
  55. 42
      StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml
  56. 46
      StabilityMatrix.Avalonia/Views/SettingsPage.axaml
  57. 9
      StabilityMatrix.Core/Helper/EventManager.cs
  58. 13
      StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs
  59. 206
      StabilityMatrix.Core/Helper/PrerequisiteHelper.cs
  60. 29
      StabilityMatrix.Core/Helper/SharedFolders.cs
  61. 58
      StabilityMatrix.Core/Models/Database/LocalImageFile.cs
  62. 1
      StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs
  63. 17
      StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs
  64. 20
      StabilityMatrix.Core/Models/GenerationParameters.cs
  65. 1
      StabilityMatrix.Core/Models/InstalledPackage.cs
  66. 1
      StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs
  67. 13
      StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs
  68. 1
      StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs
  69. 11
      StabilityMatrix.Core/Models/PackageModification/UpdatePackageStep.cs
  70. 22
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  71. 224
      StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs
  72. 15
      StabilityMatrix.Core/Models/Packages/BasePackage.cs
  73. 36
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  74. 20
      StabilityMatrix.Core/Models/Packages/DankDiffusion.cs
  75. 67
      StabilityMatrix.Core/Models/Packages/Fooocus.cs
  76. 10
      StabilityMatrix.Core/Models/Packages/FooocusMre.cs
  77. 150
      StabilityMatrix.Core/Models/Packages/InvokeAI.cs
  78. 247
      StabilityMatrix.Core/Models/Packages/StableDiffusionDirectMl.cs
  79. 267
      StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs
  80. 20
      StabilityMatrix.Core/Models/Packages/UnknownPackage.cs
  81. 35
      StabilityMatrix.Core/Models/Packages/VladAutomatic.cs
  82. 16
      StabilityMatrix.Core/Models/Packages/VoltaML.cs
  83. 5
      StabilityMatrix.Core/Models/Settings/Settings.cs
  84. 12
      StabilityMatrix.Core/Models/SharedOutputType.cs
  85. 45
      StabilityMatrix.Core/Python/PyVenvRunner.cs
  86. 10
      StabilityMatrix.Core/Services/IImageIndexService.cs
  87. 130
      StabilityMatrix.Core/Services/ImageIndexService.cs
  88. 28
      StabilityMatrix.Tests/Avalonia/FileNameFormatProviderTests.cs
  89. 24
      StabilityMatrix.Tests/Avalonia/FileNameFormatTests.cs

24
CHANGELOG.md

@ -5,6 +5,30 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.6.0
### Added
- Added **Output Sharing** option for all packages in the three-dots menu on the Packages page
- This will link the package's output folders to the relevant subfolders in the "Outputs" directory
- When a package only has a generic "outputs" folder, all generated images from that package will be linked to the "Outputs\Text2Img" folder when this option is enabled
- Added **Outputs page** for viewing generated images from any package, or the shared output folder
- Added [Stable Diffusion WebUI/UX](https://github.com/anapnoe/stable-diffusion-webui-ux) package
- Added [Stable Diffusion WebUI-DirectML](https://github.com/lshqqytiger/stable-diffusion-webui-directml) package
- Added GPU compatibility badges to the installers
- Added filtering of "incompatible" packages (ones that do not support your GPU) to all installers
- This can be overridden by checking the new "Show All Packages" checkbox
- Added more launch options for Fooocus, such as the `--preset` option
### Changed
- If ComfyUI for Inference is chosen during the One-Click Installer, the Inference page will be opened after installation instead of the Launch page
- Changed all package installs & updates to use git commands instead of downloading zip files
- The One-Click Installer now uses the new progress dialog with console
- NVIDIA GPU users will be updated to use CUDA 12.1 for ComfyUI & Fooocus packages for a slight performance improvement
- Update will occur the next time the package is updated, or on a fresh install
- Note: CUDA 12.1 is only available on Maxwell (GTX 900 series) and newer GPUs
### 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
- Fixed Inference Prompt Completion window sometimes not showing while typing
## v2.5.5
### Added
- Added Spanish UI language options, thanks to Carlos Baena and Lautaroturina for the translations

1
StabilityMatrix.Avalonia/App.axaml

@ -60,6 +60,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}" />

9
StabilityMatrix.Avalonia/App.axaml.cs

@ -259,7 +259,8 @@ public sealed class App : Application
.AddSingleton<LaunchPageViewModel>()
.AddSingleton<ProgressManagerViewModel>()
.AddSingleton<InferenceViewModel>()
.AddSingleton<ProgressManagerViewModel>();
.AddSingleton<ProgressManagerViewModel>()
.AddSingleton<OutputsPageViewModel>();
services.AddSingleton<MainWindowViewModel>(
provider =>
@ -278,6 +279,7 @@ public sealed class App : Application
provider.GetRequiredService<PackageManagerViewModel>(),
provider.GetRequiredService<CheckpointsPageViewModel>(),
provider.GetRequiredService<CheckpointBrowserViewModel>(),
provider.GetRequiredService<OutputsPageViewModel>()
},
FooterPages = { provider.GetRequiredService<SettingsViewModel>() }
}
@ -388,6 +390,7 @@ public sealed class App : Application
services.AddSingleton<ProgressManagerPage>();
services.AddSingleton<InferencePage>();
services.AddSingleton<NewCheckpointsPage>();
services.AddSingleton<OutputsPage>();
// Inference tabs
services.AddTransient<InferenceTextToImageView>();
@ -430,10 +433,12 @@ public sealed class App : Application
internal static void ConfigurePackages(IServiceCollection services)
{
services.AddSingleton<BasePackage, A3WebUI>();
services.AddSingleton<BasePackage, StableDiffusionUx>();
services.AddSingleton<BasePackage, StableDiffusionDirectMl>();
services.AddSingleton<BasePackage, Fooocus>();
services.AddSingleton<BasePackage, VladAutomatic>();
services.AddSingleton<BasePackage, InvokeAI>();
services.AddSingleton<BasePackage, ComfyUI>();
services.AddSingleton<BasePackage, VladAutomatic>();
services.AddSingleton<BasePackage, VoltaML>();
services.AddSingleton<BasePackage, FooocusMre>();
}

38
StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs

@ -1,6 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
@ -100,6 +101,7 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
return window;
}
[MethodImpl(MethodImplOptions.Synchronized)]
public void InvokeManualCompletion()
{
if (CompletionProvider is null)
@ -109,11 +111,13 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
// If window already open, skip since handled by completion window
// Unless this is an end char, where we'll open a new window
if (completionWindow is { IsOpen: true })
if (completionWindow is { ToolTipIsOpen: true })
{
Logger.ConditionalTrace("Skipping, completion window already open");
return;
}
completionWindow?.Hide();
completionWindow = null;
// Get the segment of the token the caret is currently in
if (GetCaretCompletionToken() is not { } completionRequest)
@ -138,23 +142,37 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
var token = textEditor.Document.GetText(tokenSegment);
Logger.ConditionalTrace("Using token {Token} ({@Segment})", token, tokenSegment);
completionWindow = CreateCompletionWindow(textEditor.TextArea);
completionWindow.StartOffset = tokenSegment.Offset;
completionWindow.EndOffset = tokenSegment.EndOffset;
var newWindow = CreateCompletionWindow(textEditor.TextArea);
newWindow.StartOffset = tokenSegment.Offset;
newWindow.EndOffset = tokenSegment.EndOffset;
completionWindow.UpdateQuery(completionRequest);
newWindow.UpdateQuery(completionRequest);
completionWindow.Closed += (_, _) =>
newWindow.Closed += CompletionWindow_OnClosed;
completionWindow = newWindow;
newWindow.Show();
}
private void CompletionWindow_OnClosed(object? sender, EventArgs e)
{
if (ReferenceEquals(sender, completionWindow))
{
completionWindow = null;
};
}
Logger.ConditionalTrace("Completion window closed");
completionWindow.Show();
if (sender is CompletionWindow window)
{
window.Closed -= CompletionWindow_OnClosed;
}
}
private void TextArea_TextEntered(object? sender, TextInputEventArgs e)
{
Logger.ConditionalTrace($"Text entered: {e.Text.ToRepr()}");
Logger.ConditionalTrace("Text entered: {Text}", e.Text);
if (!IsEnabled || CompletionProvider is null)
{
@ -174,7 +192,7 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
return;
}
InvokeManualCompletion();
Dispatcher.UIThread.Post(InvokeManualCompletion, DispatcherPriority.Input);
}
private void TextArea_KeyDown(object? sender, KeyEventArgs e)

6
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs

@ -202,11 +202,11 @@ public class CompletionList : TemplatedControl
{
case Key.Down:
e.Handled = true;
_listBox.SelectIndex(_listBox.SelectedIndex + 1);
_listBox.SelectNextIndexWithLoop();
break;
case Key.Up:
e.Handled = true;
_listBox.SelectIndex(_listBox.SelectedIndex - 1);
_listBox.SelectPreviousIndexWithLoop();
break;
case Key.PageDown:
e.Handled = true;
@ -325,7 +325,7 @@ public class CompletionList : TemplatedControl
return;
}
using var _ = new CodeTimer();
using var _ = CodeTimer.StartDebug();
if (_listBox == null)
{

22
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionListBox.cs

@ -91,6 +91,28 @@ public class CompletionListBox : ListBox
SelectedIndex = -1;
}
/// <summary>
/// Selects the next item. If the last item is already selected, selects the first item.
/// </summary>
public void SelectNextIndexWithLoop()
{
if (ItemCount <= 0)
return;
SelectIndex((SelectedIndex + 1) % ItemCount);
}
/// <summary>
/// Selects the previous item. If the first item is already selected, selects the last item.
/// </summary>
public void SelectPreviousIndexWithLoop()
{
if (ItemCount <= 0)
return;
SelectIndex((SelectedIndex - 1 + ItemCount) % ItemCount);
}
/// <summary>
/// Selects the item with the specified index and scrolls it into view.
/// </summary>

8
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs

@ -44,6 +44,8 @@ public class CompletionWindow : CompletionWindowBase
private PopupWithCustomPosition? _toolTip;
private ContentControl? _toolTipContent;
public bool ToolTipIsOpen => _toolTip is not null;
/// <summary>
/// Max number of items in the completion list.
/// </summary>
@ -93,7 +95,11 @@ public class CompletionWindow : CompletionWindowBase
LogicalChildren.Add(_toolTip);
//_toolTip.Closed += (o, e) => ((Popup)o).Child = null;
_toolTip.Closed += (s, e) =>
{
Hide();
// (s as Popup)!.Child = null;
};
AttachEvents();
}

206
StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml

@ -2,16 +2,12 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:fluentAvalonia="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:dbModels="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core"
xmlns:input="using:FluentAvalonia.UI.Input"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:input="using:FluentAvalonia.UI.Input"
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:dbModels="clr-namespace:StabilityMatrix.Core.Models.Database;assembly=StabilityMatrix.Core"
xmlns:animations="clr-namespace:StabilityMatrix.Avalonia.Animations"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:behaviors="clr-namespace:StabilityMatrix.Avalonia.Behaviors"
x:DataType="vmInference:ImageFolderCardViewModel">
<Design.PreviewWith>
@ -22,15 +18,10 @@
</Panel>
</Design.PreviewWith>
<!--<Style Selector="ListBox /template/ VirtualizingStackPanel">
<Setter Property="Orientation" Value="Horizontal" />
</Style>-->
<Style Selector="controls|ImageFolderCard">
<!-- Set Defaults -->
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Focusable" Value="True" />
<!-- Template -->
<Setter Property="Template">
<ControlTemplate>
@ -40,7 +31,7 @@
VerticalAlignment="{TemplateBinding VerticalAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalAlignment}">
<!--<controls:Card.Styles>
<Style Selector="ItemsRepeater">
<Setter Property="animations:ItemsRepeaterArrangeAnimation.EnableItemsArrangeAnimation" Value="True"/>
@ -50,155 +41,182 @@
<Grid RowDefinitions="Auto,*">
<TextBox
x:Name="SearchBox"
Text="{Binding SearchQuery}"
Margin="6,4,6,8"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Text="{Binding SearchQuery}"
Watermark="{x:Static lang:Resources.Label_SearchEllipsis}">
<TextBox.InnerRightContent>
<ui:SymbolIcon
<ui:SymbolIcon
Margin="0,0,6,0"
FontSize="16"
Symbol="Find" />
</TextBox.InnerRightContent>
</TextBox>
<ScrollViewer
Grid.Row="1"
IsScrollInertiaEnabled="True"
VerticalSnapPointsType="Mandatory"
<ScrollViewer
Grid.Row="1"
BringIntoViewOnFocusChange="False"
HorizontalScrollBarVisibility="Disabled">
HorizontalScrollBarVisibility="Disabled"
IsScrollInertiaEnabled="True"
VerticalSnapPointsType="Mandatory">
<ItemsRepeater
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
VerticalCacheLength="8"
ItemsSource="{Binding LocalImages}">
ItemsSource="{Binding LocalImages}"
VerticalCacheLength="8">
<ItemsRepeater.Resources>
<input:StandardUICommand
x:Key="ImageClickCommand"
Command="{Binding ImageClickCommand}"/>
<input:StandardUICommand
<input:StandardUICommand x:Key="ImageClickCommand" Command="{Binding ImageClickCommand}" />
<input:StandardUICommand
x:Key="ImageDeleteCommand"
Label="{x:Static lang:Resources.Action_Delete}"
Command="{Binding ImageDeleteCommand}"
IconSource="Delete"
Command="{Binding ImageDeleteCommand}"/>
<input:StandardUICommand
Label="{x:Static lang:Resources.Action_Delete}" />
<input:StandardUICommand
x:Key="ImageCopyCommand"
Label="Copy"
Command="{Binding ImageCopyCommand}"
IconSource="Copy"
Command="{Binding ImageCopyCommand}"/>
<input:StandardUICommand
x:Key="ImageOpenCommand"
Label="{OnPlatform Default={x:Static lang:Resources.Action_OpenInExplorer}, macOS={x:Static lang:Resources.Action_OpenInFinder}}"
Label="Copy" />
<input:StandardUICommand
x:Key="ImageOpenCommand"
Command="{Binding ImageOpenCommand}"
IconSource="Folder"
Command="{Binding ImageOpenCommand}"/>
<input:StandardUICommand
x:Key="ImageExportPngWithMetadataCommand"
Label="Png with Metadata"
Command="{Binding ImageExportPngWithMetadataCommand}"/>
<input:StandardUICommand
x:Key="ImageExportPngCommand"
Label="Png"
Command="{Binding ImageExportPngCommand}"/>
<input:StandardUICommand
x:Key="ImageExportJpegCommand"
Label="Jpeg"
Command="{Binding ImageExportJpegCommand}"/>
<input:StandardUICommand
x:Key="ImageExportWebpCommand"
Label="Webp"
Command="{Binding ImageExportWebpCommand}"/>
Label="{OnPlatform Default={x:Static lang:Resources.Action_OpenInExplorer},
macOS={x:Static lang:Resources.Action_OpenInFinder}}" />
<input:StandardUICommand
x:Key="ImageExportPngWithMetadataCommand"
Command="{Binding ImageExportPngWithMetadataCommand}"
Label="Png with Metadata" />
<input:StandardUICommand
x:Key="ImageExportPngCommand"
Command="{Binding ImageExportPngCommand}"
Label="Png" />
<input:StandardUICommand
x:Key="ImageExportJpegCommand"
Command="{Binding ImageExportJpegCommand}"
Label="Jpeg" />
<input:StandardUICommand
x:Key="ImageExportWebpCommand"
Command="{Binding ImageExportWebpCommand}"
Label="Webp" />
</ItemsRepeater.Resources>
<ItemsRepeater.Layout>
<!--<WrapLayout HorizontalSpacing="2" VerticalSpacing="2" />-->
<UniformGridLayout
Orientation="Horizontal"
MinColumnSpacing="2"
MinRowSpacing="2" />
MinRowSpacing="2"
Orientation="Horizontal" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type dbModels:LocalImageFile}">
<Button
ToolTip.Placement="LeftEdgeAlignedTop"
Padding="4"
Classes="transparent-full"
Command="{StaticResource ImageClickCommand}"
CommandParameter="{Binding $self.DataContext}"
CornerRadius="8"
HotKey="{x:Null}"
Padding="4"
Classes="transparent-full"
CornerRadius="8">
ToolTip.Placement="LeftEdgeAlignedTop">
<Interaction.Behaviors>
<BehaviorCollection>
<ContextDragBehavior HorizontalDragThreshold="6" VerticalDragThreshold="6"/>
<ContextDragBehavior HorizontalDragThreshold="6" VerticalDragThreshold="6" />
</BehaviorCollection>
</Interaction.Behaviors>
<Button.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem
HotKey="{x:Null}"
<ui:MenuFlyoutItem
Command="{StaticResource ImageDeleteCommand}"
CommandParameter="{Binding $self.DataContext}"/>
<ui:MenuFlyoutItem
HotKey="{x:Null}"
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutItem
Command="{StaticResource ImageCopyCommand}"
CommandParameter="{Binding $self.DataContext}"/>
<ui:MenuFlyoutSeparator/>
<ui:MenuFlyoutItem
HotKey="{x:Null}"
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutSeparator />
<ui:MenuFlyoutItem
Command="{StaticResource ImageOpenCommand}"
CommandParameter="{Binding $self.DataContext}"/>
<ui:MenuFlyoutSubItem Text="Export" IconSource="Share">
<ui:MenuFlyoutItem
HotKey="{x:Null}"
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutSubItem IconSource="Share" Text="Export">
<ui:MenuFlyoutItem
Command="{StaticResource ImageExportPngWithMetadataCommand}"
CommandParameter="{Binding $self.DataContext}"/>
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Command="{StaticResource ImageExportPngCommand}"
CommandParameter="{Binding $self.DataContext}"/>
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Command="{StaticResource ImageExportJpegCommand}"
CommandParameter="{Binding $self.DataContext}"/>
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Command="{StaticResource ImageExportWebpCommand}"
CommandParameter="{Binding $self.DataContext}"/>
CommandParameter="{Binding $self.DataContext}"
HotKey="{x:Null}" />
</ui:MenuFlyoutSubItem>
</ui:FAMenuFlyout>
</Button.ContextFlyout>
<ToolTip.Tip>
<TextBlock Text="{Binding GenerationParameters.PositivePrompt}"/>
<StackPanel Spacing="4">
<StackPanel.Styles>
<Style Selector="StackPanel > TextBlock">
<Setter Property="Theme" Value="{DynamicResource BodyTextBlockStyle}"/>
</Style>
<Style Selector="SelectableTextBlock">
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontFamily" Value="Cascadia Code,Consolas,Menlo,Monospace"/>
</Style>
<Style Selector="controls|Card">
<Setter Property="Padding" Value="4,4" />
</Style>
</StackPanel.Styles>
<TextBlock Text="{Binding FileName}" Theme="{DynamicResource BodyStrongTextBlockStyle}" />
<controls:Card>
<SelectableTextBlock Text="{Binding GenerationParameters.PositivePrompt, FallbackValue=''}" />
</controls:Card>
<controls:Card>
<SelectableTextBlock Text="{Binding GenerationParameters.NegativePrompt, FallbackValue=''}" />
</controls:Card>
<TextBlock Text="{Binding GenerationParameters.ModelName, FallbackValue=''}" />
<TextBlock Text="{Binding GenerationParameters.Sampler, FallbackValue=''}" />
<TextBlock Text="{Binding GenerationParameters.Seed, FallbackValue=''}" />
</StackPanel>
</ToolTip.Tip>
<Border ClipToBounds="True" CornerRadius="8">
<Grid RowDefinitions="*,Auto">
<controls:BetterAdvancedImage
Width="150"
Height="190"
Source="{Binding GlobalFullPath}"
Source="{Binding AbsolutePath}"
Stretch="UniformToFill"
StretchDirection="Both" />
<Border
Grid.Row="1"
Classes="theme-dark"
MinHeight="20"
MaxWidth="150"
Padding="4,0,0,0"
VerticalAlignment="Bottom"
Classes="theme-dark"
Opacity="0.9">
<TextBlock
FontSize="12"
<TextBlock
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
Text="{Binding FileNameWithoutExtension}"/>
FontSize="12"
Text="{Binding FileNameWithoutExtension}"
TextTrimming="CharacterEllipsis" />
</Border>
</Grid>
</Border>

190
StabilityMatrix.Avalonia/Controls/ModelCard.axaml

@ -1,41 +1,145 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
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">
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
x:DataType="inference:ModelCardViewModel">
<Design.PreviewWith>
<Panel Width="400" Height="200">
<StackPanel VerticalAlignment="Center" Width="300">
<controls:ModelCard DataContext="{x:Static mocks:DesignData.ModelCardViewModel}"/>
<StackPanel Width="300" VerticalAlignment="Center">
<controls:ModelCard DataContext="{x:Static mocks:DesignData.ModelCardViewModel}" />
</StackPanel>
</Panel>
</Design.PreviewWith>
<Style Selector="controls|ModelCard">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="12">
<Grid RowDefinitions="*,*,*" ColumnDefinitions="Auto,*,Auto">
<!-- Model -->
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="*,*,*">
<!-- Model -->
<TextBlock
Grid.Column="0"
TextAlignment="Left"
VerticalAlignment="Center"
MinWidth="60"
Text="{x:Static lang:Resources.Label_Model}" />
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Model}"
TextAlignment="Left" />
<ui:FAComboBox
Grid.Row="0"
Grid.Column="1"
IsTextSearchEnabled="True"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Models}"
DisplayMemberBinding="{Binding ShortDisplayName}"
SelectedItem="{Binding SelectedModel}">
<ui:FAComboBox.Styles>
<Style Selector="ui|FAComboBoxItem">
<Setter Property="ToolTip.Placement" Value="RightEdgeAlignedTop" />
<Setter Property="ToolTip.Tip">
<Template>
<StackPanel
x:DataType="models:HybridModelFile"
Orientation="Horizontal"
Spacing="6">
<!-- Image -->
<controls:BetterAdvancedImage
Width="64"
Height="96"
CornerRadius="6"
IsVisible="{Binding Local.PreviewImageFullPathGlobal, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=''}"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue=''}"
Stretch="UniformToFill"
StretchDirection="Both" />
<Grid RowDefinitions="Auto,Auto,*" VerticalAlignment="Stretch" MaxWidth="300">
<!-- Title -->
<TextBlock
Margin="0,0,0,4"
HorizontalAlignment="Left"
FontSize="14"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.ModelName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
<!-- Version -->
<TextBlock
Grid.Row="1"
Margin="0,0,0,8"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.VersionName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
<!-- Path -->
<TextBlock
Grid.Row="2"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding FileName}"
TextWrapping="Wrap" />
</Grid>
</StackPanel>
</Template>
</Setter>
</Style>
<Style Selector="ui|FAComboBox /template/ ContentControl#ContentPresenter &gt; StackPanel &gt; TextBlock:nth-child(2)">
<Setter Property="IsVisible" Value="False" />
</Style>
</ui:FAComboBox.Styles>
<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="models:HybridModelFile">
<StackPanel ToolTip.Placement="RightEdgeAlignedTop">
<!--<ToolTip.Tip>
<StackPanel Orientation="Horizontal" Spacing="6">
~1~ Image @1@
<controls:BetterAdvancedImage
RenderOptions.BitmapInterpolationMode="HighQuality"
Height="96"
Width="64"
CornerRadius="6"
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue=''}"
Stretch="UniformToFill"
StretchDirection="Both"
IsVisible="{Binding Local.PreviewImageFullPathGlobal, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=''}" />
<StackPanel Spacing="4">
~1~ Title @1@
<TextBlock
FontSize="14"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
HorizontalAlignment="Left"
Text="{Binding Local.ConnectedModelInfo.ModelName, FallbackValue=''}"
TextWrapping="WrapWithOverflow"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"/>
~1~ Version @1@
<TextBlock
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
HorizontalAlignment="Left"
Text="{Binding Local.ConnectedModelInfo.VersionName, FallbackValue=''}"
TextWrapping="WrapWithOverflow"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}" />
~1~ Path @1@
<TextBlock
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
HorizontalAlignment="Left"
Text="{Binding FileName}"
TextWrapping="WrapWithOverflow"/>
</StackPanel>
</StackPanel>
</ToolTip.Tip>-->
<TextBlock Text="{Binding ShortDisplayName}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ui:FAComboBox.ItemTemplate>
<!--<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="models:HybridModelFile">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*">
@ -70,67 +174,63 @@
<Button
Grid.Row="0"
Grid.Column="2"
Grid.Column="2"
Margin="8,0,0,0">
<ui:SymbolIcon FontSize="16" Symbol="Setting" />
<Button.Flyout>
<ui:FAMenuFlyout Placement="BottomEdgeAlignedLeft">
<ui:ToggleMenuFlyoutItem
IsChecked="{Binding IsRefinerSelectionEnabled}"
Text="{x:Static lang:Resources.Label_Refiner}"/>
<ui:ToggleMenuFlyoutItem
IsChecked="{Binding IsVaeSelectionEnabled}"
Text="{x:Static lang:Resources.Label_VAE}"/>
<ui:ToggleMenuFlyoutItem IsChecked="{Binding IsRefinerSelectionEnabled}" Text="{x:Static lang:Resources.Label_Refiner}" />
<ui:ToggleMenuFlyoutItem IsChecked="{Binding IsVaeSelectionEnabled}" Text="{x:Static lang:Resources.Label_VAE}" />
</ui:FAMenuFlyout>
</Button.Flyout>
</Button>
<!-- Refiner -->
<!-- Refiner -->
<TextBlock
Grid.Column="0"
Grid.Row="1"
Grid.Column="0"
MinWidth="60"
IsVisible="{Binding IsRefinerSelectionEnabled}"
Margin="0,8,0,0"
TextAlignment="Left"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Refiner}" />
IsVisible="{Binding IsRefinerSelectionEnabled}"
Text="{x:Static lang:Resources.Label_Refiner}"
TextAlignment="Left" />
<ui:FAComboBox
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
DisplayMemberBinding="{Binding ShortDisplayName}"
IsTextSearchEnabled="True"
IsVisible="{Binding IsRefinerSelectionEnabled}"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Models}"
DisplayMemberBinding="{Binding ShortDisplayName}"
SelectedItem="{Binding SelectedRefiner}" />
<!-- VAE -->
<!-- VAE -->
<TextBlock
Grid.Column="0"
Grid.Row="2"
Grid.Column="0"
MinWidth="60"
IsVisible="{Binding IsVaeSelectionEnabled}"
Margin="0,8,0,0"
TextAlignment="Left"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_VAE}" />
IsVisible="{Binding IsVaeSelectionEnabled}"
Text="{x:Static lang:Resources.Label_VAE}"
TextAlignment="Left" />
<ui:FAComboBox
Grid.Row="2"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
DisplayMemberBinding="{Binding ShortDisplayName}"
IsTextSearchEnabled="True"
IsVisible="{Binding IsVaeSelectionEnabled}"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.VaeModels}"
DisplayMemberBinding="{Binding ShortDisplayName}"
SelectedItem="{Binding SelectedVae}" />
</Grid>
</controls:Card>
</ControlTemplate>

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);
}
}

30
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,7 +31,9 @@ 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.Packages;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
@ -172,9 +176,9 @@ public static class DesignData
LaunchOptionsViewModel.UpdateFilterCards();
InstallerViewModel = Services.GetRequiredService<InstallerViewModel>();
InstallerViewModel.AvailablePackages = packageFactory
.GetAllAvailablePackages()
.ToImmutableArray();
InstallerViewModel.AvailablePackages = new ObservableCollectionExtended<BasePackage>(
packageFactory.GetAllAvailablePackages()
);
InstallerViewModel.SelectedPackage = InstallerViewModel.AvailablePackages[0];
InstallerViewModel.ReleaseNotes = "## Release Notes\nThis is a test release note.";
@ -336,6 +340,26 @@ public static class DesignData
public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>();
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",
ImageType = LocalImageFileType.TextToImage
}
)
};
return vm;
}
}
public static PackageManagerViewModel PackageManagerViewModel
{
get

68
StabilityMatrix.Avalonia/DesignData/MockImageIndexService.cs

@ -1,8 +1,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using DynamicData;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.DesignData;
@ -10,47 +9,50 @@ namespace StabilityMatrix.Avalonia.DesignData;
public class MockImageIndexService : IImageIndexService
{
/// <inheritdoc />
public IndexCollection<LocalImageFile, string> InferenceImages { get; } =
new IndexCollection<LocalImageFile, string>(null!, file => file.RelativePath)
public IndexCollection<LocalImageFile, string> InferenceImages { get; }
public MockImageIndexService()
{
InferenceImages = new IndexCollection<LocalImageFile, string>(
this,
file => file.AbsolutePath
)
{
RelativePath = "inference"
RelativePath = "Inference"
};
}
/// <inheritdoc />
public Task<IReadOnlyList<LocalImageFile>> GetLocalImagesByPrefix(string pathPrefix)
public Task RefreshIndexForAllCollections()
{
return Task.FromResult(
(IReadOnlyList<LocalImageFile>)
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",
}
}
);
return RefreshIndex(InferenceImages);
}
/// <inheritdoc />
public Task RefreshIndexForAllCollections()
private static LocalImageFile GetSampleImage(string url)
{
return Task.CompletedTask;
return new LocalImageFile
{
AbsolutePath = url,
GenerationParameters = GenerationParameters.GetSample(),
ImageSize = new System.Drawing.Size(1024, 1024)
};
}
/// <inheritdoc />
public Task RefreshIndex(IndexCollection<LocalImageFile, string> indexCollection)
{
var toAdd = new[]
{
GetSampleImage(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1024"
),
GetSampleImage(
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/16588c94-6595-4be9-8806-d7e6e22d198c/width=1152"
)
};
indexCollection.ItemsSource.EditDiff(toAdd, LocalImageFile.Comparer);
return Task.CompletedTask;
}
@ -59,10 +61,4 @@ public class MockImageIndexService : IImageIndexService
{
throw new System.NotImplementedException();
}
/// <inheritdoc />
public Task RemoveImage(LocalImageFile imageFile)
{
throw new System.NotImplementedException();
}
}

12
StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using DynamicData.Binding;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
@ -48,6 +49,17 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl
/// <inheritdoc />
public bool CanUserDisconnect => IsConnected && !IsConnecting;
public MockInferenceClientManager()
{
Models.AddRange(
new[]
{
HybridModelFile.FromRemote("v1-5-pruned-emaonly.safetensors"),
HybridModelFile.FromRemote("artshaper1.safetensors"),
}
);
}
/// <inheritdoc />
public Task CopyImageToInputAsync(
FilePath imageFile,

14
StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs

@ -282,20 +282,16 @@ public static class ComfyNodeBuilderExtensions
builder.Connections.ImageSize = builder.Connections.LatentSize;
}
var saveImage = builder.Nodes.AddNamedNode(
var previewImage = builder.Nodes.AddNamedNode(
new NamedComfyNode("SaveImage")
{
ClassType = "SaveImage",
Inputs = new Dictionary<string, object?>
{
["filename_prefix"] = "Inference/TextToImage",
["images"] = builder.Connections.Image
}
ClassType = "PreviewImage",
Inputs = new Dictionary<string, object?> { ["images"] = builder.Connections.Image }
}
);
builder.Connections.OutputNodes.Add(saveImage);
builder.Connections.OutputNodes.Add(previewImage);
return saveImage.Name;
return previewImage.Name;
}
}

47
StabilityMatrix.Avalonia/Helpers/ImageProcessor.cs

@ -13,50 +13,57 @@ public static class ImageProcessor
/// </summary>
public static (int rows, int columns) GetGridDimensionsFromImageCount(int count)
{
if (count <= 1) return (1, 1);
if (count == 2) return (1, 2);
if (count <= 1)
return (1, 1);
if (count == 2)
return (1, 2);
// Prefer one extra row over one extra column,
// the row count will be the floor of the square root
// and the column count will be floor of count / rows
var rows = (int) Math.Floor(Math.Sqrt(count));
var columns = (int) Math.Floor((double) count / rows);
var rows = (int)Math.Floor(Math.Sqrt(count));
var columns = (int)Math.Floor((double)count / rows);
return (rows, columns);
}
public static SKImage CreateImageGrid(
IReadOnlyList<SKImage> images,
int spacing = 0)
public static SKImage CreateImageGrid(IReadOnlyList<SKImage> images, int spacing = 0)
{
if (images.Count == 0)
throw new ArgumentException("Must have at least one image");
var (rows, columns) = GetGridDimensionsFromImageCount(images.Count);
var singleWidth = images[0].Width;
var singleHeight = images[0].Height;
// Make output image
using var output = new SKBitmap(
singleWidth * columns + spacing * (columns - 1),
singleHeight * rows + spacing * (rows - 1));
singleWidth * columns + spacing * (columns - 1),
singleHeight * rows + spacing * (rows - 1)
);
// Draw images
using var canvas = new SKCanvas(output);
foreach (var (row, column) in
Enumerable.Range(0, rows).Product(Enumerable.Range(0, columns)))
foreach (
var (row, column) in Enumerable.Range(0, rows).Product(Enumerable.Range(0, columns))
)
{
// Stop if we have drawn all images
var index = row * columns + column;
if (index >= images.Count) break;
if (index >= images.Count)
break;
// Get image
var image = images[index];
// Draw image
var destination = new SKRect(
singleWidth * column + spacing * column,
singleHeight * row + spacing * row,
singleWidth * column + spacing * column + image.Width,
singleHeight * row + spacing * row + image.Height);
singleHeight * row + spacing * row + image.Height
);
canvas.DrawImage(image, destination);
}

11
StabilityMatrix.Avalonia/Helpers/PngDataHelper.cs

@ -16,6 +16,17 @@ public static class PngDataHelper
private static readonly byte[] Text = { 0x74, 0x45, 0x58, 0x74 };
private static readonly byte[] Iend = { 0x49, 0x45, 0x4E, 0x44 };
public static byte[] AddMetadata(
Stream inputStream,
GenerationParameters generationParameters,
InferenceProjectDocument projectDocument
)
{
using var ms = new MemoryStream();
inputStream.CopyTo(ms);
return AddMetadata(ms.ToArray(), generationParameters, projectDocument);
}
public static byte[] AddMetadata(
byte[] inputImage,
GenerationParameters generationParameters,

6
StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs

@ -126,7 +126,11 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
}
}
public async Task RunGit(string? workingDirectory = null, params string[] args)
public async Task RunGit(
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null,
params string[] args
)
{
var command =
args.Length == 0 ? "git" : "git " + string.Join(" ", args.Select(ProcessRunner.Quote));

152
StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs

@ -17,70 +17,82 @@ namespace StabilityMatrix.Avalonia.Helpers;
public class WindowsPrerequisiteHelper : IPrerequisiteHelper
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly IGitHubClient gitHubClient;
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
private const string VcRedistDownloadUrl = "https://aka.ms/vs/16/release/vc_redist.x64.exe";
private string HomeDir => settingsManager.LibraryDir;
private string VcRedistDownloadPath => Path.Combine(HomeDir, "vcredist.x64.exe");
private string AssetsDir => Path.Combine(HomeDir, "Assets");
private string SevenZipPath => Path.Combine(AssetsDir, "7za.exe");
private string PythonDownloadPath => Path.Combine(AssetsDir, "python-3.10.11-embed-amd64.zip");
private string PythonDir => Path.Combine(AssetsDir, "Python310");
private string PythonDllPath => Path.Combine(PythonDir, "python310.dll");
private string PythonLibraryZipPath => Path.Combine(PythonDir, "python310.zip");
private string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc");
// Temporary directory to extract venv to during python install
private string VenvTempDir => Path.Combine(PythonDir, "venv");
private string PortableGitInstallDir => Path.Combine(HomeDir, "PortableGit");
private string PortableGitDownloadPath => Path.Combine(HomeDir, "PortableGit.7z.exe");
private string GitExePath => Path.Combine(PortableGitInstallDir, "bin", "git.exe");
public string GitBinPath => Path.Combine(PortableGitInstallDir, "bin");
public bool IsPythonInstalled => File.Exists(PythonDllPath);
public WindowsPrerequisiteHelper(
IGitHubClient gitHubClient,
IDownloadService downloadService,
ISettingsManager settingsManager)
IDownloadService downloadService,
ISettingsManager settingsManager
)
{
this.gitHubClient = gitHubClient;
this.downloadService = downloadService;
this.settingsManager = settingsManager;
}
public async Task RunGit(string? workingDirectory = null, params string[] args)
public async Task RunGit(
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null,
params string[] args
)
{
var process = ProcessRunner.StartAnsiProcess(GitExePath, args,
var process = ProcessRunner.StartAnsiProcess(
GitExePath,
args,
workingDirectory: workingDirectory,
environmentVariables: new Dictionary<string, string>
{
{"PATH", Compat.GetEnvPathWithExtensions(GitBinPath)}
});
{ "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) }
},
outputDataReceived: onProcessOutput
);
await ProcessRunner.WaitForExitConditionAsync(process);
}
public async Task<string> GetGitOutput(string? workingDirectory = null, params string[] args)
{
var process = await ProcessRunner.GetProcessOutputAsync(
GitExePath, string.Join(" ", args),
GitExePath,
string.Join(" ", args),
workingDirectory: workingDirectory,
environmentVariables: new Dictionary<string, string>
{
{"PATH", Compat.GetEnvPathWithExtensions(GitBinPath)}
});
{ "PATH", Compat.GetEnvPathWithExtensions(GitBinPath) }
}
);
return process;
}
public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null)
{
await InstallVcRedistIfNecessary(progress);
@ -97,16 +109,20 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
(Assets.SevenZipExecutable, AssetsDir),
(Assets.SevenZipLicense, AssetsDir),
};
progress?.Report(new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true));
progress?.Report(
new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true)
);
Directory.CreateDirectory(AssetsDir);
foreach (var (asset, extractDir) in assets)
{
await asset.ExtractToDir(extractDir);
}
progress?.Report(new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false));
progress?.Report(
new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false)
);
}
public async Task InstallPythonIfNecessary(IProgress<ProgressReport>? progress = null)
@ -120,7 +136,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
Logger.Info("Python not found at {PythonDllPath}, downloading...", PythonDllPath);
Directory.CreateDirectory(AssetsDir);
// Delete existing python zip if it exists
if (File.Exists(PythonLibraryZipPath))
{
@ -130,44 +146,45 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
var remote = Assets.PythonDownloadUrl;
var url = remote.Url.ToString();
Logger.Info($"Downloading Python from {url} to {PythonLibraryZipPath}");
// Cleanup to remove zip if download fails
try
{
// Download python zip
await downloadService.DownloadToFileAsync(url, PythonDownloadPath, progress: progress);
// Verify python hash
var downloadHash = await FileHash.GetSha256Async(PythonDownloadPath, progress);
if (downloadHash != remote.HashSha256)
{
var fileExists = File.Exists(PythonDownloadPath);
var fileSize = new FileInfo(PythonDownloadPath).Length;
var msg = $"Python download hash mismatch: {downloadHash} != {remote.HashSha256} " +
$"(file exists: {fileExists}, size: {fileSize})";
var msg =
$"Python download hash mismatch: {downloadHash} != {remote.HashSha256} "
+ $"(file exists: {fileExists}, size: {fileSize})";
throw new Exception(msg);
}
progress?.Report(new ProgressReport(progress: 1f, message: "Python download complete"));
progress?.Report(new ProgressReport(-1, "Installing Python...", isIndeterminate: true));
// We also need 7z if it's not already unpacked
if (!File.Exists(SevenZipPath))
{
await Assets.SevenZipExecutable.ExtractToDir(AssetsDir);
await Assets.SevenZipLicense.ExtractToDir(AssetsDir);
}
// Delete existing python dir
if (Directory.Exists(PythonDir))
{
Directory.Delete(PythonDir, true);
}
// Unzip python
await ArchiveHelper.Extract7Z(PythonDownloadPath, PythonDir);
try
{
// Extract embedded venv folder
@ -185,7 +202,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
await resource.ExtractTo(path);
}
// Add venv to python's library zip
await ArchiveHelper.AddToArchive7Z(PythonLibraryZipPath, VenvTempDir);
}
finally
@ -196,16 +213,16 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
Directory.Delete(VenvTempDir, true);
}
}
// Extract get-pip.pyc
await Assets.PyScriptGetPip.ExtractToDir(PythonDir);
// We need to uncomment the #import site line in python310._pth for pip to work
var pythonPthPath = Path.Combine(PythonDir, "python310._pth");
var pythonPthContent = await File.ReadAllTextAsync(pythonPthPath);
pythonPthContent = pythonPthContent.Replace("#import site", "import site");
await File.WriteAllTextAsync(pythonPthPath, pythonPthContent);
progress?.Report(new ProgressReport(1f, "Python install complete"));
}
finally
@ -225,7 +242,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
Logger.Debug("Git already installed at {GitExePath}", GitExePath);
return;
}
Logger.Info("Git not found at {GitExePath}, downloading...", GitExePath);
var portableGitUrl =
@ -233,7 +250,11 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
if (!File.Exists(PortableGitDownloadPath))
{
await downloadService.DownloadToFileAsync(portableGitUrl, PortableGitDownloadPath, progress: progress);
await downloadService.DownloadToFileAsync(
portableGitUrl,
PortableGitDownloadPath,
progress: progress
);
progress?.Report(new ProgressReport(progress: 1f, message: "Git download complete"));
}
@ -245,7 +266,9 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
{
var registry = Registry.LocalMachine;
var key = registry.OpenSubKey(
@"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64", false);
@"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64",
false
);
if (key != null)
{
var buildId = Convert.ToUInt32(key.GetValue("Bld"));
@ -254,20 +277,44 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
return;
}
}
Logger.Info("Downloading VC Redist");
await downloadService.DownloadToFileAsync(VcRedistDownloadUrl, VcRedistDownloadPath, progress: progress);
progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ download complete",
type: ProgressType.Download));
await downloadService.DownloadToFileAsync(
VcRedistDownloadUrl,
VcRedistDownloadPath,
progress: progress
);
progress?.Report(
new ProgressReport(
progress: 1f,
message: "Visual C++ download complete",
type: ProgressType.Download
)
);
Logger.Info("Installing VC Redist");
progress?.Report(new ProgressReport(progress: 0.5f, isIndeterminate: true, type: ProgressType.Generic, message: "Installing prerequisites..."));
var process = ProcessRunner.StartAnsiProcess(VcRedistDownloadPath, "/install /quiet /norestart");
progress?.Report(
new ProgressReport(
progress: 0.5f,
isIndeterminate: true,
type: ProgressType.Generic,
message: "Installing prerequisites..."
)
);
var process = ProcessRunner.StartAnsiProcess(
VcRedistDownloadPath,
"/install /quiet /norestart"
);
await process.WaitForExitAsync();
progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ install complete",
type: ProgressType.Generic));
progress?.Report(
new ProgressReport(
progress: 1f,
message: "Visual C++ install complete",
type: ProgressType.Generic
)
);
File.Delete(VcRedistDownloadPath);
}
@ -286,5 +333,4 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
File.Delete(PortableGitDownloadPath);
}
}

135
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>
@ -140,6 +149,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Copy.
/// </summary>
public static string Action_Copy {
get {
return ResourceManager.GetString("Action_Copy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delete.
/// </summary>
@ -248,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>
@ -383,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>
@ -410,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>
@ -887,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>
@ -932,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>
@ -1148,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>
@ -1157,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>
@ -1481,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>
@ -1526,6 +1643,24 @@ 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>
public static string Label_UseSharedOutputFolder {
get {
return ResourceManager.GetString("Label_UseSharedOutputFolder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to VAE.
/// </summary>

47
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -678,7 +678,52 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Restore Default Layout</value>
</data>
<data name="Label_UseSharedOutputFolder" xml:space="preserve">
<value>Output Sharing</value>
</data>
<data name="Label_BatchIndex" xml:space="preserve">
<value>Batch Index</value>
</data>
</root>
<data name="Action_Copy" xml:space="preserve">
<value>Copy</value>
</data>
<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>

73
StabilityMatrix.Avalonia/Models/Inference/FileNameFormat.cs

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace StabilityMatrix.Avalonia.Models.Inference;
public record FileNameFormat
{
public string Template { get; }
public string Prefix { get; set; } = "";
public string Postfix { get; set; } = "";
public IReadOnlyList<FileNameFormatPart> Parts { get; }
private FileNameFormat(string template, IReadOnlyList<FileNameFormatPart> parts)
{
Template = template;
Parts = parts;
}
public FileNameFormat WithBatchPostFix(int current, int total)
{
return this with { Postfix = Postfix + $" ({current}-{total})" };
}
public FileNameFormat WithGridPrefix()
{
return this with { Prefix = Prefix + "Grid_" };
}
public string GetFileName()
{
return Prefix
+ string.Join(
"",
Parts.Select(
part => part.Match(constant => constant, substitution => substitution.Invoke())
)
)
+ Postfix;
}
public static FileNameFormat Parse(string template, FileNameFormatProvider provider)
{
var parts = provider.GetParts(template).ToImmutableArray();
return new FileNameFormat(template, parts);
}
public static bool TryParse(
string template,
FileNameFormatProvider provider,
[NotNullWhen(true)] out FileNameFormat? format
)
{
try
{
format = Parse(template, provider);
return true;
}
catch (ArgumentException)
{
format = null;
return false;
}
}
public const string DefaultTemplate = "{date}_{time}-{model_name}-{seed}";
}

16
StabilityMatrix.Avalonia/Models/Inference/FileNameFormatPart.cs

@ -0,0 +1,16 @@
using System;
using System.Runtime.InteropServices;
using CSharpDiscriminatedUnion.Attributes;
namespace StabilityMatrix.Avalonia.Models.Inference;
[GenerateDiscriminatedUnion(CaseFactoryPrefix = "From")]
[StructLayout(LayoutKind.Auto)]
public readonly partial struct FileNameFormatPart
{
[StructCase("Constant", isDefaultValue: true)]
private readonly string constant;
[StructCase("Substitution")]
private readonly Func<string?> substitution;
}

192
StabilityMatrix.Avalonia/Models/Inference/FileNameFormatProvider.cs

@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text.RegularExpressions;
using Avalonia.Data;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Models.Inference;
public partial class FileNameFormatProvider
{
public GenerationParameters? GenerationParameters { get; init; }
public InferenceProjectType? ProjectType { get; init; }
public string? ProjectName { get; init; }
private Dictionary<string, Func<string?>>? _substitutions;
public Dictionary<string, Func<string?>> Substitutions =>
_substitutions ??= new Dictionary<string, Func<string?>>
{
{ "seed", () => GenerationParameters?.Seed.ToString() },
{ "prompt", () => GenerationParameters?.PositivePrompt },
{ "negative_prompt", () => GenerationParameters?.NegativePrompt },
{ "model_name", () => GenerationParameters?.ModelName },
{ "model_hash", () => GenerationParameters?.ModelHash },
{ "width", () => GenerationParameters?.Width.ToString() },
{ "height", () => GenerationParameters?.Height.ToString() },
{ "project_type", () => ProjectType?.GetStringValue() },
{ "project_name", () => ProjectName },
{ "date", () => DateTime.Now.ToString("yyyy-MM-dd") },
{ "time", () => DateTime.Now.ToString("HH-mm-ss") }
};
/// <summary>
/// Validate a format string
/// </summary>
/// <param name="format">Format string</param>
/// <exception cref="DataValidationException">Thrown if the format string contains an unknown variable</exception>
[Pure]
public ValidationResult Validate(string format)
{
var regex = BracketRegex();
var matches = regex.Matches(format);
var variables = matches.Select(m => m.Groups[1].Value);
foreach (var variableText in variables)
{
try
{
var (variable, _) = ExtractVariableAndSlice(variableText);
if (!Substitutions.ContainsKey(variable))
{
return new ValidationResult($"Unknown variable '{variable}'");
}
}
catch (Exception e)
{
return new ValidationResult($"Invalid variable '{variableText}': {e.Message}");
}
}
return ValidationResult.Success!;
}
public IEnumerable<FileNameFormatPart> GetParts(string template)
{
var regex = BracketRegex();
var matches = regex.Matches(template);
var parts = new List<FileNameFormatPart>();
// Loop through all parts of the string, including matches and non-matches
var currentIndex = 0;
foreach (var result in matches.Cast<Match>())
{
// If the match is not at the start of the string, add a constant part
if (result.Index != currentIndex)
{
var constant = template[currentIndex..result.Index];
parts.Add(FileNameFormatPart.FromConstant(constant));
currentIndex += constant.Length;
}
// Now we're at start of the current match, add the variable part
var (variable, slice) = ExtractVariableAndSlice(result.Groups[1].Value);
var substitution = Substitutions[variable];
// Slice string if necessary
if (slice is not null)
{
parts.Add(
FileNameFormatPart.FromSubstitution(() =>
{
var value = substitution();
if (value is null)
return null;
if (slice.End is null)
{
value = value[(slice.Start ?? 0)..];
}
else
{
var length =
Math.Min(value.Length, slice.End.Value) - (slice.Start ?? 0);
value = value.Substring(slice.Start ?? 0, length);
}
return value;
})
);
}
else
{
parts.Add(FileNameFormatPart.FromSubstitution(substitution));
}
currentIndex += result.Length;
}
// Add remaining as constant
if (currentIndex != template.Length)
{
var constant = template[currentIndex..];
parts.Add(FileNameFormatPart.FromConstant(constant));
}
return parts;
}
/// <summary>
/// Return a sample provider for UI preview
/// </summary>
public static FileNameFormatProvider GetSample()
{
return new FileNameFormatProvider
{
GenerationParameters = GenerationParameters.GetSample(),
ProjectType = InferenceProjectType.TextToImage,
ProjectName = "Sample Project"
};
}
/// <summary>
/// Extract variable and index from a combined string
/// </summary>
private static (string Variable, Slice? Slice) ExtractVariableAndSlice(string combined)
{
if (IndexRegex().Matches(combined).FirstOrDefault() is not { Success: true } match)
{
return (combined, null);
}
// Variable is everything before the match
var variable = combined[..match.Groups[0].Index];
var start = match.Groups["start"].Value;
var end = match.Groups["end"].Value;
var step = match.Groups["step"].Value;
var slice = new Slice(
string.IsNullOrEmpty(start) ? null : int.Parse(start),
string.IsNullOrEmpty(end) ? null : int.Parse(end),
string.IsNullOrEmpty(step) ? null : int.Parse(step)
);
return (variable, slice);
}
/// <summary>
/// Regex for matching contents within a curly brace.
/// </summary>
[GeneratedRegex(@"\{([a-z_:\d\[\]]+)\}")]
private static partial Regex BracketRegex();
/// <summary>
/// Regex for matching a Python-like array index.
/// </summary>
[GeneratedRegex(@"\[(?:(?<start>-?\d+)?)\:(?:(?<end>-?\d+)?)?(?:\:(?<step>-?\d+))?\]")]
private static partial Regex IndexRegex();
private record Slice(int? Start, int? End, int? Step);
}

8
StabilityMatrix.Avalonia/Models/Inference/FileNameFormatVar.cs

@ -0,0 +1,8 @@
namespace StabilityMatrix.Avalonia.Models.Inference;
public record FileNameFormatVar
{
public required string Variable { get; init; }
public string? Example { get; init; }
}

7
StabilityMatrix.Avalonia/Models/PackageOutputCategory.cs

@ -0,0 +1,7 @@
namespace StabilityMatrix.Avalonia.Models;
public class PackageOutputCategory
{
public required string Name { get; set; }
public required string Path { get; set; }
}

13
StabilityMatrix.Avalonia/Program.cs

@ -7,6 +7,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls;
@ -232,10 +233,16 @@ public class Program
if (e.ExceptionObject is not Exception ex)
return;
Logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message);
// Exception automatically logged by Sentry if enabled
if (SentrySdk.IsEnabled)
{
ex.SetSentryMechanism("AppDomain.UnhandledException", handled: false);
SentrySdk.CaptureException(ex);
SentrySdk.FlushAsync().SafeFireAndForget();
}
else
{
Logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message);
}
if (
@ -290,6 +297,10 @@ public class Program
[DoesNotReturn]
private static void ExitWithException(Exception exception)
{
if (SentrySdk.IsEnabled)
{
SentrySdk.Flush();
}
App.Shutdown(1);
Dispatcher.UIThread.InvokeShutdown();
Environment.Exit(Marshal.GetHRForException(exception));

45
StabilityMatrix.Avalonia/Services/InferenceClientManager.cs

@ -1,5 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -345,6 +346,44 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
return ConnectAsyncImpl(new Uri("http://127.0.0.1:8188"), cancellationToken);
}
private async Task MigrateLinksIfNeeded(PackagePair packagePair)
{
if (packagePair.InstalledPackage.FullPath is not { } packagePath)
{
throw new ArgumentException("Package path is null", nameof(packagePair));
}
var inferenceDir = settingsManager.ImagesInferenceDirectory;
inferenceDir.Create();
// For locally installed packages only
// Delete ./output/Inference
var legacyInferenceLinkDir = new DirectoryPath(
packagePair.InstalledPackage.FullPath
).JoinDir("output", "Inference");
if (legacyInferenceLinkDir.Exists)
{
logger.LogInformation(
"Deleting legacy inference link at {LegacyDir}",
legacyInferenceLinkDir
);
if (legacyInferenceLinkDir.IsSymbolicLink)
{
await legacyInferenceLinkDir.DeleteAsync(false);
}
else
{
logger.LogWarning(
"Legacy inference link at {LegacyDir} is not a symbolic link, skipping",
legacyInferenceLinkDir
);
}
}
}
/// <inheritdoc />
public async Task ConnectAsync(
PackagePair packagePair,
@ -367,11 +406,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
logger.LogError(ex, "Error setting up completion provider");
});
// Setup image folder links
await comfyPackage.SetupInferenceOutputFolderLinks(
packagePair.InstalledPackage.FullPath
?? throw new InvalidOperationException("Package does not have a Path")
);
await MigrateLinksIfNeeded(packagePair);
// Get user defined host and port
var host = packagePair.InstalledPackage.GetLaunchArgsHost();

3
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -8,7 +8,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>./Assets/Icon.ico</ApplicationIcon>
<Version>2.5.5-dev.1</Version>
<Version>2.6.0-dev.1</Version>
<InformationalVersion>$(Version)</InformationalVersion>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@ -32,6 +32,7 @@
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.2" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="CSharpDiscriminatedUnion" Version="2.0.1" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Dock.Avalonia" Version="11.0.0.2" />
<PackageReference Include="Dock.Model.Avalonia" Version="11.0.0.2" />

6
StabilityMatrix.Avalonia/ViewModels/Base/ContentDialogProgressViewModelBase.cs

@ -1,10 +1,14 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
public class ContentDialogProgressViewModelBase : ConsoleProgressViewModel
public partial class ContentDialogProgressViewModelBase : ConsoleProgressViewModel
{
[ObservableProperty]
private bool hideCloseButton;
public event EventHandler<ContentDialogResult>? PrimaryButtonClick;
public event EventHandler<ContentDialogResult>? SecondaryButtonClick;
public event EventHandler<ContentDialogResult>? CloseButtonClick;

194
StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs

@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input;
using NLog;
@ -27,6 +29,8 @@ using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
@ -41,6 +45,7 @@ public abstract partial class InferenceGenerationViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ISettingsManager settingsManager;
private readonly INotificationService notificationService;
private readonly ServiceManager<ViewModelBase> vmFactory;
@ -60,11 +65,13 @@ public abstract partial class InferenceGenerationViewModelBase
protected InferenceGenerationViewModelBase(
ServiceManager<ViewModelBase> vmFactory,
IInferenceClientManager inferenceClientManager,
INotificationService notificationService
INotificationService notificationService,
ISettingsManager settingsManager
)
: base(notificationService)
{
this.notificationService = notificationService;
this.settingsManager = settingsManager;
this.vmFactory = vmFactory;
ClientManager = inferenceClientManager;
@ -75,6 +82,101 @@ public abstract partial class InferenceGenerationViewModelBase
GenerateImageCommand.WithConditionalNotificationErrorHandler(notificationService);
}
/// <summary>
/// Write an image to the default output folder
/// </summary>
protected Task<FilePath> WriteOutputImageAsync(
Stream imageStream,
ImageGenerationEventArgs args,
int batchNum = 0,
int batchTotal = 0,
bool isGrid = false
)
{
var defaultOutputDir = settingsManager.ImagesInferenceDirectory;
defaultOutputDir.Create();
return WriteOutputImageAsync(
imageStream,
defaultOutputDir,
args,
batchNum,
batchTotal,
isGrid
);
}
/// <summary>
/// Write an image to an output folder
/// </summary>
protected async Task<FilePath> WriteOutputImageAsync(
Stream imageStream,
DirectoryPath outputDir,
ImageGenerationEventArgs args,
int batchNum = 0,
int batchTotal = 0,
bool isGrid = false
)
{
var formatTemplateStr = settingsManager.Settings.InferenceOutputImageFileNameFormat;
var formatProvider = new FileNameFormatProvider
{
GenerationParameters = args.Parameters,
ProjectType = args.Project?.ProjectType,
ProjectName = ProjectFile?.NameWithoutExtension
};
// Parse to format
if (
string.IsNullOrEmpty(formatTemplateStr)
|| !FileNameFormat.TryParse(formatTemplateStr, formatProvider, out var format)
)
{
// Fallback to default
Logger.Warn(
"Failed to parse format template: {FormatTemplate}, using default",
formatTemplateStr
);
format = FileNameFormat.Parse(FileNameFormat.DefaultTemplate, formatProvider);
}
if (isGrid)
{
format = format.WithGridPrefix();
}
if (batchNum >= 1 && batchTotal > 1)
{
format = format.WithBatchPostFix(batchNum, batchTotal);
}
var fileName = format.GetFileName();
var file = outputDir.JoinFile($"{fileName}.png");
// Until the file is free, keep adding _{i} to the end
for (var i = 0; i < 100; i++)
{
if (!file.Exists)
break;
file = outputDir.JoinFile($"{fileName}_{i + 1}.png");
}
// If that fails, append an 7-char uuid
if (file.Exists)
{
var uuid = Guid.NewGuid().ToString("N")[..7];
file = outputDir.JoinFile($"{fileName}_{uuid}.png");
}
await using var fileStream = file.Info.OpenWrite();
await imageStream.CopyToAsync(fileStream);
return file;
}
/// <summary>
/// Builds the image generation prompt
/// </summary>
@ -156,7 +258,7 @@ public abstract partial class InferenceGenerationViewModelBase
// Wait for prompt to finish
await promptTask.Task.WaitAsync(cancellationToken);
Logger.Trace($"Prompt task {promptTask.Id} finished");
Logger.Debug($"Prompt task {promptTask.Id} finished");
// Get output images
var imageOutputs = await client.GetImagesForExecutedPromptAsync(
@ -164,6 +266,20 @@ public abstract partial class InferenceGenerationViewModelBase
cancellationToken
);
if (
!imageOutputs.TryGetValue(args.OutputNodeNames[0], out var images)
|| images is not { Count: > 0 }
)
{
// No images match
notificationService.Show(
"No output",
"Did not receive any output images",
NotificationType.Warning
);
return;
}
// Disable cancellation
await promptInterrupt.DisposeAsync();
@ -172,15 +288,6 @@ public abstract partial class InferenceGenerationViewModelBase
ImageGalleryCardViewModel.ImageSources.Clear();
}
if (
!imageOutputs.TryGetValue(args.OutputNodeNames[0], out var images) || images is null
)
{
// No images match
notificationService.Show("No output", "Did not receive any output images");
return;
}
await ProcessOutputImages(images, args);
}
finally
@ -207,19 +314,22 @@ public abstract partial class InferenceGenerationViewModelBase
ImageGenerationEventArgs args
)
{
var client = args.Client;
// Write metadata to images
var outputImagesBytes = new List<byte[]>();
var outputImages = new List<ImageSource>();
foreach (
var (i, filePath) in images
.Select(image => image.ToFilePath(args.Client.OutputImagesDir!))
.Enumerate()
)
foreach (var (i, comfyImage) in images.Enumerate())
{
if (!filePath.Exists)
{
Logger.Warn($"Image file {filePath} does not exist");
continue;
}
Logger.Debug("Downloading image: {FileName}", comfyImage.FileName);
var imageStream = await client.GetImageStreamAsync(comfyImage);
using var ms = new MemoryStream();
await imageStream.CopyToAsync(ms);
var imageArray = ms.ToArray();
outputImagesBytes.Add(imageArray);
var parameters = args.Parameters!;
var project = args.Project!;
@ -248,17 +358,15 @@ public abstract partial class InferenceGenerationViewModelBase
);
}
var bytesWithMetadata = PngDataHelper.AddMetadata(
await filePath.ReadAllBytesAsync(),
parameters,
project
);
var bytesWithMetadata = PngDataHelper.AddMetadata(imageArray, parameters, project);
await using (var outputStream = filePath.Info.OpenWrite())
{
await outputStream.WriteAsync(bytesWithMetadata);
await outputStream.FlushAsync();
}
// Write using generated name
var filePath = await WriteOutputImageAsync(
new MemoryStream(bytesWithMetadata),
args,
i + 1,
images.Count
);
outputImages.Add(new ImageSource(filePath));
@ -268,17 +376,7 @@ public abstract partial class InferenceGenerationViewModelBase
// Download all images to make grid, if multiple
if (outputImages.Count > 1)
{
var outputDir = outputImages[0].LocalFile!.Directory;
var loadedImages = outputImages
.Select(i => i.LocalFile)
.Where(f => f is { Exists: true })
.Select(f =>
{
using var stream = f!.Info.OpenRead();
return SKImage.FromEncodedData(stream);
})
.ToImmutableArray();
var loadedImages = outputImagesBytes.Select(SKImage.FromEncodedData).ToImmutableArray();
var project = args.Project!;
@ -297,13 +395,11 @@ public abstract partial class InferenceGenerationViewModelBase
);
// Save to disk
var lastName = outputImages.Last().LocalFile?.Info.Name;
var gridPath = outputDir!.JoinFile($"grid-{lastName}");
await using (var fileStream = gridPath.Info.OpenWrite())
{
await fileStream.WriteAsync(gridBytesWithMetadata);
}
var gridPath = await WriteOutputImageAsync(
new MemoryStream(gridBytesWithMetadata),
args,
isGrid: true
);
// Insert to start of images
var gridImage = new ImageSource(gridPath);

3
StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs

@ -9,6 +9,7 @@ using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
@ -310,7 +311,7 @@ public abstract partial class InferenceTabViewModelBase
if (this is IImageGalleryComponent imageGalleryComponent)
{
imageGalleryComponent.LoadImagesToGallery(
new ImageSource(imageFile.GlobalFullPath)
new ImageSource(imageFile.AbsolutePath)
);
}
}

98
StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs

@ -36,6 +36,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory;
private readonly IPyRunner pyRunner;
private readonly IDownloadService downloadService;
private readonly INotificationService notificationService;
@ -47,15 +48,15 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
[ObservableProperty]
private PackageVersion? selectedVersion;
[ObservableProperty]
private IReadOnlyList<BasePackage>? availablePackages;
[ObservableProperty]
private ObservableCollection<GitCommit>? availableCommits;
[ObservableProperty]
private ObservableCollection<PackageVersion>? availableVersions;
[ObservableProperty]
private ObservableCollection<BasePackage> availablePackages;
[ObservableProperty]
private GitCommit? selectedCommit;
@ -69,6 +70,9 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
[NotifyPropertyChangedFor(nameof(CanInstall))]
private bool showDuplicateWarning;
[ObservableProperty]
private bool showIncompatiblePackages;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanInstall))]
private string? installName;
@ -115,7 +119,6 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
public bool CanInstall =>
!string.IsNullOrWhiteSpace(InstallName) && !ShowDuplicateWarning && !IsLoading;
public ProgressViewModel InstallProgress { get; } = new();
public IEnumerable<IPackageStep> Steps { get; set; }
public InstallerViewModel(
@ -128,22 +131,25 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
)
{
this.settingsManager = settingsManager;
this.packageFactory = packageFactory;
this.pyRunner = pyRunner;
this.downloadService = downloadService;
this.notificationService = notificationService;
this.prerequisiteHelper = prerequisiteHelper;
// AvailablePackages and SelectedPackage
var filtered = packageFactory.GetAllAvailablePackages().Where(p => p.IsCompatible).ToList();
AvailablePackages = new ObservableCollection<BasePackage>(
packageFactory.GetAllAvailablePackages()
filtered.Any() ? filtered : packageFactory.GetAllAvailablePackages()
);
SelectedPackage = AvailablePackages[0];
ShowIncompatiblePackages = !filtered.Any();
}
public override void OnLoaded()
{
if (AvailablePackages == null)
return;
IsReleaseMode = !SelectedPackage.ShouldIgnoreReleases;
}
@ -238,6 +244,8 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
downloadOptions.VersionTag =
SelectedVersion?.TagName
?? throw new NullReferenceException("Selected version is null");
downloadOptions.IsLatest =
AvailableVersions?.First().TagName == downloadOptions.VersionTag;
installedVersion.InstalledReleaseVersion = downloadOptions.VersionTag;
}
@ -245,6 +253,11 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
{
downloadOptions.CommitHash =
SelectedCommit?.Sha ?? throw new NullReferenceException("Selected commit is null");
downloadOptions.BranchName =
SelectedVersion?.TagName
?? throw new NullReferenceException("Selected version is null");
downloadOptions.IsLatest = AvailableCommits?.First().Sha == SelectedCommit.Sha;
installedVersion.InstalledBranch =
SelectedVersion?.TagName
?? throw new NullReferenceException("Selected version is null");
@ -259,6 +272,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
var installStep = new InstallPackageStep(
SelectedPackage,
SelectedTorchVersion,
downloadOptions,
installLocation
);
var setupModelFoldersStep = new SetupModelFoldersStep(
@ -301,12 +315,29 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
OnCloseButtonClick();
}
partial void OnShowIncompatiblePackagesChanged(bool value)
{
var filtered = packageFactory
.GetAllAvailablePackages()
.Where(p => ShowIncompatiblePackages || p.IsCompatible)
.ToList();
AvailablePackages = new ObservableCollection<BasePackage>(
filtered.Any() ? filtered : packageFactory.GetAllAvailablePackages()
);
SelectedPackage = AvailablePackages[0];
}
private void UpdateSelectedVersionToLatestMain()
{
if (AvailableVersions is null)
{
SelectedVersion = null;
}
else if (SelectedPackage is FooocusMre)
{
SelectedVersion = AvailableVersions.FirstOrDefault(x => x.TagName == "moonride-main");
}
else
{
// First try to find master
@ -358,41 +389,33 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
// When changing branch / release modes, refresh
// ReSharper disable once UnusedParameterInPartialMethod
partial void OnSelectedVersionTypeChanged(PackageVersionType value) =>
OnSelectedPackageChanged(SelectedPackage);
partial void OnSelectedPackageChanged(BasePackage value)
partial void OnSelectedVersionTypeChanged(PackageVersionType value)
{
IsLoading = true;
ReleaseNotes = string.Empty;
AvailableVersions?.Clear();
AvailableCommits?.Clear();
AvailableVersionTypes = SelectedPackage.ShouldIgnoreReleases
? PackageVersionType.Commit
: PackageVersionType.GithubRelease | PackageVersionType.Commit;
SelectedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
SelectedTorchVersion = SelectedPackage.GetRecommendedTorchVersion();
if (Design.IsDesignMode)
if (SelectedPackage is null || Design.IsDesignMode)
return;
Dispatcher.UIThread
.InvokeAsync(async () =>
{
Logger.Debug($"Release mode: {IsReleaseMode}");
var versionOptions = await value.GetAllVersionOptions();
var versionOptions = await SelectedPackage.GetAllVersionOptions();
AvailableVersions = IsReleaseMode
? new ObservableCollection<PackageVersion>(versionOptions.AvailableVersions)
: new ObservableCollection<PackageVersion>(versionOptions.AvailableBranches);
SelectedVersion = AvailableVersions.First(x => !x.IsPrerelease);
SelectedVersion = AvailableVersions?.FirstOrDefault(x => !x.IsPrerelease);
if (SelectedVersion is null)
return;
ReleaseNotes = SelectedVersion.ReleaseNotesMarkdown;
Logger.Debug($"Loaded release notes for {ReleaseNotes}");
if (!IsReleaseMode)
{
var commits = (await value.GetAllCommits(SelectedVersion.TagName))?.ToList();
var commits = (
await SelectedPackage.GetAllCommits(SelectedVersion.TagName)
)?.ToList();
if (commits is null || commits.Count == 0)
return;
@ -408,6 +431,29 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
.SafeFireAndForget();
}
partial void OnSelectedPackageChanged(BasePackage? value)
{
IsLoading = true;
ReleaseNotes = string.Empty;
AvailableVersions?.Clear();
AvailableCommits?.Clear();
if (value == null)
return;
AvailableVersionTypes = SelectedPackage.ShouldIgnoreReleases
? PackageVersionType.Commit
: PackageVersionType.GithubRelease | PackageVersionType.Commit;
IsReleaseMode = !SelectedPackage.ShouldIgnoreReleases;
SelectedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
SelectedTorchVersion = SelectedPackage.GetRecommendedTorchVersion();
SelectedVersionType = SelectedPackage.ShouldIgnoreReleases
? PackageVersionType.Commit
: PackageVersionType.GithubRelease;
OnSelectedVersionTypeChanged(SelectedVersionType);
}
partial void OnInstallNameChanged(string? value)
{
ShowDuplicateWarning = settingsManager.Settings.InstalledPackages.Any(
@ -418,7 +464,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
partial void OnSelectedVersionChanged(PackageVersion? value)
{
ReleaseNotes = value?.ReleaseNotesMarkdown ?? string.Empty;
if (value == null)
if (value == null || Design.IsDesignMode)
return;
SelectedCommit = null;

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

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
@ -7,10 +8,12 @@ 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;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
@ -18,14 +21,14 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
public partial class OneClickInstallViewModel : ViewModelBase
public partial class OneClickInstallViewModel : ContentDialogViewModelBase
{
private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory;
private readonly IPrerequisiteHelper prerequisiteHelper;
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]
@ -43,6 +46,9 @@ public partial class OneClickInstallViewModel : ViewModelBase
[ObservableProperty]
private bool isIndeterminate;
[ObservableProperty]
private bool showIncompatiblePackages;
[ObservableProperty]
private ObservableCollection<BasePackage> allPackages;
@ -53,6 +59,8 @@ public partial class OneClickInstallViewModel : ViewModelBase
[NotifyPropertyChangedFor(nameof(IsProgressBarVisible))]
private int oneClickInstallProgress;
private bool isInferenceInstall;
public bool IsProgressBarVisible => OneClickInstallProgress > 0 || IsIndeterminate;
public OneClickInstallViewModel(
@ -61,7 +69,7 @@ public partial class OneClickInstallViewModel : ViewModelBase
IPrerequisiteHelper prerequisiteHelper,
ILogger<OneClickInstallViewModel> logger,
IPyRunner pyRunner,
ISharedFolders sharedFolders
INavigationService navigationService
)
{
this.settingsManager = settingsManager;
@ -69,13 +77,21 @@ public partial class OneClickInstallViewModel : ViewModelBase
this.prerequisiteHelper = prerequisiteHelper;
this.logger = logger;
this.pyRunner = pyRunner;
this.sharedFolders = sharedFolders;
this.navigationService = navigationService;
HeaderText = Resources.Text_WelcomeToStabilityMatrix;
SubHeaderText = Resources.Text_OneClickInstaller_SubHeader;
ShowInstallButton = true;
var filteredPackages = this.packageFactory
.GetAllAvailablePackages()
.Where(p => p is { OfferInOneClickInstaller: true, IsCompatible: true })
.ToList();
AllPackages = new ObservableCollection<BasePackage>(
this.packageFactory.GetAllAvailablePackages().Where(p => p.OfferInOneClickInstaller)
filteredPackages.Any()
? filteredPackages
: this.packageFactory.GetAllAvailablePackages()
);
SelectedPackage = AllPackages[0];
}
@ -95,39 +111,34 @@ public partial class OneClickInstallViewModel : ViewModelBase
return Task.CompletedTask;
}
private async Task DoInstall()
[RelayCommand]
private async Task InstallComfyForInference()
{
HeaderText = $"{Resources.Label_Installing} {SelectedPackage.DisplayName}";
var progressHandler = new Progress<ProgressReport>(progress =>
{
SubHeaderText = $"{progress.Title} {progress.Percentage:N0}%";
IsIndeterminate = progress.IsIndeterminate;
OneClickInstallProgress = Convert.ToInt32(progress.Percentage);
});
await prerequisiteHelper.InstallAllIfNecessary(progressHandler);
SubHeaderText = Resources.Progress_InstallingPrerequisites;
IsIndeterminate = true;
if (!PyRunner.PipInstalled)
var comfyPackage = AllPackages.FirstOrDefault(x => x is ComfyUI);
if (comfyPackage != null)
{
await pyRunner.SetupPip();
SelectedPackage = comfyPackage;
isInferenceInstall = true;
await InstallCommand.ExecuteAsync(null);
}
}
if (!PyRunner.VenvInstalled)
private async Task DoInstall()
{
var steps = new List<IPackageStep>
{
await pyRunner.InstallPackage("virtualenv");
}
IsIndeterminate = false;
var libraryDir = settingsManager.LibraryDir;
new SetPackageInstallingStep(settingsManager, SelectedPackage.Name),
new SetupPrerequisitesStep(prerequisiteHelper, pyRunner)
};
// get latest version & download & install
var installLocation = Path.Combine(libraryDir, "Packages", SelectedPackage.Name);
var installLocation = Path.Combine(
settingsManager.LibraryDir,
"Packages",
SelectedPackage.Name
);
var downloadVersion = new DownloadPackageVersionOptions();
var downloadVersion = new DownloadPackageVersionOptions { IsLatest = true };
var installedVersion = new InstalledPackageVersion();
var versionOptions = await SelectedPackage.GetAllVersionOptions();
@ -139,16 +150,39 @@ public partial class OneClickInstallViewModel : ViewModelBase
else
{
downloadVersion.BranchName = await SelectedPackage.GetLatestVersion();
downloadVersion.CommitHash =
(await SelectedPackage.GetAllCommits(downloadVersion.BranchName))
?.FirstOrDefault()
?.Sha ?? string.Empty;
installedVersion.InstalledBranch = downloadVersion.BranchName;
installedVersion.InstalledCommitSha = downloadVersion.CommitHash;
}
var torchVersion = SelectedPackage.GetRecommendedTorchVersion();
var recommendedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
await DownloadPackage(installLocation, downloadVersion);
await InstallPackage(installLocation, torchVersion);
var downloadStep = new DownloadPackageVersionStep(
SelectedPackage,
installLocation,
downloadVersion
);
steps.Add(downloadStep);
var recommendedSharedFolderMethod = SelectedPackage.RecommendedSharedFolderMethod;
await SelectedPackage.SetupModelFolders(installLocation, recommendedSharedFolderMethod);
var installStep = new InstallPackageStep(
SelectedPackage,
torchVersion,
downloadVersion,
installLocation
);
steps.Add(installStep);
var setupModelFoldersStep = new SetupModelFoldersStep(
SelectedPackage,
recommendedSharedFolderMethod,
installLocation
);
steps.Add(setupModelFoldersStep);
var installedPackage = new InstalledPackage
{
@ -162,59 +196,47 @@ public partial class OneClickInstallViewModel : ViewModelBase
PreferredTorchVersion = torchVersion,
PreferredSharedFolderMethod = recommendedSharedFolderMethod
};
await using var st = settingsManager.BeginTransaction();
st.Settings.InstalledPackages.Add(installedPackage);
st.Settings.ActiveInstalledPackageId = installedPackage.Id;
EventManager.Instance.OnInstalledPackagesChanged();
HeaderText = Resources.Progress_InstallationComplete;
SubSubHeaderText = string.Empty;
OneClickInstallProgress = 100;
var addInstalledPackageStep = new AddInstalledPackageStep(
settingsManager,
installedPackage
);
steps.Add(addInstalledPackageStep);
for (var i = 0; i < 3; i++)
var runner = new PackageModificationRunner
{
ShowDialogOnStart = true,
HideCloseButton = true,
};
EventManager.Instance.OnPackageInstallProgressAdded(runner);
await runner.ExecuteSteps(steps);
EventManager.Instance.OnInstalledPackagesChanged();
HeaderText = $"{SelectedPackage.DisplayName} installed successfully";
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);
}
private async Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions
)
{
SubHeaderText = Resources.Progress_DownloadingPackage;
var progress = new Progress<ProgressReport>(progress =>
if (isInferenceInstall)
{
IsIndeterminate = progress.IsIndeterminate;
OneClickInstallProgress = Convert.ToInt32(progress.Percentage);
EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress);
});
await SelectedPackage.DownloadPackage(installLocation, versionOptions, progress);
SubHeaderText = Resources.Progress_DownloadComplete;
OneClickInstallProgress = 100;
navigationService.NavigateTo<InferenceViewModel>();
}
}
private async Task InstallPackage(string installLocation, TorchVersion torchVersion)
partial void OnShowIncompatiblePackagesChanged(bool value)
{
var progress = new Progress<ProgressReport>(progress =>
{
SubHeaderText = Resources.Progress_InstallingPackageRequirements;
IsIndeterminate = progress.IsIndeterminate;
OneClickInstallProgress = Convert.ToInt32(progress.Percentage);
EventManager.Instance.OnGlobalProgressChanged(OneClickInstallProgress);
});
var filteredPackages = packageFactory
.GetAllAvailablePackages()
.Where(p => p.OfferInOneClickInstaller && (ShowIncompatiblePackages || p.IsCompatible))
.ToList();
await SelectedPackage.InstallPackage(
installLocation,
torchVersion,
progress,
(output) => SubSubHeaderText = output.Text
AllPackages = new ObservableCollection<BasePackage>(
filteredPackages.Any() ? filteredPackages : packageFactory.GetAllAvailablePackages()
);
SelectedPackage = AllPackages[0];
}
}

51
StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs

@ -4,7 +4,6 @@ using System.Reactive.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls.Notifications;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
@ -120,20 +119,45 @@ public partial class ImageFolderCardViewModel : ViewModelBase
imageIndexService.RefreshIndexForAllCollections().SafeFireAndForget();
}
/// <summary>
/// Gets the image path if it exists, returns null.
/// If the image path is resolved but the file doesn't exist, it will be removed from the index.
/// </summary>
private FilePath? GetImagePathIfExists(LocalImageFile item)
{
var imageFile = new FilePath(item.AbsolutePath);
if (!imageFile.Exists)
{
// Remove from index
imageIndexService.InferenceImages.Remove(item);
// Invalidate cache
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
loader.RemoveAllNamesFromCache(imageFile.Name);
}
return null;
}
return imageFile;
}
/// <summary>
/// Handles image clicks to show preview
/// </summary>
[RelayCommand]
private async Task OnImageClick(LocalImageFile item)
{
if (item.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
if (GetImagePathIfExists(item) is not { } imageFile)
{
return;
}
var currentIndex = LocalImages.IndexOf(item);
var image = new ImageSource(new FilePath(imagePath));
var image = new ImageSource(imageFile);
// Preload
await image.GetBitmapAsync();
@ -156,14 +180,12 @@ public partial class ImageFolderCardViewModel : ViewModelBase
if (newIndex >= 0 && newIndex < LocalImages.Count)
{
var newImage = LocalImages[newIndex];
var newImageSource = new ImageSource(
new FilePath(newImage.GetFullPath(settingsManager.ImagesDirectory))
);
var newImageSource = new ImageSource(newImage.AbsolutePath);
// Preload
await newImageSource.GetBitmapAsync();
var oldImageSource = sender.ImageSource;
// var oldImageSource = sender.ImageSource;
sender.ImageSource = newImageSource;
sender.LocalImageFile = newImage;
@ -185,13 +207,12 @@ public partial class ImageFolderCardViewModel : ViewModelBase
[RelayCommand]
private async Task OnImageDelete(LocalImageFile? item)
{
if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
if (item is null || GetImagePathIfExists(item) is not { } imageFile)
{
return;
}
// Delete the file
var imageFile = new FilePath(imagePath);
var result = await notificationService.TryAsync(imageFile.DeleteAsync());
if (!result.IsSuccessful)
@ -215,14 +236,14 @@ public partial class ImageFolderCardViewModel : ViewModelBase
[RelayCommand]
private async Task OnImageCopy(LocalImageFile? item)
{
if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
if (item is null || GetImagePathIfExists(item) is not { } imageFile)
{
return;
}
var clipboard = App.Clipboard;
await clipboard.SetFileDataObjectAsync(imagePath);
await clipboard.SetFileDataObjectAsync(imageFile.FullPath);
}
/// <summary>
@ -231,12 +252,12 @@ public partial class ImageFolderCardViewModel : ViewModelBase
[RelayCommand]
private async Task OnImageOpen(LocalImageFile? item)
{
if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } imagePath)
if (item is null || GetImagePathIfExists(item) is not { } imageFile)
{
return;
}
await ProcessRunner.OpenFileBrowser(imagePath);
await ProcessRunner.OpenFileBrowser(imageFile);
}
/// <summary>
@ -248,13 +269,11 @@ public partial class ImageFolderCardViewModel : ViewModelBase
bool includeMetadata = false
)
{
if (item?.GetFullPath(settingsManager.ImagesDirectory) is not { } sourcePath)
if (item is null || GetImagePathIfExists(item) is not { } sourceFile)
{
return;
}
var sourceFile = new FilePath(sourcePath);
var formatName = format.ToString();
var storageFile = await App.StorageProvider.SaveFilePickerAsync(

4
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs

@ -19,6 +19,7 @@ using StabilityMatrix.Avalonia.Views.Inference;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Services;
using Path = System.IO.Path;
#pragma warning disable CS0657 // Not a valid attribute location for this declaration
@ -60,9 +61,10 @@ public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase
public InferenceImageUpscaleViewModel(
INotificationService notificationService,
IInferenceClientManager inferenceClientManager,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> vmFactory
)
: base(vmFactory, inferenceClientManager, notificationService)
: base(vmFactory, inferenceClientManager, notificationService, settingsManager)
{
this.notificationService = notificationService;

26
StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs

@ -86,10 +86,11 @@ public class InferenceTextToImageViewModel
public InferenceTextToImageViewModel(
INotificationService notificationService,
IInferenceClientManager inferenceClientManager,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> vmFactory,
IModelIndexService modelIndexService
)
: base(vmFactory, inferenceClientManager, notificationService)
: base(vmFactory, inferenceClientManager, notificationService, settingsManager)
{
this.notificationService = notificationService;
this.modelIndexService = modelIndexService;
@ -381,22 +382,7 @@ public class InferenceTextToImageViewModel
Client = ClientManager.Client,
Nodes = buildPromptArgs.Builder.ToNodeDictionary(),
OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(),
Parameters = new GenerationParameters
{
Seed = (ulong)seed,
Steps = SamplerCardViewModel.Steps,
CfgScale = SamplerCardViewModel.CfgScale,
Sampler = SamplerCardViewModel.SelectedSampler?.Name,
ModelName = ModelCardViewModel.SelectedModelName,
ModelHash = ModelCardViewModel
.SelectedModel
?.Local
?.ConnectedModelInfo
?.Hashes
.SHA256,
PositivePrompt = PromptCardViewModel.PromptDocument.Text,
NegativePrompt = PromptCardViewModel.NegativePromptDocument.Text
},
Parameters = SaveStateToParameters(new GenerationParameters()),
Project = InferenceProjectDocument.FromLoadable(this),
// Only clear output images on the first batch
ClearOutputImages = i == 0
@ -417,10 +403,9 @@ public class InferenceTextToImageViewModel
{
PromptCardViewModel.LoadStateFromParameters(parameters);
SamplerCardViewModel.LoadStateFromParameters(parameters);
ModelCardViewModel.LoadStateFromParameters(parameters);
SeedCardViewModel.Seed = Convert.ToInt64(parameters.Seed);
ModelCardViewModel.LoadStateFromParameters(parameters);
}
/// <inheritdoc />
@ -428,11 +413,10 @@ public class InferenceTextToImageViewModel
{
parameters = PromptCardViewModel.SaveStateToParameters(parameters);
parameters = SamplerCardViewModel.SaveStateToParameters(parameters);
parameters = ModelCardViewModel.SaveStateToParameters(parameters);
parameters.Seed = (ulong)SeedCardViewModel.Seed;
parameters = ModelCardViewModel.SaveStateToParameters(parameters);
return parameters;
}
}

6
StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs

@ -114,6 +114,10 @@ public partial class ModelCardViewModel : LoadableViewModelBase, IParametersLoad
/// <inheritdoc />
public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
{
return parameters with { ModelName = SelectedModel?.FileName };
return parameters with
{
ModelName = SelectedModel?.FileName,
ModelHash = SelectedModel?.Local?.ConnectedModelInfo?.Hashes.SHA256
};
}
}

2
StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs

@ -88,7 +88,7 @@ public partial class SelectImageCardViewModel : ViewModelBase, IDropTarget
{
var current = ImageSource;
ImageSource = new ImageSource(imageFile.GlobalFullPath);
ImageSource = new ImageSource(imageFile.AbsolutePath);
current?.Dispose();
});

78
StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs

@ -111,6 +111,10 @@ public partial class InferenceViewModel : PageViewModelBase
// Keep RunningPackage updated with the current package pair
EventManager.Instance.RunningPackageStatusChanged += OnRunningPackageStatusChanged;
// "Send to Inference"
EventManager.Instance.InferenceTextToImageRequested += OnInferenceTextToImageRequested;
EventManager.Instance.InferenceUpscaleRequested += OnInferenceUpscaleRequested;
MenuSaveAsCommand.WithConditionalNotificationErrorHandler(notificationService);
MenuOpenProjectCommand.WithConditionalNotificationErrorHandler(notificationService);
}
@ -232,6 +236,16 @@ public partial class InferenceViewModel : PageViewModelBase
}
}
private void OnInferenceTextToImageRequested(object? sender, LocalImageFile e)
{
Dispatcher.UIThread.Post(() => AddTabFromImage(e).SafeFireAndForget());
}
private void OnInferenceUpscaleRequested(object? sender, LocalImageFile e)
{
Dispatcher.UIThread.Post(() => AddUpscalerTabFromImage(e).SafeFireAndForget());
}
/// <summary>
/// Update the database with current tabs
/// </summary>
@ -593,6 +607,70 @@ public partial class InferenceViewModel : PageViewModelBase
await SyncTabStatesWithDatabase();
}
private async Task AddTabFromImage(LocalImageFile imageFile)
{
var metadata = imageFile.ReadMetadata();
InferenceTabViewModelBase? vm = null;
if (!string.IsNullOrWhiteSpace(metadata.SMProject))
{
var document = JsonSerializer.Deserialize<InferenceProjectDocument>(metadata.SMProject);
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");
}
document.VerifyVersion();
var textToImage = vmFactory.Get<InferenceTextToImageViewModel>();
textToImage.LoadStateFromJsonObject(document.State);
vm = textToImage;
}
else if (!string.IsNullOrWhiteSpace(metadata.Parameters))
{
if (GenerationParameters.TryParse(metadata.Parameters, out var generationParameters))
{
var textToImageViewModel = vmFactory.Get<InferenceTextToImageViewModel>();
textToImageViewModel.LoadStateFromParameters(generationParameters);
vm = textToImageViewModel;
}
}
if (vm == null)
{
notificationService.Show(
"Unable to load project from image",
"No image metadata found",
NotificationType.Error
);
return;
}
Tabs.Add(vm);
SelectedTab = vm;
await SyncTabStatesWithDatabase();
}
private async Task AddUpscalerTabFromImage(LocalImageFile imageFile)
{
var upscaleVm = vmFactory.Get<InferenceImageUpscaleViewModel>();
upscaleVm.IsUpscaleEnabled = true;
upscaleVm.SelectImageCardViewModel.ImageSource = new ImageSource(imageFile.AbsolutePath);
Tabs.Add(upscaleVm);
SelectedTab = upscaleVm;
await SyncTabStatesWithDatabase();
}
/// <summary>
/// Menu "Open Project" command.
/// </summary>

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;
}
}

402
StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs

@ -0,0 +1,402 @@
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.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.OutputsPage;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(Views.OutputsPage))]
public partial class OutputsPageViewModel : PageViewModelBase
{
private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory;
private readonly INotificationService notificationService;
private readonly INavigationService navigationService;
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<OutputImageViewModel, string> OutputsCache { get; } =
new(p => p.ImageFile.AbsolutePath);
public IObservableCollection<OutputImageViewModel> Outputs { get; set; } =
new ObservableCollectionExtended<OutputImageViewModel>();
public IEnumerable<SharedOutputType> OutputTypes { get; } = Enum.GetValues<SharedOutputType>();
[ObservableProperty]
private ObservableCollection<PackageOutputCategory> categories;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanShowOutputTypes))]
private PackageOutputCategory selectedCategory;
[ObservableProperty]
private SharedOutputType selectedOutputType;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(NumImagesSelected))]
private int numItemsSelected;
[ObservableProperty]
private string searchQuery;
public bool CanShowOutputTypes =>
SelectedCategory?.Name?.Equals("Shared Output Folder") ?? false;
public string NumImagesSelected =>
NumItemsSelected == 1
? Resources.Label_OneImageSelected
: string.Format(Resources.Label_NumImagesSelected, NumItemsSelected);
public OutputsPageViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
INotificationService notificationService,
INavigationService navigationService,
ILogger<OutputsPageViewModel> logger
)
{
this.settingsManager = settingsManager;
this.packageFactory = packageFactory;
this.notificationService = notificationService;
this.navigationService = navigationService;
this.logger = logger;
var predicate = this.WhenPropertyChanged(vm => vm.SearchQuery)
.Throttle(TimeSpan.FromMilliseconds(50))!
.Select<PropertyValue<OutputsPageViewModel, string>, Func<OutputImageViewModel, bool>>(
propertyValue =>
output =>
{
if (string.IsNullOrWhiteSpace(propertyValue.Value))
return true;
return output.ImageFile.FileName.Contains(
propertyValue.Value,
StringComparison.OrdinalIgnoreCase
)
|| (
output.ImageFile.GenerationParameters?.PositivePrompt != null
&& output.ImageFile.GenerationParameters.PositivePrompt.Contains(
propertyValue.Value,
StringComparison.OrdinalIgnoreCase
)
);
}
)
.AsObservable();
OutputsCache
.Connect()
.DeferUntilLoaded()
.Filter(predicate)
.SortBy(x => x.ImageFile.CreatedAt, SortDirection.Descending)
.Bind(Outputs)
.WhenPropertyChanged(p => p.IsSelected)
.Subscribe(_ =>
{
NumItemsSelected = Outputs.Count(o => o.IsSelected);
});
}
public override void OnLoaded()
{
if (Design.IsDesignMode)
return;
if (!settingsManager.IsLibraryDirSet)
return;
Directory.CreateDirectory(settingsManager.ImagesDirectory);
var packageCategories = settingsManager.Settings.InstalledPackages
.Where(x => !x.UseSharedOutputFolder)
.Select(p =>
{
var basePackage = packageFactory[p.PackageName!];
if (basePackage is null)
return null;
return new PackageOutputCategory
{
Path = Path.Combine(p.FullPath, basePackage.OutputFolderName),
Name = p.DisplayName
};
})
.ToList();
packageCategories.Insert(
0,
new PackageOutputCategory
{
Path = settingsManager.ImagesDirectory,
Name = "Shared Output Folder"
}
);
Categories = new ObservableCollection<PackageOutputCategory>(packageCategories);
SelectedCategory = Categories.First();
SelectedOutputType = SharedOutputType.All;
SearchQuery = string.Empty;
var path =
CanShowOutputTypes && SelectedOutputType != SharedOutputType.All
? Path.Combine(SelectedCategory.Path, SelectedOutputType.ToString())
: SelectedCategory.Path;
GetOutputs(path);
}
partial void OnSelectedCategoryChanged(
PackageOutputCategory? oldValue,
PackageOutputCategory? newValue
)
{
if (oldValue == newValue || newValue == null)
return;
var path =
CanShowOutputTypes && SelectedOutputType != SharedOutputType.All
? Path.Combine(newValue.Path, SelectedOutputType.ToString())
: SelectedCategory.Path;
GetOutputs(path);
}
partial void OnSelectedOutputTypeChanged(SharedOutputType oldValue, SharedOutputType newValue)
{
if (oldValue == newValue)
return;
var path =
newValue == SharedOutputType.All
? SelectedCategory.Path
: Path.Combine(SelectedCategory.Path, newValue.ToString());
GetOutputs(path);
}
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.ImageFile.AbsolutePath));
// Preload
await image.GetBitmapAsync();
var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item.ImageFile };
using var onNext = Observable
.FromEventPattern<DirectionalNavigationEventArgs>(
vm,
nameof(ImageViewerViewModel.NavigationRequested)
)
.Subscribe(ctx =>
{
Dispatcher.UIThread
.InvokeAsync(async () =>
{
var sender = (ImageViewerViewModel)ctx.Sender!;
var newIndex = currentIndex + (ctx.EventArgs.IsNext ? 1 : -1);
if (newIndex >= 0 && newIndex < Outputs.Count)
{
var newImage = Outputs[newIndex];
var newImageSource = new ImageSource(
new FilePath(newImage.ImageFile.AbsolutePath)
);
// Preload
await newImageSource.GetBitmapAsync();
sender.ImageSource = newImageSource;
sender.LocalImageFile = newImage.ImageFile;
currentIndex = newIndex;
}
})
.SafeFireAndForget();
});
await vm.GetDialog().ShowAsync();
}
public async Task CopyImage(string imagePath)
{
var clipboard = App.Clipboard;
await clipboard.SetFileDataObjectAsync(imagePath);
}
public async Task OpenImage(string imagePath) => await ProcessRunner.OpenFileBrowser(imagePath);
public async Task DeleteImage(OutputImageViewModel? item)
{
if (item is null)
return;
var confirmationDialog = new BetterContentDialog
{
Title = "Are you sure you want to delete this image?",
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;
// Delete the file
var imageFile = new FilePath(item.ImageFile.AbsolutePath);
var result = await notificationService.TryAsync(imageFile.DeleteAsync());
if (!result.IsSuccessful)
{
return;
}
OutputsCache.Remove(item);
// Invalidate cache
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
loader.RemoveAllNamesFromCache(imageFile.Name);
}
}
public void SendToTextToImage(OutputImageViewModel vm)
{
navigationService.NavigateTo<InferenceViewModel>();
EventManager.Instance.OnInferenceTextToImageRequested(vm.ImageFile);
}
public void SendToUpscale(OutputImageViewModel vm)
{
navigationService.NavigateTo<InferenceViewModel>();
EventManager.Instance.OnInferenceUpscaleRequested(vm.ImageFile);
}
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)
{
// Delete the file
var imageFile = new FilePath(output.ImageFile.AbsolutePath);
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)
return;
if (!Directory.Exists(directory) && SelectedOutputType != SharedOutputType.All)
{
Directory.CreateDirectory(directory);
return;
}
var list = Directory
.EnumerateFiles(directory, "*.png", SearchOption.AllDirectories)
.Select(file => new OutputImageViewModel(LocalImageFile.FromPath(file)))
.OrderByDescending(f => f.ImageFile.CreatedAt);
OutputsCache.EditDiff(list, (x, y) => x.ImageFile.AbsolutePath == y.ImageFile.AbsolutePath);
}
}

55
StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs

@ -62,6 +62,9 @@ public partial class PackageCardViewModel : ProgressViewModel
[ObservableProperty]
private bool canUseConfigMethod;
[ObservableProperty]
private bool useSharedOutput;
public PackageCardViewModel(
ILogger<PackageCardViewModel> logger,
IPackageFactory packageFactory,
@ -103,6 +106,7 @@ public partial class PackageCardViewModel : ProgressViewModel
CanUseConfigMethod =
basePackage?.AvailableSharedFolderMethods.Contains(SharedFolderMethod.Configuration)
?? false;
UseSharedOutput = Package?.UseSharedOutputFolder ?? false;
}
}
@ -243,7 +247,29 @@ public partial class PackageCardViewModel : ProgressViewModel
{
ModificationCompleteMessage = $"{packageName} Update Complete"
};
var updatePackageStep = new UpdatePackageStep(settingsManager, Package, basePackage);
var versionOptions = new DownloadPackageVersionOptions { IsLatest = true };
if (Package.Version.IsReleaseMode)
{
versionOptions.VersionTag = await basePackage.GetLatestVersion();
}
else
{
var commits = await basePackage.GetAllCommits(Package.Version.InstalledBranch);
var latest = commits?.FirstOrDefault();
if (latest == null)
throw new Exception("Could not find latest commit");
versionOptions.BranchName = Package.Version.InstalledBranch;
versionOptions.CommitHash = latest.Sha;
}
var updatePackageStep = new UpdatePackageStep(
settingsManager,
Package,
versionOptions,
basePackage
);
var steps = new List<IPackageStep> { updatePackageStep };
EventManager.Instance.OnPackageInstallProgressAdded(runner);
@ -374,6 +400,33 @@ public partial class PackageCardViewModel : ProgressViewModel
public void ToggleSharedModelNone() => IsSharedModelDisabled = !IsSharedModelDisabled;
public void ToggleSharedOutput() => UseSharedOutput = !UseSharedOutput;
partial void OnUseSharedOutputChanged(bool value)
{
if (Package == null)
return;
if (value == Package.UseSharedOutputFolder)
return;
using var st = settingsManager.BeginTransaction();
Package.UseSharedOutputFolder = value;
var basePackage = packageFactory[Package.PackageName!];
if (basePackage == null)
return;
if (value)
{
basePackage.SetupOutputFolderLinks(Package.FullPath!);
}
else
{
basePackage.RemoveOutputFolderLinks(Package.FullPath!);
}
}
// fake radio button stuff
partial void OnIsSharedModelSymlinkChanged(bool oldValue, bool newValue)
{

4
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
@ -122,17 +123,18 @@ public partial class PackageManagerViewModel : PageViewModelBase
public async Task ShowInstallDialog(BasePackage? selectedPackage = null)
{
var viewModel = dialogFactory.Get<InstallerViewModel>();
viewModel.AvailablePackages = packageFactory.GetAllAvailablePackages().ToImmutableArray();
viewModel.SelectedPackage = selectedPackage ?? viewModel.AvailablePackages[0];
var dialog = new BetterContentDialog
{
MaxDialogWidth = 900,
MinDialogWidth = 900,
FullSizeDesired = true,
DefaultButton = ContentDialogButton.Close,
IsPrimaryButtonEnabled = false,
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
ContentVerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
Content = new InstallerDialog { DataContext = viewModel }
};

6
StabilityMatrix.Avalonia/ViewModels/Progress/PackageInstallProgressItemViewModel.cs

@ -17,7 +17,10 @@ public class PackageInstallProgressItemViewModel : ProgressItemViewModelBase
private readonly IPackageModificationRunner packageModificationRunner;
private BetterContentDialog? dialog;
public PackageInstallProgressItemViewModel(IPackageModificationRunner packageModificationRunner)
public PackageInstallProgressItemViewModel(
IPackageModificationRunner packageModificationRunner,
bool hideCloseButton = false
)
{
this.packageModificationRunner = packageModificationRunner;
Id = packageModificationRunner.Id;
@ -25,6 +28,7 @@ public class PackageInstallProgressItemViewModel : ProgressItemViewModelBase
Progress.Value = packageModificationRunner.CurrentProgress.Percentage;
Progress.Text = packageModificationRunner.ConsoleOutput.LastOrDefault();
Progress.IsIndeterminate = packageModificationRunner.CurrentProgress.IsIndeterminate;
Progress.HideCloseButton = hideCloseButton;
Progress.Console.StartUpdates();

17
StabilityMatrix.Avalonia/ViewModels/Progress/ProgressManagerViewModel.cs

@ -120,19 +120,22 @@ public partial class ProgressManagerViewModel : PageViewModelBase
}
}
private async Task AddPackageInstall(IPackageModificationRunner packageModificationRunner)
private Task AddPackageInstall(IPackageModificationRunner packageModificationRunner)
{
if (ProgressItems.Any(vm => vm.Id == packageModificationRunner.Id))
{
return;
return Task.CompletedTask;
}
var vm = new PackageInstallProgressItemViewModel(packageModificationRunner);
var vm = new PackageInstallProgressItemViewModel(
packageModificationRunner,
packageModificationRunner.HideCloseButton
);
ProgressItems.Add(vm);
if (packageModificationRunner.ShowDialogOnStart)
{
await vm.ShowProgressDialog();
}
return packageModificationRunner.ShowDialogOnStart
? vm.ShowProgressDialog()
: Task.CompletedTask;
}
private void ShowFailedNotification(string title, string message)

64
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

@ -3,10 +3,12 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
@ -21,6 +23,7 @@ using Avalonia.Styling;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using NLog;
using SkiaSharp;
@ -29,6 +32,7 @@ using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
@ -107,6 +111,25 @@ public partial class SettingsViewModel : PageViewModelBase
[ObservableProperty]
private bool isCompletionRemoveUnderscoresEnabled = true;
[ObservableProperty]
[CustomValidation(typeof(SettingsViewModel), nameof(ValidateOutputImageFileNameFormat))]
private string? outputImageFileNameFormat;
[ObservableProperty]
private string? outputImageFileNameFormatSample;
public IEnumerable<FileNameFormatVar> OutputImageFileNameFormatVars =>
FileNameFormatProvider
.GetSample()
.Substitutions.Select(
kv =>
new FileNameFormatVar
{
Variable = $"{{{kv.Key}}}",
Example = kv.Value.Invoke()
}
);
[ObservableProperty]
private bool isImageViewerPixelGridEnabled = true;
@ -201,6 +224,39 @@ public partial class SettingsViewModel : PageViewModelBase
true
);
this.WhenPropertyChanged(vm => vm.OutputImageFileNameFormat)
.Throttle(TimeSpan.FromMilliseconds(50))
.Subscribe(formatProperty =>
{
var provider = FileNameFormatProvider.GetSample();
var template = formatProperty.Value;
if (
!string.IsNullOrEmpty(template)
&& provider.Validate(template) == ValidationResult.Success
)
{
var format = FileNameFormat.Parse(template, provider);
OutputImageFileNameFormatSample = format.GetFileName() + ".png";
}
else
{
// Use default format if empty
var defaultFormat = FileNameFormat.Parse(
FileNameFormat.DefaultTemplate,
provider
);
OutputImageFileNameFormatSample = defaultFormat.GetFileName() + ".png";
}
});
settingsManager.RelayPropertyFor(
this,
vm => vm.OutputImageFileNameFormat,
settings => settings.InferenceOutputImageFileNameFormat,
true
);
settingsManager.RelayPropertyFor(
this,
vm => vm.IsImageViewerPixelGridEnabled,
@ -225,6 +281,14 @@ public partial class SettingsViewModel : PageViewModelBase
UpdateAvailableTagCompletionCsvs();
}
public static ValidationResult ValidateOutputImageFileNameFormat(
string format,
ValidationContext context
)
{
return FileNameFormatProvider.GetSample().Validate(format);
}
partial void OnSelectedThemeChanged(string? value)
{
// In case design / tests

14
StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml

@ -106,6 +106,20 @@
Grid.Row="1"
Text="{Binding NegativePrompt}" />
</Grid>
<Grid RowDefinitions="Auto,*">
<TextBlock Text="Model" Theme="{DynamicResource BodyStrongTextBlockStyle}" />
<SelectableTextBlock
Grid.Row="1"
Text="{Binding ModelName}" />
</Grid>
<Grid RowDefinitions="Auto,*">
<TextBlock Text="Seed" Theme="{DynamicResource BodyStrongTextBlockStyle}" />
<SelectableTextBlock
Grid.Row="1"
Text="{Binding Seed}" />
</Grid>
</StackPanel>
</ui:TeachingTip>

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

@ -16,34 +16,140 @@
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.InstallerDialog">
<Grid MaxHeight="900"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto"
RowDefinitions="Auto,Auto,Auto,2*,*"
ColumnDefinitions="*,Auto">
<!-- Title and image -->
<Grid Grid.Row="0" Grid.Column="0"
Margin="16, 0, 0, 0"
RowDefinitions="Auto, Auto, Auto, Auto, Auto">
RowDefinitions="Auto, Auto, Auto, Auto, *, Auto">
<TextBlock Grid.Row="0" Text="{Binding SelectedPackage.DisplayName}"
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"
TextWrapping="Wrap"
Foreground="{DynamicResource ThemeRedColor}"
IsVisible="{Binding SelectedPackage.Disclaimer, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<ItemsRepeater Grid.Row="3"
Margin="0, 0, 0, 8"
ItemsSource="{Binding SelectedPackage.AvailableTorchVersions}">
<ItemsRepeater.Layout>
<StackLayout Orientation="Horizontal" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type models:TorchVersion}">
<controls:Card
Tag="{Binding }"
HorizontalAlignment="Left"
Padding="4"
Margin="0,8,8,0"
VerticalAlignment="Top">
<controls:Card.Styles>
<Style Selector="controls|Card[Tag=Cuda]">
<Setter Property="Background" Value="{DynamicResource ThemeGreenColorTransparent}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeGreenColorTransparent}" />
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="NVIDIA"
TextAlignment="Center"
VerticalAlignment="Center" />
</Template>
</Setter>
</Style>
<Style Selector="controls|Card[Tag=Rocm]">
<Setter Property="Background" Value="{DynamicResource ThemeDarkRedColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkRedColor}" />
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="AMD (Linux)"
TextAlignment="Center"
VerticalAlignment="Center"
ToolTip.Tip="For AMD GPUs that support ROCm on Linux" />
</Template>
</Setter>
</Style>
<Style Selector="controls|Card[Tag=DirectMl]">
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkBlueColor}" />
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="DirectML"
TextAlignment="Center"
VerticalAlignment="Center"
ToolTip.Tip="For any DirectX compatible GPU on Windows" />
</Template>
</Setter>
</Style>
<Style Selector="controls|Card[Tag=Mps]">
<Setter Property="Background" Value="White" />
<Setter Property="BorderBrush" Value="White" />
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="macOS"
TextAlignment="Center"
Foreground="Black"
VerticalAlignment="Center" />
</Template>
</Setter>
</Style>
<Style Selector="controls|Card[Tag=Cpu]">
<Setter Property="Background" Value="{DynamicResource ThemeBlueGreyColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBlueGreyColor}" />
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="CPU"
TextAlignment="Center"
VerticalAlignment="Center" />
</Template>
</Setter>
</Style>
</controls:Card.Styles>
</controls:Card>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
<controls:BetterAdvancedImage Grid.Row="4"
Margin="0, 12,0,0"
Source="{Binding SelectedPackage.PreviewImageUri}"
HorizontalAlignment="Center"
MaxHeight="300"
HorizontalAlignment="Left"
MaxHeight="250"
MaxWidth="600"
Stretch="UniformToFill" />
</Grid>
<Grid Grid.Column="0" Grid.Row="2"
Grid.ColumnSpan="2"
Margin="16,0,16,0"
Margin="16,16,16,0"
RowDefinitions="Auto, Auto, Auto, Auto"
HorizontalAlignment="Center"
ColumnDefinitions="Auto, Auto, Auto, Auto">
@ -60,7 +166,7 @@
<StackPanel Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="4"
Orientation="Horizontal"
Margin="8"
Margin="8,8,8,0"
IsVisible="{Binding ShowDuplicateWarning}">
<ui:SymbolIcon
Foreground="{DynamicResource ThemeRedColor}"
@ -93,10 +199,10 @@
</DataTemplate>
</ui:FAComboBox.ItemTemplate>
</ui:FAComboBox>
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="4"
HorizontalAlignment="Center"
Orientation="Horizontal" Margin="0,8,0,8"
Orientation="Horizontal" Margin="0,0,0,8"
IsVisible="{Binding IsReleaseModeAvailable}">
<ToggleButton
Content="{x:Static lang:Resources.Label_Releases}"
@ -158,7 +264,7 @@
</DataTemplate>
</ui:FAComboBox.ItemTemplate>
</ui:FAComboBox>
<Label Content="{x:Static lang:Resources.Label_PyTorchVersion}"
IsVisible="{Binding ShowTorchVersionOptions}"
Margin="0,8,0,4" />
@ -185,7 +291,7 @@
<!-- Install Button -->
<UniformGrid Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"
Columns="2"
Margin="0,32,0,0"
Margin="0,16,0,0"
HorizontalAlignment="Center">
<Button
Content="{x:Static lang:Resources.Action_Cancel}"
@ -205,25 +311,39 @@
Padding="16, 8, 16, 8" />
</UniformGrid>
<CheckBox Grid.Column="0"
Grid.Row="4"
VerticalAlignment="Bottom"
Margin="16,0,0,16"
Content="Show All Packages"
IsChecked="{Binding ShowIncompatiblePackages}"
ToolTip.Tip="Enabling &quot;Show All Packages&quot; will include packages that may not be compatible with your system and may run very slowly as a result."/>
<!-- 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="Auto">
<ListBox
ItemsSource="{Binding AvailablePackages}"
SelectedItem="{Binding SelectedPackage}"
CornerRadius="8">
<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>

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

@ -8,13 +8,14 @@
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"
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="700"
x:DataType="dialogs:OneClickInstallViewModel"
d:DataContext="{x:Static designData:DesignData.OneClickInstallViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.OneClickInstallDialog">
<Grid MaxHeight="900"
RowDefinitions="Auto,Auto,Auto,Auto,Auto"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,*"
ColumnDefinitions="*,Auto">
<Grid.Transitions>
@ -37,8 +38,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,16 +71,123 @@
FontSize="24"
Margin="16, 16, 0, 4"/>
<TextBlock Text="{Binding SelectedPackage.Blurb}"
Margin="16, 0, 0, 8"/>
TextWrapping="Wrap"
Margin="16, 0, 0, 4"/>
<TextBlock Text="{Binding SelectedPackage.Disclaimer}"
Padding="4"
Margin="16, 0, 0, 0"
TextWrapping="Wrap"
Foreground="OrangeRed"
IsVisible="{Binding SelectedPackage.Disclaimer, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<ItemsRepeater Margin="16, 0, 0, 0"
ItemsSource="{Binding SelectedPackage.AvailableTorchVersions}">
<ItemsRepeater.Layout>
<StackLayout Orientation="Horizontal" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type models:TorchVersion}">
<controls:Card
Tag="{Binding }"
HorizontalAlignment="Left"
Padding="4"
Margin="0,8,8,0"
VerticalAlignment="Top">
<controls:Card.Styles>
<Style Selector="controls|Card[Tag=Cuda]">
<Setter Property="Background" Value="{DynamicResource ThemeGreenColorTransparent}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeGreenColorTransparent}" />
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="NVIDIA"
TextAlignment="Center"
VerticalAlignment="Center" />
</Template>
</Setter>
</Style>
<Style Selector="controls|Card[Tag=Rocm]">
<Setter Property="Background" Value="{DynamicResource ThemeDarkRedColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkRedColor}" />
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="AMD (Linux)"
TextAlignment="Center"
VerticalAlignment="Center"
ToolTip.Tip="For AMD GPUs that support ROCm on Linux" />
</Template>
</Setter>
</Style>
<Style Selector="controls|Card[Tag=DirectMl]">
<Setter Property="Background" Value="{DynamicResource ThemeDarkBlueColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeDarkBlueColor}" />
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="DirectML"
TextAlignment="Center"
VerticalAlignment="Center"
ToolTip.Tip="For any DirectX compatible GPU on Windows" />
</Template>
</Setter>
</Style>
<Style Selector="controls|Card[Tag=Mps]">
<Setter Property="Background" Value="White" />
<Setter Property="BorderBrush" Value="White" />
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="macOS"
TextAlignment="Center"
Foreground="Black"
VerticalAlignment="Center" />
</Template>
</Setter>
</Style>
<Style Selector="controls|Card[Tag=Cpu]">
<Setter Property="Background" Value="{DynamicResource ThemeBlueGreyColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBlueGreyColor}" />
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<Setter Property="Content">
<Template>
<TextBlock
FontWeight="Medium"
HorizontalAlignment="Center"
Text="CPU"
TextAlignment="Center"
VerticalAlignment="Center" />
</Template>
</Setter>
</Style>
</controls:Card.Styles>
</controls:Card>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
<controls:BetterAdvancedImage Source="{Binding SelectedPackage.PreviewImageUri}"
HorizontalAlignment="Left"
MaxHeight="300"
Height="300"
Width="400"
Stretch="UniformToFill"
Margin="16, 8"/>
</StackPanel>
@ -114,7 +224,7 @@
FontSize="32"
HorizontalAlignment="Center"
Classes="success"
Margin="16"
Margin="8"
Padding="16, 8, 16, 8" />
<Button Command="{Binding ToggleAdvancedModeCommand}"
Content="{x:Static lang:Resources.Label_SkipSetup}"
@ -122,36 +232,50 @@
HorizontalAlignment="Center"
Padding="16, 8, 16, 8"/>
</StackPanel>
<CheckBox Grid.Column="1"
Grid.Row="2"
VerticalAlignment="Bottom"
Content="Show All Packages"
IsVisible="{Binding ShowInstallButton}"
HorizontalAlignment="Center"
IsChecked="{Binding ShowIncompatiblePackages}"
ToolTip.Tip="Enabling &quot;Show All Packages&quot; will include packages that may not be compatible with your system and may run very slowly as a result."/>
<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, 8">
<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");

5
StabilityMatrix.Avalonia/Views/Dialogs/PackageModificationDialog.axaml

@ -12,7 +12,8 @@
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.PackageModificationDialog">
<Grid Margin="8" RowDefinitions="Auto, Auto, Auto, *, Auto, Auto">
<TextBlock Grid.Row="0" Text="{Binding Text}"
FontSize="16"
FontSize="24"
FontWeight="Light"
Margin="4"
TextWrapping="WrapWithOverflow"
TextAlignment="Center"
@ -47,11 +48,13 @@
<CheckBox Grid.Row="4" IsChecked="{Binding CloseWhenFinished}"
HorizontalAlignment="Center"
IsVisible="{Binding !HideCloseButton}"
Margin="4" Content="{x:Static lang:Resources.Label_CloseDialogWhenFinished}"/>
<Button Grid.Row="5" Content="{x:Static lang:Resources.Action_Close}"
FontSize="20"
HorizontalAlignment="Center"
IsVisible="{Binding !HideCloseButton}"
Command="{Binding OnCloseButtonClick}"/>
</Grid>
</controls:UserControlBase>

213
StabilityMatrix.Avalonia/Views/OutputsPage.axaml

@ -0,0 +1,213 @@
<controls:UserControlBase
x:Class="StabilityMatrix.Avalonia.Views.OutputsPage"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:models1="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:fluentAvalonia="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:outputsPage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.OutputsPage"
xmlns:selectableImageCard="clr-namespace:StabilityMatrix.Avalonia.Controls.SelectableImageCard"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
d:DataContext="{x:Static mocks:DesignData.OutputsPageViewModel}"
d:DesignHeight="450"
d:DesignWidth="700"
x:CompileBindings="True"
x:DataType="vm:OutputsPageViewModel"
mc:Ignorable="d">
<Grid RowDefinitions="Auto, *" Margin="16">
<Grid Grid.Row="0" Margin="0,0,0,16"
HorizontalAlignment="Stretch"
RowDefinitions="Auto, Auto"
ColumnDefinitions="Auto, 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="0"
Grid.Column="2"
Margin="4"
Text="Search"/>
<TextBox Grid.Row="1"
Grid.Column="2"
Text="{Binding SearchQuery, Mode=TwoWay}"
Watermark="Search"
Margin="4, 0"
VerticalContentAlignment="Center"
MinWidth="150"/>
<TextBlock Grid.Row="1"
Grid.Column="4"
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="5"
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="6"
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="6"
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
ItemsSource="{Binding Outputs}"
VerticalAlignment="Top">
<ItemsRepeater.Layout>
<UniformGridLayout MinColumnSpacing="16" MinRowSpacing="16" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type outputsPage:OutputImageViewModel}">
<selectableImageCard:SelectableImageButton
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).OnImageClick}"
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_Copy}"
IconSource="Copy"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).CopyImage}"
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 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 ImageFile.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="{x:Static lang:Resources.Label_TextToImage}"
IconSource="FullScreenMaximize"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).SendToTextToImage}"
CommandParameter="{Binding }" />
<ui:MenuFlyoutItem
HotKey="{x:Null}"
Text="{x:Static lang:Resources.Label_ImageToImage}"
IsEnabled="False"
IconSource="ImageCopy"
CommandParameter="{Binding }" />
<ui:MenuFlyoutItem
Text="{x:Static lang:Resources.Label_Inpainting}"
IconSource="ImageEdit"
IsEnabled="False"
HotKey="{x:Null}"
CommandParameter="{Binding }" />
<ui:MenuFlyoutItem
Text="{x:Static lang:Resources.Label_Upscale}"
HotKey="{x:Null}"
Command="{Binding $parent[ItemsRepeater].((vm:OutputsPageViewModel)DataContext).SendToUpscale}"
CommandParameter="{Binding }">
<ui:MenuFlyoutItem.IconSource>
<fluentAvalonia:SymbolIconSource
FontSize="10"
Symbol="ResizeImage" />
</ui:MenuFlyoutItem.IconSource>
</ui:MenuFlyoutItem>
</ui:MenuFlyoutSubItem>
</ui:FAMenuFlyout>
</selectableImageCard:SelectableImageButton.ContextFlyout>
</selectableImageCard:SelectableImageButton>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</Grid>
</controls:UserControlBase>

14
StabilityMatrix.Avalonia/Views/OutputsPage.axaml.cs

@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls;
namespace StabilityMatrix.Avalonia.Views;
public partial class OutputsPage : UserControlBase
{
public OutputsPage()
{
InitializeComponent();
}
}

42
StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml

@ -79,24 +79,60 @@
<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 -->
</MenuItem>
<MenuItem
Header="{x:Static lang:Resources.Label_UseSharedOutputFolder}"
Command="{Binding ToggleSharedOutput}"
IsVisible="{Binding !IsUnknownPackage}">
<MenuItem.Icon>
<CheckBox Margin="8,0,0,0"
Padding="0"
Width="28" Height="28"
IsChecked="{Binding UseSharedOutput}">
<CheckBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5"/>
</CheckBox.RenderTransform>
</CheckBox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="{x:Static lang:Resources.Action_Uninstall}"
Command="{Binding Uninstall}">

46
StabilityMatrix.Avalonia/Views/SettingsPage.axaml

@ -6,10 +6,15 @@
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
xmlns:inference="clr-namespace:StabilityMatrix.Avalonia.Models.Inference"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
Focusable="True"
d:DataContext="{x:Static mocks:DesignData.SettingsViewModel}"
d:DesignHeight="700"
d:DesignWidth="800"
@ -83,10 +88,10 @@
</Grid>
<!-- Inference UI -->
<Grid Margin="0,8,0,0" RowDefinitions="auto,*,*">
<Grid Margin="0,8,0,0" RowDefinitions="auto,*,*,*">
<TextBlock
FontWeight="Medium"
Text="Inference UI"
Text="Inference"
Margin="0,0,0,8" />
<!-- Auto Completion -->
<ui:SettingsExpander Grid.Row="1"
@ -155,6 +160,43 @@
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<!-- Output Image Files -->
<ui:SettingsExpander Grid.Row="3"
Header="Output Image Files"
Margin="8,0,8,4">
<ui:SettingsExpander.IconSource>
<fluentIcons:SymbolIconSource Symbol="TabDesktopImage"/>
</ui:SettingsExpander.IconSource>
<!-- File name pattern -->
<ui:SettingsExpanderItem
Content="File name pattern"
Description="{Binding OutputImageFileNameFormatSample}"
IconSource="Rename">
<ui:SettingsExpanderItem.Footer>
<TextBox
Name="OutputImageFileNameFormatTextBox"
Watermark="{x:Static inference:FileNameFormat.DefaultTemplate}"
FontSize="13"
MinWidth="150"
Text="{Binding OutputImageFileNameFormat}"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"/>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<ui:TeachingTip
IsOpen="{Binding #OutputImageFileNameFormatTextBox.IsFocused}"
Target="{Binding #OutputImageFileNameFormatTextBox, Mode=OneWay}"
PreferredPlacement="Top"
Title="Format Variables"
Grid.Row="3">
<DataGrid
AutoGenerateColumns="True"
ItemsSource="{Binding OutputImageFileNameFormatVars}" />
<!--<mdxaml:MarkdownScrollViewer
Markdown="{Binding OutputImageFileNameFormatGuideMarkdown}"/>-->
</ui:TeachingTip>
</Grid>
<!-- Environment Options -->

9
StabilityMatrix.Core/Helper/EventManager.cs

@ -1,5 +1,6 @@
using System.Globalization;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.PackageModification;
using StabilityMatrix.Core.Models.Progress;
@ -34,6 +35,8 @@ public class EventManager
public event EventHandler? ModelIndexChanged;
public event EventHandler<FilePath>? ImageFileAdded;
public event EventHandler<LocalImageFile>? InferenceTextToImageRequested;
public event EventHandler<LocalImageFile>? InferenceUpscaleRequested;
public void OnGlobalProgressChanged(int progress) =>
GlobalProgressChanged?.Invoke(this, progress);
@ -74,4 +77,10 @@ public class EventManager
public void OnModelIndexChanged() => ModelIndexChanged?.Invoke(this, EventArgs.Empty);
public void OnImageFileAdded(FilePath filePath) => ImageFileAdded?.Invoke(this, filePath);
public void OnInferenceTextToImageRequested(LocalImageFile imageFile) =>
InferenceTextToImageRequested?.Invoke(this, imageFile);
public void OnInferenceUpscaleRequested(LocalImageFile imageFile) =>
InferenceUpscaleRequested?.Invoke(this, imageFile);
}

13
StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs

@ -1,25 +1,30 @@
using System.Runtime.Versioning;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Core.Helper;
public interface IPrerequisiteHelper
{
string GitBinPath { get; }
bool IsPythonInstalled { get; }
Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null);
Task UnpackResourcesIfNecessary(IProgress<ProgressReport>? progress = null);
Task InstallGitIfNecessary(IProgress<ProgressReport>? progress = null);
Task InstallPythonIfNecessary(IProgress<ProgressReport>? progress = null);
[SupportedOSPlatform("Windows")]
Task InstallVcRedistIfNecessary(IProgress<ProgressReport>? progress = null);
/// <summary>
/// Run embedded git with the given arguments.
/// </summary>
Task RunGit(string? workingDirectory = null, params string[] args);
Task RunGit(
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null,
params string[] args
);
Task<string> GetGitOutput(string? workingDirectory = null, params string[] args);
}

206
StabilityMatrix.Core/Helper/PrerequisiteHelper.cs

@ -17,35 +17,42 @@ public class PrerequisiteHelper : IPrerequisiteHelper
private readonly IGitHubClient gitHubClient;
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
private const string VcRedistDownloadUrl = "https://aka.ms/vs/16/release/vc_redist.x64.exe";
private const string PythonDownloadUrl = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip";
private const string PythonDownloadHashBlake3 = "24923775f2e07392063aaa0c78fbd4ae0a320e1fc9c6cfbab63803402279fe5a";
private const string PythonDownloadUrl =
"https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip";
private const string PythonDownloadHashBlake3 =
"24923775f2e07392063aaa0c78fbd4ae0a320e1fc9c6cfbab63803402279fe5a";
private string HomeDir => settingsManager.LibraryDir;
private string VcRedistDownloadPath => Path.Combine(HomeDir, "vcredist.x64.exe");
private string AssetsDir => Path.Combine(HomeDir, "Assets");
private string SevenZipPath => Path.Combine(AssetsDir, "7za.exe");
private string PythonDownloadPath => Path.Combine(AssetsDir, "python-3.10.11-embed-amd64.zip");
private string PythonDir => Path.Combine(AssetsDir, "Python310");
private string PythonDllPath => Path.Combine(PythonDir, "python310.dll");
private string PythonLibraryZipPath => Path.Combine(PythonDir, "python310.zip");
private string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc");
// Temporary directory to extract venv to during python install
private string VenvTempDir => Path.Combine(PythonDir, "venv");
private string PortableGitInstallDir => Path.Combine(HomeDir, "PortableGit");
private string PortableGitDownloadPath => Path.Combine(HomeDir, "PortableGit.7z.exe");
private string GitExePath => Path.Combine(PortableGitInstallDir, "bin", "git.exe");
public string GitBinPath => Path.Combine(PortableGitInstallDir, "bin");
public bool IsPythonInstalled => File.Exists(PythonDllPath);
public PrerequisiteHelper(ILogger<PrerequisiteHelper> logger, IGitHubClient gitHubClient,
IDownloadService downloadService, ISettingsManager settingsManager)
public PrerequisiteHelper(
ILogger<PrerequisiteHelper> logger,
IGitHubClient gitHubClient,
IDownloadService downloadService,
ISettingsManager settingsManager
)
{
this.logger = logger;
this.gitHubClient = gitHubClient;
@ -53,19 +60,33 @@ public class PrerequisiteHelper : IPrerequisiteHelper
this.settingsManager = settingsManager;
}
public async Task RunGit(string? workingDirectory = null, params string[] args)
public async Task RunGit(
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null,
params string[] args
)
{
var process = ProcessRunner.StartAnsiProcess(GitExePath, args, workingDirectory: workingDirectory);
var process = ProcessRunner.StartAnsiProcess(
GitExePath,
args,
workingDirectory,
onProcessOutput
);
await ProcessRunner.WaitForExitConditionAsync(process).ConfigureAwait(false);
}
public async Task<string> GetGitOutput(string? workingDirectory = null, params string[] args)
{
var output = await ProcessRunner.GetProcessOutputAsync(GitExePath, string.Join(" ", args),
workingDirectory: workingDirectory).ConfigureAwait(false);
var output = await ProcessRunner
.GetProcessOutputAsync(
GitExePath,
string.Join(" ", args),
workingDirectory: workingDirectory
)
.ConfigureAwait(false);
return output;
}
public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null)
{
await InstallVcRedistIfNecessary(progress);
@ -85,11 +106,14 @@ public class PrerequisiteHelper : IPrerequisiteHelper
// from "StabilityMatrix.Assets.Python310.libssl-1_1.dll"
// to "Python310\libssl-1_1.dll"
var fileExt = Path.GetExtension(resourceName);
var fileName = resourceName
.Replace(fileExt, "")
.Replace("StabilityMatrix.Assets.", "")
.Replace(".", Path.DirectorySeparatorChar.ToString()) + fileExt;
await using var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)!;
var fileName =
resourceName
.Replace(fileExt, "")
.Replace("StabilityMatrix.Assets.", "")
.Replace(".", Path.DirectorySeparatorChar.ToString()) + fileExt;
await using var resourceStream = Assembly
.GetExecutingAssembly()
.GetManifestResourceStream(resourceName)!;
if (resourceStream == null)
{
throw new Exception($"Resource {resourceName} not found");
@ -101,7 +125,11 @@ public class PrerequisiteHelper : IPrerequisiteHelper
/// <summary>
/// Extracts all embedded resources starting with resourceDir to outputDirectory
/// </summary>
private async Task ExtractAllEmbeddedResources(string resourceDir, string outputDirectory, string resourceRoot = "StabilityMatrix.Assets.")
private async Task ExtractAllEmbeddedResources(
string resourceDir,
string outputDirectory,
string resourceRoot = "StabilityMatrix.Assets."
)
{
Directory.CreateDirectory(outputDirectory);
// Unpack from embedded resources
@ -117,12 +145,15 @@ public class PrerequisiteHelper : IPrerequisiteHelper
// from "StabilityMatrix.Assets.Python310.libssl-1_1.dll"
// to "Python310\libssl-1_1.dll"
var fileExt = Path.GetExtension(resourceName);
var fileName = resourceName
.Replace(fileExt, "")
.Replace(resourceRoot, "")
.Replace(".", Path.DirectorySeparatorChar.ToString()) + fileExt;
var fileName =
resourceName
.Replace(fileExt, "")
.Replace(resourceRoot, "")
.Replace(".", Path.DirectorySeparatorChar.ToString()) + fileExt;
// Unpack resource
await using var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)!;
await using var resourceStream = Assembly
.GetExecutingAssembly()
.GetManifestResourceStream(resourceName)!;
var outputFilePath = Path.Combine(outputDirectory, fileName);
// Create missing directories
var outputDir = Path.GetDirectoryName(outputFilePath);
@ -134,13 +165,21 @@ public class PrerequisiteHelper : IPrerequisiteHelper
await resourceStream.CopyToAsync(fileStream);
copied.Add(outputFilePath);
}
logger.LogInformation("Successfully unpacked {Num} embedded resources: [{Resources}]", total, string.Join(",", copied));
logger.LogInformation(
"Successfully unpacked {Num} embedded resources: [{Resources}]",
total,
string.Join(",", copied)
);
}
public async Task UnpackResourcesIfNecessary(IProgress<ProgressReport>? progress = null)
{
// Skip if all files exist
if (File.Exists(SevenZipPath) && File.Exists(PythonDllPath) && File.Exists(PythonLibraryZipPath))
if (
File.Exists(SevenZipPath)
&& File.Exists(PythonDllPath)
&& File.Exists(PythonLibraryZipPath)
)
{
return;
}
@ -149,14 +188,14 @@ public class PrerequisiteHelper : IPrerequisiteHelper
// Create directories
Directory.CreateDirectory(AssetsDir);
Directory.CreateDirectory(PythonDir);
// Run if 7za missing
if (!File.Exists(SevenZipPath))
{
await ExtractEmbeddedResource("StabilityMatrix.Assets.7za.exe", AssetsDir);
await ExtractEmbeddedResource("StabilityMatrix.Assets.7za - LICENSE.txt", AssetsDir);
}
progress?.Report(new ProgressReport(1f, "Unpacking complete"));
}
@ -171,60 +210,70 @@ public class PrerequisiteHelper : IPrerequisiteHelper
logger.LogInformation("Python not found at {PythonDllPath}, downloading...", PythonDllPath);
Directory.CreateDirectory(AssetsDir);
// Delete existing python zip if it exists
if (File.Exists(PythonLibraryZipPath))
{
File.Delete(PythonLibraryZipPath);
}
logger.LogInformation(
"Downloading Python from {PythonLibraryZipUrl} to {PythonLibraryZipPath}",
PythonDownloadUrl, PythonLibraryZipPath);
"Downloading Python from {PythonLibraryZipUrl} to {PythonLibraryZipPath}",
PythonDownloadUrl,
PythonLibraryZipPath
);
// Cleanup to remove zip if download fails
try
{
// Download python zip
await downloadService.DownloadToFileAsync(PythonDownloadUrl, PythonDownloadPath, progress: progress);
await downloadService.DownloadToFileAsync(
PythonDownloadUrl,
PythonDownloadPath,
progress: progress
);
// Verify python hash
var downloadHash = await FileHash.GetBlake3Async(PythonDownloadPath, progress);
if (downloadHash != PythonDownloadHashBlake3)
{
var fileExists = File.Exists(PythonDownloadPath);
var fileSize = new FileInfo(PythonDownloadPath).Length;
var msg = $"Python download hash mismatch: {downloadHash} != {PythonDownloadHashBlake3} " +
$"(file exists: {fileExists}, size: {fileSize})";
var msg =
$"Python download hash mismatch: {downloadHash} != {PythonDownloadHashBlake3} "
+ $"(file exists: {fileExists}, size: {fileSize})";
throw new Exception(msg);
}
progress?.Report(new ProgressReport(progress: 1f, message: "Python download complete"));
progress?.Report(new ProgressReport(-1, "Installing Python...", isIndeterminate: true));
// We also need 7z if it's not already unpacked
if (!File.Exists(SevenZipPath))
{
await ExtractEmbeddedResource("StabilityMatrix.Assets.7za.exe", AssetsDir);
await ExtractEmbeddedResource("StabilityMatrix.Assets.7za - LICENSE.txt", AssetsDir);
await ExtractEmbeddedResource(
"StabilityMatrix.Assets.7za - LICENSE.txt",
AssetsDir
);
}
// Delete existing python dir
if (Directory.Exists(PythonDir))
{
Directory.Delete(PythonDir, true);
}
// Unzip python
await ArchiveHelper.Extract7Z(PythonDownloadPath, PythonDir);
try
{
// Extract embedded venv
await ExtractAllEmbeddedResources("StabilityMatrix.Assets.venv", PythonDir);
// Add venv to python's library zip
await ArchiveHelper.AddToArchive7Z(PythonLibraryZipPath, VenvTempDir);
}
finally
@ -235,16 +284,16 @@ public class PrerequisiteHelper : IPrerequisiteHelper
Directory.Delete(VenvTempDir, true);
}
}
// Extract get-pip.pyc
await ExtractEmbeddedResource("StabilityMatrix.Assets.get-pip.pyc", PythonDir);
// We need to uncomment the #import site line in python310._pth for pip to work
var pythonPthPath = Path.Combine(PythonDir, "python310._pth");
var pythonPthContent = await File.ReadAllTextAsync(pythonPthPath);
pythonPthContent = pythonPthContent.Replace("#import site", "import site");
await File.WriteAllTextAsync(pythonPthPath, pythonPthContent);
progress?.Report(new ProgressReport(1f, "Python install complete"));
}
finally
@ -264,7 +313,7 @@ public class PrerequisiteHelper : IPrerequisiteHelper
logger.LogDebug("Git already installed at {GitExePath}", GitExePath);
return;
}
logger.LogInformation("Git not found at {GitExePath}, downloading...", GitExePath);
var portableGitUrl =
@ -272,7 +321,11 @@ public class PrerequisiteHelper : IPrerequisiteHelper
if (!File.Exists(PortableGitDownloadPath))
{
await downloadService.DownloadToFileAsync(portableGitUrl, PortableGitDownloadPath, progress: progress);
await downloadService.DownloadToFileAsync(
portableGitUrl,
PortableGitDownloadPath,
progress: progress
);
progress?.Report(new ProgressReport(progress: 1f, message: "Git download complete"));
}
@ -284,7 +337,9 @@ public class PrerequisiteHelper : IPrerequisiteHelper
{
var registry = Registry.LocalMachine;
var key = registry.OpenSubKey(
@"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64", false);
@"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64",
false
);
if (key != null)
{
var buildId = Convert.ToUInt32(key.GetValue("Bld"));
@ -293,20 +348,44 @@ public class PrerequisiteHelper : IPrerequisiteHelper
return;
}
}
logger.LogInformation("Downloading VC Redist");
await downloadService.DownloadToFileAsync(VcRedistDownloadUrl, VcRedistDownloadPath, progress: progress);
progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ download complete",
type: ProgressType.Download));
await downloadService.DownloadToFileAsync(
VcRedistDownloadUrl,
VcRedistDownloadPath,
progress: progress
);
progress?.Report(
new ProgressReport(
progress: 1f,
message: "Visual C++ download complete",
type: ProgressType.Download
)
);
logger.LogInformation("Installing VC Redist");
progress?.Report(new ProgressReport(progress: 0.5f, isIndeterminate: true, type: ProgressType.Generic, message: "Installing prerequisites..."));
var process = ProcessRunner.StartAnsiProcess(VcRedistDownloadPath, "/install /quiet /norestart");
progress?.Report(
new ProgressReport(
progress: 0.5f,
isIndeterminate: true,
type: ProgressType.Generic,
message: "Installing prerequisites..."
)
);
var process = ProcessRunner.StartAnsiProcess(
VcRedistDownloadPath,
"/install /quiet /norestart"
);
await process.WaitForExitAsync();
progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ install complete",
type: ProgressType.Generic));
progress?.Report(
new ProgressReport(
progress: 1f,
message: "Visual C++ install complete",
type: ProgressType.Generic
)
);
File.Delete(VcRedistDownloadPath);
}
@ -335,5 +414,4 @@ public class PrerequisiteHelper : IPrerequisiteHelper
settingsManager.AddPathExtension(GitBinPath);
settingsManager.InsertPathExtensions();
}
}

29
StabilityMatrix.Core/Helper/SharedFolders.cs

@ -45,10 +45,12 @@ public class SharedFolders : ISharedFolders
/// <param name="sourceDir">Shared source (i.e. "Models/")</param>
/// <param name="destinationDir">Destination (i.e. "webui/models/lora")</param>
/// <param name="overwrite">Whether to overwrite the destination if it exists</param>
/// <param name="recursiveDelete">Whether to recursively delete the directory after moving data out of it</param>
public static async Task CreateOrUpdateLink(
DirectoryPath sourceDir,
DirectoryPath destinationDir,
bool overwrite = false
bool overwrite = false,
bool recursiveDelete = false
)
{
// Create source folder if it doesn't exist
@ -74,6 +76,7 @@ public class SharedFolders : ISharedFolders
// Otherwise delete the link
Logger.Info($"Deleting existing junction at target {destinationDir}");
destinationDir.Info.Attributes = FileAttributes.Normal;
await destinationDir.DeleteAsync(false).ConfigureAwait(false);
}
else
@ -93,7 +96,7 @@ public class SharedFolders : ISharedFolders
}
Logger.Info($"Deleting existing empty folder at target {destinationDir}");
await destinationDir.DeleteAsync(false).ConfigureAwait(false);
await destinationDir.DeleteAsync(recursiveDelete).ConfigureAwait(false);
}
}
@ -117,11 +120,13 @@ public class SharedFolders : ISharedFolders
/// Updates or creates shared links for a package.
/// Will attempt to move files from the destination to the source if the destination is not empty.
/// </summary>
public static async Task UpdateLinksForPackage(
Dictionary<SharedFolderType, IReadOnlyList<string>> sharedFolders,
public static async Task UpdateLinksForPackage<T>(
Dictionary<T, IReadOnlyList<string>> sharedFolders,
DirectoryPath modelsDirectory,
DirectoryPath installDirectory
DirectoryPath installDirectory,
bool recursiveDelete = false
)
where T : Enum
{
foreach (var (folderType, relativePaths) in sharedFolders)
{
@ -130,14 +135,22 @@ public class SharedFolders : ISharedFolders
var sourceDir = new DirectoryPath(modelsDirectory, folderType.GetStringValue());
var destinationDir = installDirectory.JoinDir(relativePath);
await CreateOrUpdateLink(sourceDir, destinationDir).ConfigureAwait(false);
await CreateOrUpdateLink(
sourceDir,
destinationDir,
recursiveDelete: recursiveDelete
)
.ConfigureAwait(false);
}
}
}
public static void RemoveLinksForPackage(BasePackage package, DirectoryPath installPath)
public static void RemoveLinksForPackage<T>(
Dictionary<T, IReadOnlyList<string>>? sharedFolders,
DirectoryPath installPath
)
where T : Enum
{
var sharedFolders = package.SharedFolders;
if (sharedFolders == null)
{
return;

58
StabilityMatrix.Core/Models/Database/LocalImageFile.cs

@ -11,54 +11,42 @@ namespace StabilityMatrix.Core.Models.Database;
/// </summary>
public class LocalImageFile
{
/// <summary>
/// Relative path of the file from the root images directory ("%LIBRARY%/Images").
/// </summary>
[BsonId]
public required string RelativePath { get; set; }
public required string AbsolutePath { get; init; }
/// <summary>
/// Type of the model file.
/// </summary>
public LocalImageFileType ImageType { get; set; }
public LocalImageFileType ImageType { get; init; }
/// <summary>
/// Creation time of the file.
/// </summary>
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// Last modified time of the file.
/// </summary>
public DateTimeOffset LastModifiedAt { get; set; }
public DateTimeOffset LastModifiedAt { get; init; }
/// <summary>
/// Generation parameters metadata of the file.
/// </summary>
public GenerationParameters? GenerationParameters { get; set; }
public GenerationParameters? GenerationParameters { get; init; }
/// <summary>
/// Dimensions of the image
/// </summary>
public Size? ImageSize { get; set; }
public Size? ImageSize { get; init; }
/// <summary>
/// File name of the relative path.
/// </summary>
public string FileName => Path.GetFileName(RelativePath);
public string FileName => Path.GetFileName(AbsolutePath);
/// <summary>
/// File name of the relative path without extension.
/// </summary>
public string FileNameWithoutExtension => Path.GetFileNameWithoutExtension(RelativePath);
public string GlobalFullPath =>
GlobalConfig.LibraryDir.JoinDir("Images").JoinFile(RelativePath);
public string GetFullPath(string rootImageDirectory)
{
return Path.Combine(rootImageDirectory, RelativePath);
}
public string FileNameWithoutExtension => Path.GetFileNameWithoutExtension(AbsolutePath);
public (
string? Parameters,
@ -68,7 +56,7 @@ public class LocalImageFile
) ReadMetadata()
{
using var stream = new FileStream(
GlobalFullPath,
AbsolutePath,
FileMode.Open,
FileAccess.Read,
FileShare.Read
@ -90,29 +78,19 @@ public class LocalImageFile
public static LocalImageFile FromPath(FilePath filePath)
{
var relativePath = Path.GetRelativePath(
GlobalConfig.LibraryDir.JoinDir("Images"),
filePath
);
// TODO: Support other types
const LocalImageFileType imageType =
LocalImageFileType.Inference | LocalImageFileType.TextToImage;
// Get metadata
using var stream = new FileStream(
filePath.FullPath,
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
using var stream = filePath.Info.OpenRead();
using var reader = new BinaryReader(stream);
var imageSize = ImageMetadata.GetImageSize(reader);
var metadata = ImageMetadata.ReadTextChunk(reader, "parameters-json");
GenerationParameters? genParams = null;
GenerationParameters? genParams;
if (!string.IsNullOrWhiteSpace(metadata))
{
@ -124,9 +102,11 @@ public class LocalImageFile
GenerationParameters.TryParse(metadata, out genParams);
}
filePath.Info.Refresh();
return new LocalImageFile
{
RelativePath = relativePath,
AbsolutePath = filePath,
ImageType = imageType,
CreatedAt = filePath.Info.CreationTimeUtc,
LastModifiedAt = filePath.Info.LastWriteTimeUtc,
@ -150,21 +130,23 @@ public class LocalImageFile
return false;
if (x.GetType() != y.GetType())
return false;
return x.RelativePath == y.RelativePath
return x.AbsolutePath == y.AbsolutePath
&& x.ImageType == y.ImageType
&& x.CreatedAt.Equals(y.CreatedAt)
&& x.LastModifiedAt.Equals(y.LastModifiedAt)
&& Equals(x.GenerationParameters, y.GenerationParameters);
&& Equals(x.GenerationParameters, y.GenerationParameters)
&& Nullable.Equals(x.ImageSize, y.ImageSize);
}
public int GetHashCode(LocalImageFile obj)
{
return HashCode.Combine(
obj.RelativePath,
obj.AbsolutePath,
obj.ImageType,
obj.CreatedAt,
obj.LastModifiedAt,
obj.GenerationParameters
obj.GenerationParameters,
obj.ImageSize
);
}
}

1
StabilityMatrix.Core/Models/DownloadPackageVersionOptions.cs

@ -5,4 +5,5 @@ public class DownloadPackageVersionOptions
public string BranchName { get; set; }
public string CommitHash { get; set; }
public string VersionTag { get; set; }
public bool IsLatest { get; set; }
}

17
StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs

@ -11,7 +11,6 @@ public class FilePath : FileSystemPath, IPathObject
{
private FileInfo? _info;
// ReSharper disable once MemberCanBePrivate.Global
[JsonIgnore]
public FileInfo Info => _info ??= new FileInfo(FullPath);
@ -161,7 +160,21 @@ public class FilePath : FileSystemPath, IPathObject
/// </summary>
public async Task<FilePath> MoveToAsync(FilePath destinationFile)
{
await Task.Run(() => Info.MoveTo(destinationFile.FullPath)).ConfigureAwait(false);
await Task.Run(() =>
{
var path = destinationFile.FullPath;
if (destinationFile.Exists)
{
var num = Random.Shared.NextInt64(0, 10000);
path = path.Replace(
destinationFile.NameWithoutExtension,
$"{destinationFile.NameWithoutExtension}_{num}"
);
}
Info.MoveTo(path);
})
.ConfigureAwait(false);
// Return the new path
return destinationFile;
}

20
StabilityMatrix.Core/Models/GenerationParameters.cs

@ -126,6 +126,26 @@ public partial record GenerationParameters
return (sampler, scheduler);
}
/// <summary>
/// Return a sample parameters for UI preview
/// </summary>
public static GenerationParameters GetSample()
{
return new GenerationParameters
{
PositivePrompt = "(cat:1.2), by artist, detailed, [shaded]",
NegativePrompt = "blurry, jpg artifacts",
Steps = 30,
CfgScale = 7,
Width = 640,
Height = 896,
Seed = 124825529,
ModelName = "ExampleMix7",
ModelHash = "b899d188a1ac7356bfb9399b2277d5b21712aa360f8f9514fba6fcce021baff7",
Sampler = "DPM++ 2M Karras"
};
}
// Example: Steps: 30, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 2216407431, Size: 640x896, Model hash: eb2h052f91, Model: anime_v1
[GeneratedRegex(
"""^Steps: (?<Steps>\d+), Sampler: (?<Sampler>.+?), CFG scale: (?<CfgScale>\d+(\.\d+)?), Seed: (?<Seed>\d+), Size: (?<Width>\d+)x(?<Height>\d+), Model hash: (?<ModelHash>.+?), Model: (?<ModelName>.+)$"""

1
StabilityMatrix.Core/Models/InstalledPackage.cs

@ -50,6 +50,7 @@ public class InstalledPackage : IJsonOnDeserialized
public bool UpdateAvailable { get; set; }
public TorchVersion? PreferredTorchVersion { get; set; }
public SharedFolderMethod? PreferredSharedFolderMethod { get; set; }
public bool UseSharedOutputFolder { get; set; }
/// <summary>
/// Get the launch args host option value.

1
StabilityMatrix.Core/Models/PackageModification/IPackageModificationRunner.cs

@ -12,6 +12,7 @@ public interface IPackageModificationRunner
List<string> ConsoleOutput { get; }
Guid Id { get; }
bool ShowDialogOnStart { get; init; }
bool HideCloseButton { get; init; }
string? ModificationCompleteMessage { get; init; }
bool Failed { get; set; }
}

13
StabilityMatrix.Core/Models/PackageModification/InstallPackageStep.cs

@ -8,12 +8,19 @@ public class InstallPackageStep : IPackageStep
{
private readonly BasePackage package;
private readonly TorchVersion torchVersion;
private readonly DownloadPackageVersionOptions versionOptions;
private readonly string installPath;
public InstallPackageStep(BasePackage package, TorchVersion torchVersion, string installPath)
public InstallPackageStep(
BasePackage package,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
string installPath
)
{
this.package = package;
this.torchVersion = torchVersion;
this.versionOptions = versionOptions;
this.installPath = installPath;
}
@ -25,9 +32,9 @@ public class InstallPackageStep : IPackageStep
}
await package
.InstallPackage(installPath, torchVersion, progress, OnConsoleOutput)
.InstallPackage(installPath, torchVersion, versionOptions, progress, OnConsoleOutput)
.ConfigureAwait(false);
}
public string ProgressTitle => "Installing package...";
public string ProgressTitle => $"Installing {package.DisplayName}...";
}

1
StabilityMatrix.Core/Models/PackageModification/PackageModificationRunner.cs

@ -54,6 +54,7 @@ public class PackageModificationRunner : IPackageModificationRunner
IsRunning = false;
}
public bool HideCloseButton { get; init; }
public string? ModificationCompleteMessage { get; init; }
public bool ShowDialogOnStart { get; init; }

11
StabilityMatrix.Core/Models/PackageModification/UpdatePackageStep.cs

@ -9,16 +9,19 @@ public class UpdatePackageStep : IPackageStep
{
private readonly ISettingsManager settingsManager;
private readonly InstalledPackage installedPackage;
private readonly DownloadPackageVersionOptions versionOptions;
private readonly BasePackage basePackage;
public UpdatePackageStep(
ISettingsManager settingsManager,
InstalledPackage installedPackage,
DownloadPackageVersionOptions versionOptions,
BasePackage basePackage
)
{
this.settingsManager = settingsManager;
this.installedPackage = installedPackage;
this.versionOptions = versionOptions;
this.basePackage = basePackage;
}
@ -33,7 +36,13 @@ public class UpdatePackageStep : IPackageStep
}
var updateResult = await basePackage
.Update(installedPackage, torchVersion, progress, onConsoleOutput: OnConsoleOutput)
.Update(
installedPackage,
torchVersion,
versionOptions,
progress,
onConsoleOutput: OnConsoleOutput
)
.ConfigureAwait(false);
settingsManager.UpdatePackageVersionNumber(installedPackage.Id, updateResult);

22
StabilityMatrix.Core/Models/Packages/A3WebUI.cs

@ -61,6 +61,17 @@ public class A3WebUI : BaseGitPackage
[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/txt2img-images" },
[SharedOutputType.Img2ImgGrids] = new[] { "outputs/img2img-grids" },
[SharedOutputType.Text2ImgGrids] = new[] { "outputs/txt2img-grids" }
};
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
@ -157,7 +168,7 @@ public class A3WebUI : BaseGitPackage
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm };
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm };
public override async Task<string> GetLatestVersion()
{
@ -165,15 +176,16 @@ public class A3WebUI : BaseGitPackage
return release.TagName!;
}
public override string OutputFolderName => "outputs";
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
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"));
@ -191,10 +203,6 @@ public class A3WebUI : BaseGitPackage
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);
}

224
StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs

@ -170,31 +170,45 @@ public abstract class BaseGitPackage : BasePackage
IProgress<ProgressReport>? progress = null
)
{
var downloadUrl = GetDownloadUrl(versionOptions);
if (!Directory.Exists(DownloadLocation.Replace($"{Name}.zip", "")))
if (!string.IsNullOrWhiteSpace(versionOptions.VersionTag))
{
await PrerequisiteHelper
.RunGit(
null,
null,
"clone",
"--branch",
versionOptions.VersionTag,
GithubUrl,
$"\"{installLocation}\""
)
.ConfigureAwait(false);
}
else if (!string.IsNullOrWhiteSpace(versionOptions.BranchName))
{
Directory.CreateDirectory(DownloadLocation.Replace($"{Name}.zip", ""));
await PrerequisiteHelper
.RunGit(
null,
null,
"clone",
"--branch",
versionOptions.BranchName,
GithubUrl,
$"\"{installLocation}\""
)
.ConfigureAwait(false);
}
await DownloadService
.DownloadToFileAsync(downloadUrl, DownloadLocation, progress: progress)
.ConfigureAwait(false);
if (!versionOptions.IsLatest && !string.IsNullOrWhiteSpace(versionOptions.CommitHash))
{
await PrerequisiteHelper
.RunGit(installLocation, null, "checkout", versionOptions.CommitHash)
.ConfigureAwait(false);
}
progress?.Report(new ProgressReport(100, message: "Download Complete"));
}
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
await UnzipPackage(installLocation, progress).ConfigureAwait(false);
File.Delete(DownloadLocation);
}
protected Task UnzipPackage(string installLocation, IProgress<ProgressReport>? progress = null)
{
using var zip = ZipFile.OpenRead(DownloadLocation);
@ -279,6 +293,7 @@ public abstract class BaseGitPackage : BasePackage
public override async Task<InstalledPackageVersion> Update(
InstalledPackage installedPackage,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false,
Action<ProcessOutput>? onConsoleOutput = null
@ -287,47 +302,146 @@ public abstract class BaseGitPackage : BasePackage
if (installedPackage.Version == null)
throw new NullReferenceException("Version is null");
if (installedPackage.Version.IsReleaseMode)
if (!Directory.Exists(Path.Combine(installedPackage.FullPath!, ".git")))
{
var releases = await GetAllReleases().ConfigureAwait(false);
var latestRelease = releases.First(x => includePrerelease || !x.Prerelease);
Logger.Info("not a git repo, initializing...");
progress?.Report(
new ProgressReport(-1f, "Initializing git repo", isIndeterminate: true)
);
await PrerequisiteHelper
.RunGit(installedPackage.FullPath!, onConsoleOutput, "init")
.ConfigureAwait(false);
await PrerequisiteHelper
.RunGit(
installedPackage.FullPath!,
onConsoleOutput,
"remote",
"add",
"origin",
GithubUrl
)
.ConfigureAwait(false);
}
await DownloadPackage(
installedPackage.FullPath,
new DownloadPackageVersionOptions { VersionTag = latestRelease.TagName },
progress
if (!string.IsNullOrWhiteSpace(versionOptions.VersionTag))
{
progress?.Report(new ProgressReport(-1f, "Fetching tags...", isIndeterminate: true));
await PrerequisiteHelper
.RunGit(installedPackage.FullPath!, onConsoleOutput, "fetch", "--tags")
.ConfigureAwait(false);
progress?.Report(
new ProgressReport(
-1f,
$"Checking out {versionOptions.VersionTag}",
isIndeterminate: true
)
);
await PrerequisiteHelper
.RunGit(
installedPackage.FullPath!,
onConsoleOutput,
"checkout",
versionOptions.VersionTag,
"--force"
)
.ConfigureAwait(false);
await InstallPackage(installedPackage.FullPath, torchVersion, progress, onConsoleOutput)
await InstallPackage(
installedPackage.FullPath!,
torchVersion,
new DownloadPackageVersionOptions
{
VersionTag = versionOptions.VersionTag,
IsLatest = versionOptions.IsLatest
},
progress,
onConsoleOutput
)
.ConfigureAwait(false);
return new InstalledPackageVersion { InstalledReleaseVersion = latestRelease.TagName };
return new InstalledPackageVersion
{
InstalledReleaseVersion = versionOptions.VersionTag
};
}
// Commit mode
var allCommits = await GetAllCommits(installedPackage.Version.InstalledBranch)
// fetch
progress?.Report(new ProgressReport(-1f, "Fetching data...", isIndeterminate: true));
await PrerequisiteHelper
.RunGit(installedPackage.FullPath!, onConsoleOutput, "fetch")
.ConfigureAwait(false);
var latestCommit = allCommits?.First();
if (latestCommit is null || string.IsNullOrEmpty(latestCommit.Sha))
if (versionOptions.IsLatest)
{
throw new Exception("No commits found for branch");
// checkout
progress?.Report(
new ProgressReport(
-1f,
$"Checking out {installedPackage.Version.InstalledBranch}...",
isIndeterminate: true
)
);
await PrerequisiteHelper
.RunGit(
installedPackage.FullPath!,
onConsoleOutput,
"checkout",
versionOptions.BranchName,
"--force"
)
.ConfigureAwait(false);
// pull
progress?.Report(new ProgressReport(-1f, "Pulling changes...", isIndeterminate: true));
await PrerequisiteHelper
.RunGit(
installedPackage.FullPath!,
onConsoleOutput,
"pull",
"origin",
installedPackage.Version.InstalledBranch
)
.ConfigureAwait(false);
}
else
{
// checkout
progress?.Report(
new ProgressReport(
-1f,
$"Checking out {installedPackage.Version.InstalledBranch}...",
isIndeterminate: true
)
);
await PrerequisiteHelper
.RunGit(
installedPackage.FullPath!,
onConsoleOutput,
"checkout",
versionOptions.CommitHash,
"--force"
)
.ConfigureAwait(false);
}
await DownloadPackage(
await InstallPackage(
installedPackage.FullPath,
new DownloadPackageVersionOptions { CommitHash = latestCommit.Sha },
progress
torchVersion,
new DownloadPackageVersionOptions
{
CommitHash = versionOptions.CommitHash,
IsLatest = versionOptions.IsLatest
},
progress,
onConsoleOutput
)
.ConfigureAwait(false);
await InstallPackage(installedPackage.FullPath, torchVersion, progress, onConsoleOutput)
.ConfigureAwait(false);
return new InstalledPackageVersion
{
InstalledBranch = installedPackage.Version.InstalledBranch,
InstalledCommitSha = latestCommit.Sha
InstalledBranch = versionOptions.BranchName,
InstalledCommitSha = versionOptions.CommitHash
};
}
@ -372,7 +486,37 @@ public abstract class BaseGitPackage : BasePackage
{
if (SharedFolders is not null && sharedFolderMethod == SharedFolderMethod.Symlink)
{
StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(this, installDirectory);
StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(
SharedFolders,
installDirectory
);
}
return Task.CompletedTask;
}
public override Task SetupOutputFolderLinks(DirectoryPath installDirectory)
{
if (SharedOutputFolders is { } sharedOutputFolders)
{
return StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage(
sharedOutputFolders,
SettingsManager.ImagesDirectory,
installDirectory,
recursiveDelete: true
);
}
return Task.CompletedTask;
}
public override Task RemoveOutputFolderLinks(DirectoryPath installDirectory)
{
if (SharedOutputFolders is { } sharedOutputFolders)
{
StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(
sharedOutputFolders,
installDirectory
);
}
return Task.CompletedTask;
}

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

@ -38,6 +38,8 @@ public abstract class BasePackage
public virtual bool IsInferenceCompatible => false;
public abstract string OutputFolderName { get; }
public abstract Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions,
@ -47,6 +49,7 @@ public abstract class BasePackage
public abstract Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
);
@ -63,6 +66,7 @@ public abstract class BasePackage
public abstract Task<InstalledPackageVersion> Update(
InstalledPackage installedPackage,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false,
Action<ProcessOutput>? onConsoleOutput = null
@ -93,8 +97,13 @@ public abstract class BasePackage
SharedFolderMethod sharedFolderMethod
);
public abstract Task SetupOutputFolderLinks(DirectoryPath installDirectory);
public abstract Task RemoveOutputFolderLinks(DirectoryPath installDirectory);
public abstract IEnumerable<TorchVersion> AvailableTorchVersions { get; }
public virtual bool IsCompatible => GetRecommendedTorchVersion() != TorchVersion.Cpu;
public virtual TorchVersion GetRecommendedTorchVersion()
{
// if there's only one AvailableTorchVersion, return that
@ -142,7 +151,11 @@ public abstract class BasePackage
/// The shared folders that this package supports.
/// Mapping of <see cref="SharedFolderType"/> to the relative paths from the package root.
/// </summary>
public virtual Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public abstract Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public abstract Dictionary<
SharedOutputType,
IReadOnlyList<string>
>? SharedOutputFolders { get; }
public abstract Task<string> GetLatestVersion();
public abstract Task<PackageVersionOptions> GetAllVersionOptions();

36
StabilityMatrix.Core/Models/Packages/ComfyUI.cs

@ -30,6 +30,8 @@ public class ComfyUI : BaseGitPackage
new("https://github.com/comfyanonymous/ComfyUI/raw/master/comfyui_screenshot.png");
public override bool ShouldIgnoreReleases => true;
public override bool IsInferenceCompatible => true;
public override string OutputFolderName => "output";
public override SharedFolderMethod RecommendedSharedFolderMethod =>
SharedFolderMethod.Configuration;
@ -58,6 +60,9 @@ public class ComfyUI : BaseGitPackage
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" },
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new() { [SharedOutputType.Text2Img] = new[] { "output" } };
public override List<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
{
@ -141,17 +146,23 @@ public class ComfyUI : BaseGitPackage
public override Task<string> GetLatestVersion() => Task.FromResult("master");
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm };
new[]
{
TorchVersion.Cpu,
TorchVersion.Cuda,
TorchVersion.DirectMl,
TorchVersion.Rocm,
TorchVersion.Mps
};
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true));
// Setup venv
await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
@ -165,13 +176,24 @@ public class ComfyUI : BaseGitPackage
await InstallCpuTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
break;
case TorchVersion.Cuda:
await InstallCudaTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsCuda121, onConsoleOutput)
.ConfigureAwait(false);
await venvRunner
.PipInstall("xformers==0.0.22.post4 --upgrade")
.ConfigureAwait(false);
break;
case TorchVersion.DirectMl:
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsDirectML, onConsoleOutput)
.ConfigureAwait(false);
break;
case TorchVersion.Rocm:
await InstallRocmTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
break;
case TorchVersion.DirectMl:
await InstallDirectMlTorch(venvRunner, progress, onConsoleOutput)
case TorchVersion.Mps:
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsNightlyCpu, onConsoleOutput)
.ConfigureAwait(false);
break;
default:
@ -441,7 +463,7 @@ public class ComfyUI : BaseGitPackage
await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false);
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm542, onConsoleOutput)
.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm56, onConsoleOutput)
.ConfigureAwait(false);
}

20
StabilityMatrix.Core/Models/Packages/DankDiffusion.cs

@ -1,6 +1,7 @@
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;
@ -30,6 +31,19 @@ public class DankDiffusion : BaseGitPackage
public override Uri PreviewImageUri { get; }
public override string OutputFolderName { get; }
public override Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
throw new NotImplementedException();
}
public override Task RunPackage(
string installedPackagePath,
string command,
@ -68,6 +82,12 @@ public class DankDiffusion : BaseGitPackage
public override List<LaunchOptionDefinition> LaunchOptions { get; }
public override Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public override Dictionary<
SharedOutputType,
IReadOnlyList<string>
>? SharedOutputFolders { get; }
public override Task<string> GetLatestVersion()
{
throw new NotImplementedException();

67
StabilityMatrix.Core/Models/Packages/Fooocus.cs

@ -38,6 +38,12 @@ public class Fooocus : BaseGitPackage
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
{
new LaunchOptionDefinition
{
Name = "Preset",
Type = LaunchOptionType.Bool,
Options = { "--preset anime", "--preset realistic" }
},
new LaunchOptionDefinition
{
Name = "Port",
@ -59,6 +65,49 @@ public class Fooocus : BaseGitPackage
Description = "Set the listen interface",
Options = { "--listen" }
},
new LaunchOptionDefinition
{
Name = "Output Directory",
Type = LaunchOptionType.String,
Description = "Override the output directory",
Options = { "--output-directory" }
},
new()
{
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" }
},
new LaunchOptionDefinition
{
Name = "Use DirectML",
Type = LaunchOptionType.Bool,
Description = "Use pytorch with DirectML support",
InitialValue = HardwareHelper.PreferDirectML(),
Options = { "--directml" }
},
new LaunchOptionDefinition
{
Name = "Disable Xformers",
Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu(),
Options = { "--disable-xformers" }
},
new LaunchOptionDefinition
{
Name = "Auto-Launch",
Type = LaunchOptionType.Bool,
Options = { "--auto-launch" }
},
LaunchOptionDefinition.Extras
};
@ -83,21 +132,26 @@ public class Fooocus : BaseGitPackage
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new() { [SharedOutputType.Text2Img] = new[] { "outputs" } };
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm };
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm };
public override Task<string> GetLatestVersion() => Task.FromResult("main");
public override bool ShouldIgnoreReleases => true;
public override string OutputFolderName => "outputs";
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
var venvRunner = await SetupVenv(installLocation, forceRecreate: true)
.ConfigureAwait(false);
@ -108,7 +162,7 @@ public class Fooocus : BaseGitPackage
switch (torchVersion)
{
case TorchVersion.Cuda:
torchVersionStr = "cu118";
torchVersionStr = "cu121";
break;
case TorchVersion.Rocm:
torchVersionStr = "rocm5.4.2";
@ -121,11 +175,16 @@ public class Fooocus : BaseGitPackage
await venvRunner
.PipInstall(
$"torch==2.0.1 torchvision==0.15.2 --extra-index-url https://download.pytorch.org/whl/{torchVersionStr}",
$"torch==2.1.0 torchvision==0.16.0 --extra-index-url https://download.pytorch.org/whl/{torchVersionStr}",
onConsoleOutput
)
.ConfigureAwait(false);
if (torchVersion == TorchVersion.Cuda)
{
await venvRunner.PipInstall("xformers==0.0.22.post4 --upgrade").ConfigureAwait(false);
}
var requirements = new FilePath(installLocation, "requirements_versions.txt");
await venvRunner
.PipInstallFromRequirements(requirements, onConsoleOutput, excludes: "torch")

10
StabilityMatrix.Core/Models/Packages/FooocusMre.cs

@ -37,6 +37,9 @@ public class FooocusMre : BaseGitPackage
"https://user-images.githubusercontent.com/130458190/265366059-ce430ea0-0995-4067-98dd-cef1d7dc1ab6.png"
);
public override string Disclaimer =>
"This package may no longer receive updates from its author. It may be removed from Stability Matrix in the future.";
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
{
@ -85,6 +88,9 @@ public class FooocusMre : BaseGitPackage
[SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new() { [SharedOutputType.Text2Img] = new[] { "outputs" } };
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm };
@ -94,14 +100,16 @@ public class FooocusMre : BaseGitPackage
return release.TagName!;
}
public override string OutputFolderName => "outputs";
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
var venvRunner = await SetupVenv(installLocation, forceRecreate: true)
.ConfigureAwait(false);

150
StabilityMatrix.Core/Models/Packages/InvokeAI.cs

@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
using System.Globalization;
using System.Text.RegularExpressions;
using NLog;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
@ -69,6 +70,11 @@ public class InvokeAI : BaseGitPackage
[SharedFolderType.ControlNet] = new[] { RelativeRootPath + "/autoimport/controlnet" },
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new() { [SharedOutputType.Text2Img] = new[] { "invokeai-root/outputs/images" } };
public override string OutputFolderName => "invokeai-root/outputs/images";
// https://github.com/invoke-ai/InvokeAI/blob/main/docs/features/CONFIGURATION.md
public override List<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
@ -138,18 +144,10 @@ public class InvokeAI : BaseGitPackage
return base.GetRecommendedTorchVersion();
}
public override Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions downloadOptions,
IProgress<ProgressReport>? progress = null
)
{
return Task.CompletedTask;
}
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
@ -157,7 +155,10 @@ public class InvokeAI : BaseGitPackage
// Setup venv
progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true));
await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
var venvPath = Path.Combine(installLocation, "venv");
var exists = Directory.Exists(venvPath);
await using var venvRunner = new PyVenvRunner(venvPath);
venvRunner.WorkingDirectory = installLocation;
await venvRunner.Setup(true, onConsoleOutput).ConfigureAwait(false);
@ -165,29 +166,36 @@ public class InvokeAI : BaseGitPackage
progress?.Report(new ProgressReport(-1f, "Installing Package", isIndeterminate: true));
var pipCommandArgs =
"InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu";
"-e . --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu";
switch (torchVersion)
{
// If has Nvidia Gpu, install CUDA version
case TorchVersion.Cuda:
await InstallCudaTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
Logger.Info("Starting InvokeAI install (CUDA)...");
pipCommandArgs =
"InvokeAI[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu117";
"-e .[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu118";
break;
// For AMD, Install ROCm version
case TorchVersion.Rocm:
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm542, onConsoleOutput)
.ConfigureAwait(false);
Logger.Info("Starting InvokeAI install (ROCm)...");
pipCommandArgs =
"InvokeAI --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2";
"-e . --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2";
break;
case TorchVersion.Mps:
// For Apple silicon, use MPS
Logger.Info("Starting InvokeAI install (MPS)...");
pipCommandArgs = "InvokeAI --use-pep517";
pipCommandArgs = "-e . --use-pep517";
break;
}
await venvRunner.PipInstall(pipCommandArgs, onConsoleOutput).ConfigureAwait(false);
await venvRunner
.PipInstall($"{pipCommandArgs}{(exists ? " --upgrade" : "")}", onConsoleOutput)
.ConfigureAwait(false);
await venvRunner
.PipInstall("rich packaging python-dotenv", onConsoleOutput)
@ -207,75 +215,6 @@ public class InvokeAI : BaseGitPackage
progress?.Report(new ProgressReport(1f, "Done!", isIndeterminate: false));
}
public override async Task<InstalledPackageVersion> Update(
InstalledPackage installedPackage,
TorchVersion torchVersion,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false,
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true));
if (installedPackage.FullPath is null || installedPackage.Version is null)
{
throw new NullReferenceException("Installed package is missing Path and/or Version");
}
await using var venvRunner = new PyVenvRunner(
Path.Combine(installedPackage.FullPath, "venv")
);
venvRunner.WorkingDirectory = installedPackage.FullPath;
venvRunner.EnvironmentVariables = GetEnvVars(installedPackage.FullPath);
var latestVersion = await GetUpdateVersion(installedPackage).ConfigureAwait(false);
var isReleaseMode = installedPackage.Version.IsReleaseMode;
var downloadUrl = isReleaseMode
? $"https://github.com/invoke-ai/InvokeAI/archive/{latestVersion}.zip"
: $"https://github.com/invoke-ai/InvokeAI/archive/refs/heads/{installedPackage.Version.InstalledBranch}.zip";
var gpus = HardwareHelper.IterGpuInfo().ToList();
progress?.Report(new ProgressReport(-1f, "Installing Package", isIndeterminate: true));
var pipCommandArgs =
$"\"InvokeAI @ {downloadUrl}\" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu --upgrade";
switch (torchVersion)
{
// If has Nvidia Gpu, install CUDA version
case TorchVersion.Cuda:
Logger.Info("Starting InvokeAI install (CUDA)...");
pipCommandArgs =
$"\"InvokeAI[xformers] @ {downloadUrl}\" --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu117 --upgrade";
break;
// For AMD, Install ROCm version
case TorchVersion.Rocm:
Logger.Info("Starting InvokeAI install (ROCm)...");
pipCommandArgs =
$"\"InvokeAI @ {downloadUrl}\" --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2 --upgrade";
break;
case TorchVersion.Mps:
// For Apple silicon, use MPS
Logger.Info("Starting InvokeAI install (MPS)...");
pipCommandArgs = $"\"InvokeAI @ {downloadUrl}\" --use-pep517 --upgrade";
break;
}
await venvRunner.PipInstall(pipCommandArgs, onConsoleOutput).ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, "Done!", isIndeterminate: false));
return isReleaseMode
? new InstalledPackageVersion { InstalledReleaseVersion = latestVersion }
: new InstalledPackageVersion
{
InstalledBranch = installedPackage.Version.InstalledBranch,
InstalledCommitSha = latestVersion
};
}
public override Task RunPackage(
string installedPackagePath,
string command,
@ -283,27 +222,6 @@ public class InvokeAI : BaseGitPackage
Action<ProcessOutput>? onConsoleOutput
) => RunInvokeCommand(installedPackagePath, command, arguments, true, onConsoleOutput);
private async Task<string> GetUpdateVersion(
InstalledPackage installedPackage,
bool includePrerelease = false
)
{
if (installedPackage.Version == null)
throw new NullReferenceException("Installed package version is null");
if (installedPackage.Version.IsReleaseMode)
{
var releases = await GetAllReleases().ConfigureAwait(false);
var latestRelease = releases.First(x => includePrerelease || !x.Prerelease);
return latestRelease.TagName;
}
var allCommits = await GetAllCommits(installedPackage.Version.InstalledBranch)
.ConfigureAwait(false);
var latestCommit = allCommits.First();
return latestCommit.Sha;
}
private async Task RunInvokeCommand(
string installedPackagePath,
string command,
@ -317,7 +235,6 @@ public class InvokeAI : BaseGitPackage
arguments = command switch
{
"invokeai-configure" => "--yes --skip-sd-weights",
"invokeai-model-install" => "--yes",
_ => arguments
};
@ -340,6 +257,21 @@ public class InvokeAI : BaseGitPackage
// above the minimum in invokeai.frontend.install.widgets
var code = $"""
try:
import os
import shutil
from invokeai.frontend.install import widgets
_min_cols = widgets.MIN_COLS
_min_lines = widgets.MIN_LINES
static_size_fn = lambda: os.terminal_size((_min_cols, _min_lines))
shutil.get_terminal_size = static_size_fn
widgets.get_terminal_size = static_size_fn
except Exception as e:
import warnings
warnings.warn('Could not patch terminal size for InvokeAI' + str(e))
import sys
from {split[0]} import {split[1]}
sys.exit({split[1]}())

247
StabilityMatrix.Core/Models/Packages/StableDiffusionDirectMl.cs

@ -0,0 +1,247 @@
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 StableDiffusionDirectMl : BaseGitPackage
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
public override string Name => "stable-diffusion-webui-directml";
public override string DisplayName { get; set; } = "Stable Diffusion Web UI";
public override string Author => "lshqqytiger";
public override string LicenseType => "AGPL-3.0";
public override string LicenseUrl =>
"https://github.com/lshqqytiger/stable-diffusion-webui-directml/blob/master/LICENSE.txt";
public override string Blurb =>
"A fork of Automatic1111's Stable Diffusion WebUI with DirectML support";
public override string LaunchCommand => "launch.py";
public override Uri PreviewImageUri =>
new(
"https://github.com/lshqqytiger/stable-diffusion-webui-directml/raw/master/screenshot.png"
);
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public StableDiffusionDirectMl(
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/txt2img-images" },
[SharedOutputType.Img2ImgGrids] = new[] { "outputs/img2img-grids" },
[SharedOutputType.Text2ImgGrids] = new[] { "outputs/txt2img-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.DirectMl };
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,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
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.DirectMl:
await InstallDirectMlTorch(venvRunner, progress, onConsoleOutput)
.ConfigureAwait(false);
break;
case TorchVersion.Cpu:
await InstallCpuTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
break;
}
// 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, "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);
}
}

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

@ -0,0 +1,267 @@
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/txt2img-images" },
[SharedOutputType.Img2ImgGrids] = new[] { "outputs/img2img-grids" },
[SharedOutputType.Text2ImgGrids] = new[] { "outputs/txt2img-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.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,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
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;
}
// 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, "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);
}
}

20
StabilityMatrix.Core/Models/Packages/UnknownPackage.cs

@ -26,6 +26,8 @@ public class UnknownPackage : BasePackage
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override string OutputFolderName { get; }
public override Task DownloadPackage(
string installLocation,
DownloadPackageVersionOptions versionOptions,
@ -39,6 +41,7 @@ public class UnknownPackage : BasePackage
public override Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
@ -83,6 +86,16 @@ public class UnknownPackage : BasePackage
throw new NotImplementedException();
}
public override Task SetupOutputFolderLinks(DirectoryPath installDirectory)
{
throw new NotImplementedException();
}
public override Task RemoveOutputFolderLinks(DirectoryPath installDirectory)
{
throw new NotImplementedException();
}
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cuda, TorchVersion.Cpu, TorchVersion.Rocm, TorchVersion.DirectMl };
@ -108,6 +121,7 @@ public class UnknownPackage : BasePackage
public override Task<InstalledPackageVersion> Update(
InstalledPackage installedPackage,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false,
Action<ProcessOutput>? onConsoleOutput = null
@ -122,6 +136,12 @@ public class UnknownPackage : BasePackage
public override List<LaunchOptionDefinition> LaunchOptions => new();
public override Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public override Dictionary<
SharedOutputType,
IReadOnlyList<string>
>? SharedOutputFolders { get; }
public override Task<string> GetLatestVersion() => Task.FromResult(string.Empty);
public override Task<PackageVersionOptions> GetAllVersionOptions() =>

35
StabilityMatrix.Core/Models/Packages/VladAutomatic.cs

@ -34,7 +34,7 @@ public class VladAutomatic : BaseGitPackage
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Rocm, TorchVersion.DirectMl, TorchVersion.Cuda };
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm };
public VladAutomatic(
IGithubApiCache githubApi,
@ -67,6 +67,19 @@ public class VladAutomatic : BaseGitPackage
[SharedFolderType.ControlNet] = new[] { "models/ControlNet" }
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new()
{
[SharedOutputType.Text2Img] = new[] { "outputs/text" },
[SharedOutputType.Img2Img] = new[] { "outputs/image" },
[SharedOutputType.Extras] = new[] { "outputs/extras" },
[SharedOutputType.Img2ImgGrids] = new[] { "outputs/grids" },
[SharedOutputType.Text2ImgGrids] = new[] { "outputs/grids" },
[SharedOutputType.Saved] = new[] { "outputs/save" },
};
public override string OutputFolderName => "outputs";
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
@ -161,6 +174,7 @@ public class VladAutomatic : BaseGitPackage
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
@ -225,6 +239,7 @@ public class VladAutomatic : BaseGitPackage
await PrerequisiteHelper
.RunGit(
installDir.Parent ?? "",
null,
"clone",
"https://github.com/vladmandic/automatic",
installDir.Name
@ -232,7 +247,7 @@ public class VladAutomatic : BaseGitPackage
.ConfigureAwait(false);
await PrerequisiteHelper
.RunGit(installLocation, "checkout", downloadOptions.CommitHash)
.RunGit(installLocation, null, "checkout", downloadOptions.CommitHash)
.ConfigureAwait(false);
}
else if (!string.IsNullOrWhiteSpace(downloadOptions.BranchName))
@ -240,6 +255,7 @@ public class VladAutomatic : BaseGitPackage
await PrerequisiteHelper
.RunGit(
installDir.Parent ?? "",
null,
"clone",
"-b",
downloadOptions.BranchName,
@ -288,16 +304,12 @@ public class VladAutomatic : BaseGitPackage
public override async Task<InstalledPackageVersion> Update(
InstalledPackage installedPackage,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
bool includePrerelease = false,
Action<ProcessOutput>? onConsoleOutput = null
)
{
if (installedPackage.Version is null)
{
throw new Exception("Version is null");
}
progress?.Report(
new ProgressReport(
-1f,
@ -308,7 +320,12 @@ public class VladAutomatic : BaseGitPackage
);
await PrerequisiteHelper
.RunGit(installedPackage.FullPath, "checkout", installedPackage.Version.InstalledBranch)
.RunGit(
installedPackage.FullPath,
onConsoleOutput,
"checkout",
versionOptions.BranchName
)
.ConfigureAwait(false);
var venvRunner = new PyVenvRunner(Path.Combine(installedPackage.FullPath!, "venv"));
@ -327,7 +344,7 @@ public class VladAutomatic : BaseGitPackage
return new InstalledPackageVersion
{
InstalledBranch = installedPackage.Version.InstalledBranch,
InstalledBranch = versionOptions.BranchName,
InstalledCommitSha = output.Replace(Environment.NewLine, "").Replace("\n", "")
};
}

16
StabilityMatrix.Core/Models/Packages/VoltaML.cs

@ -45,9 +45,20 @@ public class VoltaML : BaseGitPackage
[SharedFolderType.TextualInversion] = new[] { "data/textual-inversion" },
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new()
{
[SharedOutputType.Text2Img] = new[] { "data/outputs/txt2img" },
[SharedOutputType.Extras] = new[] { "data/outputs/extra" },
[SharedOutputType.Img2Img] = new[] { "data/outputs/img2img" },
};
public override string OutputFolderName => "data/outputs";
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override IEnumerable<TorchVersion> AvailableTorchVersions => new[] { TorchVersion.None };
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Mps };
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };
@ -136,12 +147,11 @@ public class VoltaML : BaseGitPackage
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
await base.InstallPackage(installLocation, torchVersion, progress).ConfigureAwait(false);
// Setup venv
progress?.Report(new ProgressReport(-1, "Setting up venv", isIndeterminate: true));
await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));

5
StabilityMatrix.Core/Models/Settings/Settings.cs

@ -70,6 +70,11 @@ public class Settings
/// </summary>
public bool IsCompletionRemoveUnderscoresEnabled { get; set; } = true;
/// <summary>
/// Format for Inference output image file names
/// </summary>
public string? InferenceOutputImageFileNameFormat { get; set; }
/// <summary>
/// Whether the Inference Image Viewer shows pixel grids at high zoom levels
/// </summary>

12
StabilityMatrix.Core/Models/SharedOutputType.cs

@ -0,0 +1,12 @@
namespace StabilityMatrix.Core.Models;
public enum SharedOutputType
{
All,
Text2Img,
Img2Img,
Extras,
Text2ImgGrids,
Img2ImgGrids,
Saved
}

45
StabilityMatrix.Core/Python/PyVenvRunner.cs

@ -23,6 +23,8 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
public const string TorchPipInstallArgsCuda =
$"{TorchPipInstallArgs} --extra-index-url https://download.pytorch.org/whl/cu118";
public const string TorchPipInstallArgsCuda121 =
"torch torchvision --extra-index-url https://download.pytorch.org/whl/cu121";
public const string TorchPipInstallArgsCpu = TorchPipInstallArgs;
public const string TorchPipInstallArgsDirectML = "torch-directml";
@ -30,8 +32,11 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
$"{TorchPipInstallArgs} --extra-index-url https://download.pytorch.org/whl/rocm5.1.1";
public const string TorchPipInstallArgsRocm542 =
$"{TorchPipInstallArgs} --extra-index-url https://download.pytorch.org/whl/rocm5.4.2";
public const string TorchPipInstallArgsRocmNightly56 =
$"--pre {TorchPipInstallArgs} --index-url https://download.pytorch.org/whl/nightly/rocm5.6";
public const string TorchPipInstallArgsRocm56 =
$"{TorchPipInstallArgs} --index-url https://download.pytorch.org/whl/rocm5.6";
public const string TorchPipInstallArgsNightlyCpu =
"--pre torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/nightly/cpu";
/// <summary>
/// Relative path to the site-packages folder from the venv root.
@ -243,6 +248,42 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
}
}
/// <summary>
/// Run a pip uninstall command. Waits for the process to exit.
/// workingDirectory defaults to RootPath.
/// </summary>
public async Task PipUninstall(string args, Action<ProcessOutput>? outputDataReceived = null)
{
if (!File.Exists(PipPath))
{
throw new FileNotFoundException("pip not found", PipPath);
}
// Record output for errors
var output = new StringBuilder();
var outputAction = new Action<ProcessOutput>(s =>
{
Logger.Debug($"Pip output: {s.Text}");
// Record to output
output.Append(s.Text);
// Forward to callback
outputDataReceived?.Invoke(s);
});
SetPyvenvCfg(PyRunner.PythonDir);
RunDetached($"-m pip uninstall -y {args}", outputAction);
await Process.WaitForExitAsync().ConfigureAwait(false);
// Check return code
if (Process.ExitCode != 0)
{
throw new ProcessException(
$"pip install failed with code {Process.ExitCode}: {output.ToString().ToRepr()}"
);
}
}
/// <summary>
/// Pip install from a requirements.txt file.
/// </summary>

10
StabilityMatrix.Core/Services/IImageIndexService.cs

@ -9,11 +9,6 @@ public interface IImageIndexService
{
IndexCollection<LocalImageFile, string> InferenceImages { get; }
/// <summary>
/// Gets a list of local images that start with the given path prefix
/// </summary>
Task<IReadOnlyList<LocalImageFile>> GetLocalImagesByPrefix(string pathPrefix);
/// <summary>
/// Refresh index for all collections
/// </summary>
@ -25,9 +20,4 @@ public interface IImageIndexService
/// Refreshes the index of local images in the background
/// </summary>
void BackgroundRefreshIndex();
/// <summary>
/// Removes a local image from the database
/// </summary>
Task RemoveImage(LocalImageFile imageFile);
}

130
StabilityMatrix.Core/Services/ImageIndexService.cs

@ -1,11 +1,8 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.Json;
using AsyncAwaitBestPractices;
using DynamicData;
using DynamicData.Binding;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
@ -16,46 +13,30 @@ namespace StabilityMatrix.Core.Services;
public class ImageIndexService : IImageIndexService
{
private readonly ILogger<ImageIndexService> logger;
private readonly ILiteDbContext liteDbContext;
private readonly ISettingsManager settingsManager;
/// <inheritdoc />
public IndexCollection<LocalImageFile, string> InferenceImages { get; }
public ImageIndexService(
ILogger<ImageIndexService> logger,
ILiteDbContext liteDbContext,
ISettingsManager settingsManager
)
public ImageIndexService(ILogger<ImageIndexService> logger, ISettingsManager settingsManager)
{
this.logger = logger;
this.liteDbContext = liteDbContext;
this.settingsManager = settingsManager;
InferenceImages = new IndexCollection<LocalImageFile, string>(
this,
file => file.RelativePath
file => file.AbsolutePath
)
{
RelativePath = "inference"
RelativePath = "Inference"
};
EventManager.Instance.ImageFileAdded += OnImageFileAdded;
}
/// <inheritdoc />
public async Task<IReadOnlyList<LocalImageFile>> GetLocalImagesByPrefix(string pathPrefix)
{
return await liteDbContext.LocalImageFiles
.Query()
.Where(imageFile => imageFile.RelativePath.StartsWith(pathPrefix))
.ToArrayAsync()
.ConfigureAwait(false);
}
public async Task RefreshIndexForAllCollections()
public Task RefreshIndexForAllCollections()
{
await RefreshIndex(InferenceImages).ConfigureAwait(false);
return RefreshIndex(InferenceImages);
}
public async Task RefreshIndex(IndexCollection<LocalImageFile, string> indexCollection)
@ -120,110 +101,9 @@ public class ImageIndexService : IImageIndexService
}
}
/*public async Task RefreshIndex(IndexCollection<LocalImageFile, string> indexCollection)
{
var imagesDir = settingsManager.ImagesDirectory;
if (!imagesDir.Exists)
{
return;
}
// Start
var stopwatch = Stopwatch.StartNew();
logger.LogInformation("Refreshing images index...");
using var db = await liteDbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
var localImageFiles = db.GetCollection<LocalImageFile>("LocalImageFiles")!;
await localImageFiles.DeleteAllAsync().ConfigureAwait(false);
// Record start of actual indexing
var indexStart = stopwatch.Elapsed;
var added = 0;
foreach (
var file in imagesDir.Info
.EnumerateFiles("*.*", SearchOption.AllDirectories)
.Where(info => LocalImageFile.SupportedImageExtensions.Contains(info.Extension))
.Select(info => new FilePath(info))
)
{
var relativePath = Path.GetRelativePath(imagesDir, file);
// Skip if not in sub-path
if (!string.IsNullOrEmpty(subPath) && !relativePath.StartsWith(subPath))
{
continue;
}
// TODO: Support other types
const LocalImageFileType imageType =
LocalImageFileType.Inference | LocalImageFileType.TextToImage;
// Get metadata
using var reader = new BinaryReader(new FileStream(file.FullPath, FileMode.Open));
var metadata = ImageMetadata.ReadTextChunk(reader, "parameters-json");
GenerationParameters? genParams = null;
if (!string.IsNullOrWhiteSpace(metadata))
{
genParams = JsonSerializer.Deserialize<GenerationParameters>(metadata);
}
else
{
metadata = ImageMetadata.ReadTextChunk(reader, "parameters");
if (!string.IsNullOrWhiteSpace(metadata))
{
GenerationParameters.TryParse(metadata, out genParams);
}
}
var localImage = new LocalImageFile
{
RelativePath = relativePath,
ImageType = imageType,
CreatedAt = file.Info.CreationTimeUtc,
LastModifiedAt = file.Info.LastWriteTimeUtc,
GenerationParameters = genParams
};
// Insert into database
await localImageFiles.InsertAsync(localImage).ConfigureAwait(false);
added++;
}
// Record end of actual indexing
var indexEnd = stopwatch.Elapsed;
await db.CommitAsync().ConfigureAwait(false);
// End
stopwatch.Stop();
var indexDuration = indexEnd - indexStart;
var dbDuration = stopwatch.Elapsed - indexDuration;
logger.LogInformation(
"Image index updated for {Prefix} with {Entries} files, took {IndexDuration:F1}ms ({DbDuration:F1}ms db)",
subPath,
added,
indexDuration.TotalMilliseconds,
dbDuration.TotalMilliseconds
);
}*/
/// <inheritdoc />
public void BackgroundRefreshIndex()
{
RefreshIndexForAllCollections().SafeFireAndForget();
}
/// <inheritdoc />
public async Task RemoveImage(LocalImageFile imageFile)
{
await liteDbContext.LocalImageFiles
.DeleteAsync(imageFile.RelativePath)
.ConfigureAwait(false);
}
}

28
StabilityMatrix.Tests/Avalonia/FileNameFormatProviderTests.cs

@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using StabilityMatrix.Avalonia.Models.Inference;
namespace StabilityMatrix.Tests.Avalonia;
[TestClass]
public class FileNameFormatProviderTests
{
[TestMethod]
public void TestFileNameFormatProviderValidate_Valid_ShouldNotThrow()
{
var provider = new FileNameFormatProvider();
var result = provider.Validate("{date}_{time}-{model_name}-{seed}");
Assert.AreEqual(ValidationResult.Success, result);
}
[TestMethod]
public void TestFileNameFormatProviderValidate_Invalid_ShouldThrow()
{
var provider = new FileNameFormatProvider();
var result = provider.Validate("{date}_{time}-{model_name}-{seed}-{invalid}");
Assert.AreNotEqual(ValidationResult.Success, result);
Assert.AreEqual("Unknown variable 'invalid'", result.ErrorMessage);
}
}

24
StabilityMatrix.Tests/Avalonia/FileNameFormatTests.cs

@ -0,0 +1,24 @@
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Tests.Avalonia;
[TestClass]
public class FileNameFormatTests
{
[TestMethod]
public void TestFileNameFormatParse()
{
var provider = new FileNameFormatProvider
{
GenerationParameters = new GenerationParameters { Seed = 123 },
ProjectName = "uwu",
ProjectType = InferenceProjectType.TextToImage,
};
var format = FileNameFormat.Parse("{project_type} - {project_name} ({seed})", provider);
Assert.AreEqual("TextToImage - uwu (123)", format.GetFileName());
}
}
Loading…
Cancel
Save