Browse Source

Merge pull request #546 from ionite34/merge-dev-to-main-298e577

Merge dev to main
pull/495/head
Ionite 9 months ago committed by GitHub
parent
commit
6f1f2662c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      Avalonia.Gif/Avalonia.Gif.csproj
  2. 67
      CHANGELOG.md
  3. 4
      StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
  4. 2
      StabilityMatrix.Avalonia/App.axaml
  5. 11
      StabilityMatrix.Avalonia/Assets/hf-packages.json
  6. 8
      StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
  7. 37
      StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml.cs
  8. 10
      StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml
  9. 81
      StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
  10. 50
      StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml
  11. 96
      StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml
  12. 52
      StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml.cs
  13. 4
      StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml
  14. 65
      StabilityMatrix.Avalonia/Controls/TreeFileExplorer.axaml
  15. 135
      StabilityMatrix.Avalonia/Controls/TreeFileExplorer.axaml.cs
  16. 5
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  17. 18
      StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs
  18. 24
      StabilityMatrix.Avalonia/Helpers/IOCommands.cs
  19. 153
      StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs
  20. 114
      StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs
  21. 47
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  22. 4
      StabilityMatrix.Avalonia/Languages/Resources.de.resx
  23. 181
      StabilityMatrix.Avalonia/Languages/Resources.es.resx
  24. 38
      StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx
  25. 31
      StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx
  26. 24
      StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx
  27. 17
      StabilityMatrix.Avalonia/Languages/Resources.resx
  28. 58
      StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx
  29. 7
      StabilityMatrix.Avalonia/Models/AppArgs.cs
  30. 42
      StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs
  31. 8
      StabilityMatrix.Avalonia/Models/Inference/PromptCardModel.cs
  32. 50
      StabilityMatrix.Avalonia/Models/TreeFileExplorer/TreeFileExplorerDirectory.cs
  33. 6
      StabilityMatrix.Avalonia/Models/TreeFileExplorer/TreeFileExplorerFile.cs
  34. 10
      StabilityMatrix.Avalonia/Models/TreeFileExplorer/TreeFileExplorerItem.cs
  35. 15
      StabilityMatrix.Avalonia/Models/TreeFileExplorer/TreeFileExplorerOptions.cs
  36. 8
      StabilityMatrix.Avalonia/Models/TreeFileExplorer/TreeFileExplorerType.cs
  37. 11
      StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs
  38. 46
      StabilityMatrix.Avalonia/Services/InferenceClientManager.cs
  39. 33
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  40. 2
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings
  41. 33
      StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml
  42. 174
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
  43. 14
      StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs
  44. 37
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
  45. 12
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  46. 49
      StabilityMatrix.Avalonia/ViewModels/Dialogs/DownloadResourceViewModel.cs
  47. 111
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
  48. 27
      StabilityMatrix.Avalonia/ViewModels/Inference/ControlNetCardViewModel.cs
  49. 2
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
  50. 2
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
  51. 71
      StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
  52. 40
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/PromptExpansionModule.cs
  53. 50
      StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs
  54. 47
      StabilityMatrix.Avalonia/ViewModels/Inference/PromptExpansionCardViewModel.cs
  55. 13
      StabilityMatrix.Avalonia/ViewModels/Inference/SelectImageCardViewModel.cs
  56. 12
      StabilityMatrix.Avalonia/ViewModels/Inference/StackEditableCardViewModel.cs
  57. 31
      StabilityMatrix.Avalonia/ViewModels/Inference/UpscalerCardViewModel.cs
  58. 36
      StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
  59. 6
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallBrowserViewModel.cs
  60. 47
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs
  61. 2
      StabilityMatrix.Avalonia/Views/Dialogs/DownloadResourceDialog.axaml
  62. 3
      StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml
  63. 58
      StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml
  64. 10
      StabilityMatrix.Core/Api/ApiFactory.cs
  65. 6
      StabilityMatrix.Core/Api/IApiFactory.cs
  66. 14
      StabilityMatrix.Core/Attributes/TypedNodeOptionsAttribute.cs
  67. 49
      StabilityMatrix.Core/Converters/Json/NodeConnectionBaseJsonConverter.cs
  68. 63
      StabilityMatrix.Core/Converters/Json/OneOfJsonConverter.cs
  69. 13
      StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs
  70. 22
      StabilityMatrix.Core/Helper/RemoteModels.cs
  71. 107
      StabilityMatrix.Core/Inference/ComfyClient.cs
  72. 12
      StabilityMatrix.Core/Models/Api/CivitBaseModelType.cs
  73. 18
      StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnectionBase.cs
  74. 26
      StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs
  75. 44
      StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs
  76. 25
      StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyTypedNodeBase.cs
  77. 33
      StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs
  78. 31
      StabilityMatrix.Core/Models/Database/LocalModelFile.cs
  79. 58
      StabilityMatrix.Core/Models/FDS/ComfyUiSelfStartSettings.cs
  80. 380
      StabilityMatrix.Core/Models/FDS/StableSwarmSettings.cs
  81. 15
      StabilityMatrix.Core/Models/FileInterfaces/DirectoryPath.cs
  82. 14
      StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs
  83. 4
      StabilityMatrix.Core/Models/FileInterfaces/FileSystemPath.cs
  84. 13
      StabilityMatrix.Core/Models/HybridModelFile.cs
  85. 5
      StabilityMatrix.Core/Models/PackagePrerequisite.cs
  86. 4
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  87. 10
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  88. 5
      StabilityMatrix.Core/Models/Packages/Extensions/ExtensionManifest.cs
  89. 78
      StabilityMatrix.Core/Models/Packages/Extensions/GitPackageExtensionManager.cs
  90. 29
      StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs
  91. 9
      StabilityMatrix.Core/Models/Packages/Extensions/VladExtensionItem.cs
  92. 2
      StabilityMatrix.Core/Models/Packages/InvokeAI.cs
  93. 158
      StabilityMatrix.Core/Models/Packages/SDWebForge.cs
  94. 47
      StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs
  95. 386
      StabilityMatrix.Core/Models/Packages/StableSwarm.cs
  96. 90
      StabilityMatrix.Core/Models/Packages/VladAutomatic.cs
  97. 10
      StabilityMatrix.Core/Models/RemoteResource.cs
  98. 2
      StabilityMatrix.Core/Models/SharedFolderType.cs
  99. 54
      StabilityMatrix.Core/Models/TrackedDownload.cs
  100. 7
      StabilityMatrix.Core/Services/DownloadService.cs
  101. Some files were not shown because too many files have changed in this diff Show More

2
Avalonia.Gif/Avalonia.Gif.csproj

@ -10,7 +10,7 @@
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.7" />
<PackageReference Include="Avalonia" Version="11.0.9" />
<PackageReference Include="SkiaSharp" Version="2.88.7" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
</ItemGroup>

67
CHANGELOG.md

@ -5,6 +5,73 @@ 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.9.0
### Added
- Added new package: [StableSwarmUI](https://github.com/Stability-AI/StableSwarmUI) by Stability AI
- Added new package: [Stable Diffusion WebUI Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) by lllyasviel
- Added extension management for SD.Next and Stable Diffusion WebUI-UX
- Added the ability to choose where CivitAI model downloads are saved
- Added `--launch-package` argument to launch a specific package on startup, using display name or package ID (i.e. `--launch-package "Stable Diffusion WebUI Forge"` or `--launch-package c0b3ecc5-9664-4be9-952d-a10b3dcaee14`)
- Added more Base Model search options to the CivitAI Model Browser
- Added Stable Cascade to the HuggingFace Model Browser
#### Inference
- Added Inference Prompt Styles, with Prompt Expansion model support (i.e. Fooocus V2)
- Added option to load a .yaml config file next to the model with the same name. Can be used with VPred and other models that require a config file.
- Added copy image support on linux and macOS for Inference outputs viewer menu
### Changed
- Updated translations for German, Spanish, French, Japanese, Portuguese, and Turkish
- (Internal) Updated to Avalonia 11.0.9
### Fixed
- Fixed StableSwarmUI not installing properly on macOS
- Fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS
- Fixed [#335](https://github.com/LykosAI/StabilityMatrix/issues/335) Update hanging indefinitely after git step for Auto1111 and SDWebUI Forge
- Fixed Inference output viewer menu "Copy" not copying image
- Fixed image viewer dialog arrow key navigation not working
- Fixed CivitAI login prompt not showing when downloading models that require CivitAI logins
- Fixed unknown model types not showing on checkpoints page (thanks Jerry!)
- Improved error handling for Inference Select Image hash calculation in case file is being written to while being read
## v2.9.0-pre.2
### Added
- Added `--launch-package` argument to launch a specific package on startup, using display name or package ID (i.e. `--launch-package "Stable Diffusion WebUI Forge"` or `--launch-package c0b3ecc5-9664-4be9-952d-a10b3dcaee14`)
- Added more Base Model search options to the CivitAI Model Browser
- Added Stable Cascade to the HuggingFace Model Browser
### Changed
- (Internal) Updated to Avalonia 11.0.9
### Fixed
- Fixed image viewer dialog arrow key navigation not working
- Fixed CivitAI login prompt not showing when downloading models that require CivitAI logins
## v2.9.0-pre.1
### Added
- Added Inference Prompt Styles, with Prompt Expansion model support (i.e. Fooocus V2)
- Added copy image support on linux and macOS for Inference outputs viewer menu
### Fixed
- Fixed StableSwarmUI not installing properly on macOS
- Hopefully actually fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS
- Fixed default command line args for SDWebUI Forge on macOS
- Fixed output paths and output sharing for SDWebUI Forge
- Maybe fixed update hanging for Auto1111 and SDWebUI Forge
- Fixed Inference output viewer menu "Copy" not copying image
## v2.9.0-dev.2
### Added
#### Inference
- Added option to load a .yaml config file next to the model with the same name. Can be used with VPred and other models that require a config file.
### Fixed
- Fixed icon sizes of Inference Addons and Steps buttons
## v2.9.0-dev.1
### Added
- Added new package: [StableSwarmUI](https://github.com/Stability-AI/StableSwarmUI) by Stability AI
- Added new package: [Stable Diffusion WebUI Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) by lllyasviel
- Added extension management for SD.Next and Stable Diffusion WebUI-UX
- Added the ability to choose where CivitAI model downloads are saved
## v2.8.4
### Fixed
- Hopefully actually fixed [#464](https://github.com/LykosAI/StabilityMatrix/issues/464) - error when installing InvokeAI on macOS
## v2.8.3
### Fixed
- Fixed user tokens read error causing failed downloads

4
StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj

@ -19,8 +19,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.7" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.7" />
<PackageReference Include="Avalonia" Version="11.0.9" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.9" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />

2
StabilityMatrix.Avalonia/App.axaml

@ -63,6 +63,7 @@
<StyleInclude Source="Controls/StarsRating.axaml"/>
<StyleInclude Source="Controls/VideoGenerationSettingsCard.axaml"/>
<StyleInclude Source="Controls/VideoOutputSettingsCard.axaml"/>
<StyleInclude Source="Controls/TreeFileExplorer.axaml"/>
<StyleInclude Source="Controls/Inference/StackCard.axaml"/>
<StyleInclude Source="Controls/Inference/StackEditableCard.axaml"/>
<StyleInclude Source="Controls/Inference/StackExpander.axaml"/>
@ -77,6 +78,7 @@
<StyleInclude Source="Controls/Inference/SharpenCard.axaml"/>
<StyleInclude Source="Controls/Inference/FreeUCard.axaml"/>
<StyleInclude Source="Controls/Inference/ControlNetCard.axaml"/>
<StyleInclude Source="Controls/Inference/PromptExpansionCard.axaml"/>
<Style Selector="DockControl">
<Setter Property="(DockProperties.ControlRecycling)" Value="{StaticResource ControlRecyclingKey}" />

11
StabilityMatrix.Avalonia/Assets/hf-packages.json

@ -58,6 +58,17 @@
"LicenseType": "Stable-Video-Diffusion-NC-Community",
"LicensePath": "LICENSE"
},
{
"ModelCategory": "BaseModel",
"ModelName": "Stable Cascade",
"RepositoryPath": "stabilityai/stable-cascade",
"Files": [
"comfyui_checkpoints/stable_cascade_stage_b.safetensors",
"comfyui_checkpoints/stable_cascade_stage_c.safetensors"
],
"LicenseType": "stable-cascade-nc-community",
"LicensePath": "LICENSE"
},
{
"ModelCategory": "ControlNet",
"ModelName": "Canny",

8
StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml

@ -9,6 +9,7 @@
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:gif="clr-namespace:Avalonia.Gif;assembly=Avalonia.Gif"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
d:DataContext="{x:Static mocks:DesignData.SampleImageSource}"
d:DesignHeight="450"
d:DesignWidth="800"
@ -35,12 +36,11 @@
<controls:AdvancedImageBox.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem
x:Name="CopyMenuItem"
CommandParameter="{Binding $parent[controls:AdvancedImageBox].Image}"
Command="{x:Static controls:AdvancedImageBoxView.FlyoutCopyCommand}"
CommandParameter="{Binding}"
HotKey="Ctrl+C"
IconSource="Copy"
IsEnabled="{OnPlatform Windows=True, Default=False}"
Text="Copy" />
Text="{x:Static lang:Resources.Action_Copy}" />
</ui:FAMenuFlyout>
</controls:AdvancedImageBox.ContextFlyout>
</controls:AdvancedImageBox>

37
StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml.cs

@ -1,10 +1,9 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Avalonia.Controls;
@ -16,28 +15,26 @@ public partial class AdvancedImageBoxView : UserControl
InitializeComponent();
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (this.FindControl<MenuFlyoutItem>("CopyMenuItem") is { } copyMenuItem)
{
copyMenuItem.Command = new AsyncRelayCommand<Bitmap?>(FlyoutCopy);
}
}
public static AsyncRelayCommand<ImageSource?> FlyoutCopyCommand { get; } = new(FlyoutCopy);
private static async Task FlyoutCopy(Bitmap? image)
private static async Task FlyoutCopy(ImageSource? imageSource)
{
if (image is null || !Compat.IsWindows)
if (imageSource is null)
return;
await Task.Run(() =>
if (Compat.IsWindows && imageSource.Bitmap is { } bitmap)
{
if (Compat.IsWindows)
// Use bitmap on Windows if available
await Task.Run(() =>
{
WindowsClipboard.SetBitmap(image);
}
});
WindowsClipboard.SetBitmap(bitmap);
});
}
else if (imageSource.LocalFile is { } imagePath)
{
// Other OS or no bitmap, use image source
var clipboard = App.Clipboard;
await clipboard.SetFileDataObjectAsync(imagePath);
}
}
}

10
StabilityMatrix.Avalonia/Controls/Inference/ImageFolderCard.axaml

@ -43,18 +43,12 @@
<Grid RowDefinitions="Auto,*">
<TextBox
x:Name="SearchBox"
Classes="search"
Margin="6,4,6,8"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Text="{Binding SearchQuery}"
Watermark="{x:Static lang:Resources.Label_SearchEllipsis}">
<TextBox.InnerRightContent>
<ui:SymbolIcon
Margin="0,0,6,0"
FontSize="16"
Symbol="Find" />
</TextBox.InnerRightContent>
</TextBox>
Watermark="{x:Static lang:Resources.Label_SearchEllipsis}"/>
<scroll:BetterScrollViewer
Grid.Row="1"

81
StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml

@ -1,14 +1,16 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="https://github.com/projektanker/icons.avalonia"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:fluent="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
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"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
x:DataType="inference:ModelCardViewModel">
<Design.PreviewWith>
<Panel Width="400" Height="200">
@ -19,44 +21,73 @@
</Panel>
</Design.PreviewWith>
<Styles.Resources>
<fluent:SymbolIconSource
x:Key="IconCube"
IsFilled="False"
Symbol="Cube" />
<fluent:SymbolIconSource
x:Key="IconQuestionCircle"
IsFilled="True"
Symbol="QuestionCircle" />
<fluent:SymbolIconSource
x:Key="IconTableCellEdit"
IsFilled="True"
Symbol="TableCellEdit" />
</Styles.Resources>
<Style Selector="controls|ModelCard">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="12">
<sg:SpacedGrid
ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="8"
RowSpacing="0"
ColumnDefinitions="Auto,*,Auto"
RowDefinitions="*,*,*,*">
RowDefinitions="*,*,*,*"
RowSpacing="0">
<!-- Model -->
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Model}"
TextAlignment="Left" />
<controls:BetterComboBox
Grid.Row="0"
Grid.Column="1"
Padding="8,6,4,6"
Theme="{StaticResource BetterComboBoxHybridModelTheme}"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Models}"
SelectedItem="{Binding SelectedModel}"/>
SelectedItem="{Binding SelectedModel}"
Theme="{StaticResource BetterComboBoxHybridModelTheme}" />
<Button
Grid.Row="0"
Grid.Column="2"
VerticalAlignment="Stretch"
Margin="0,0,0,0"
HorizontalAlignment="Stretch"
Margin="0,0,0,0">
VerticalAlignment="Stretch">
<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 IsClipSkipEnabled}" Text="{x:Static lang:Resources.Label_CLIPSkip}" />
<ui:ToggleMenuFlyoutItem
IconSource="{StaticResource IconCube}"
IsChecked="{Binding IsRefinerSelectionEnabled}"
Text="{x:Static lang:Resources.Label_Refiner}" />
<ui:ToggleMenuFlyoutItem
IconSource="{StaticResource IconCube}"
IsChecked="{Binding IsVaeSelectionEnabled}"
Text="{x:Static lang:Resources.Label_VAE}" />
<ui:ToggleMenuFlyoutItem
IconSource="{StaticResource IconTableCellEdit}"
IsChecked="{Binding IsClipSkipEnabled}"
Text="{x:Static lang:Resources.Label_CLIPSkip}" />
<ui:MenuFlyoutSeparator />
<ui:MenuFlyoutItem
Command="{Binding ConfigClickCommand}"
IconSource="{StaticResource IconQuestionCircle}"
Text="{x:Static lang:Resources.Label_Config}" />
</ui:FAMenuFlyout>
</Button.Flyout>
</Button>
@ -77,12 +108,12 @@
Grid.ColumnSpan="2"
Margin="0,8,0,0"
Padding="8,6,4,6"
IsVisible="{Binding IsRefinerSelectionEnabled}"
Theme="{StaticResource BetterComboBoxHybridModelTheme}"
HorizontalAlignment="Stretch"
IsVisible="{Binding IsRefinerSelectionEnabled}"
ItemsSource="{Binding ClientManager.Models}"
SelectedItem="{Binding SelectedRefiner}"/>
SelectedItem="{Binding SelectedRefiner}"
Theme="{StaticResource BetterComboBoxHybridModelTheme}" />
<!-- VAE -->
<TextBlock
Grid.Row="2"
@ -104,7 +135,7 @@
IsVisible="{Binding IsVaeSelectionEnabled}"
ItemsSource="{Binding ClientManager.VaeModels}"
SelectedItem="{Binding SelectedVae}" />
<!-- CLIP Skip -->
<TextBlock
Grid.Row="3"
@ -119,16 +150,16 @@
Grid.Row="3"
Grid.Column="1"
Grid.ColumnSpan="2"
IsVisible="{Binding IsClipSkipEnabled}"
Watermark="1"
HorizontalAlignment="Stretch"
Margin="0,8,0,0"
Minimum="1"
Maximum="24"
HorizontalAlignment="Stretch"
ClipValueToMinMax="True"
Increment="1"
IsVisible="{Binding IsClipSkipEnabled}"
Maximum="24"
Minimum="1"
ParsingNumberStyle="Integer"
Value="{Binding ClipSkip, Converter={x:Static converters:NullableDefaultNumericConverters.IntToDecimal}}"
ClipValueToMinMax="True"/>
Watermark="1"
Value="{Binding ClipSkip, Converter={x:Static converters:NullableDefaultNumericConverters.IntToDecimal}}" />
</sg:SpacedGrid>
</controls:Card>

50
StabilityMatrix.Avalonia/Controls/Inference/PromptCard.axaml

@ -33,7 +33,7 @@
</Style>
</controls:Card.Styles>
<Grid RowDefinitions="*,16,*">
<Grid RowDefinitions="*,16,*,16,Auto">
<!-- Prompt -->
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*">
<StackPanel
@ -137,6 +137,54 @@
</Border>
</Grid>
<GridSplitter
Grid.Row="3"
MaxWidth="45"
VerticalAlignment="Center"
BorderThickness="1"
CornerRadius="4"
Opacity="0.3" />
<controls:StackEditableCard
Margin="2,0,0,0"
DataContext="{Binding ModulesCardViewModel}"
Grid.Row="4">
</controls:StackEditableCard>
<!-- Styles and Prompt Expansions -->
<!--<Grid Grid.Row="4" RowDefinitions="Auto,*">
<StackPanel Margin="4,0,4,8" Orientation="Horizontal">
<TextBlock FontSize="14" Text="Styles" />
<icons:Icon
Margin="8,0"
FontSize="10"
Value="fa-solid fa-caret-down" />
</StackPanel>
<Border
Grid.Row="1"
Classes="theme-dark"
VerticalAlignment="Stretch"
CornerRadius="4">
<avaloniaEdit:TextEditor
x:Name="ExtraPromptEditor"
Document="{Binding NegativePromptDocument}"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace">
<i:Interaction.Behaviors>
<behaviors:TextEditorCompletionBehavior
CompletionProvider="{Binding CompletionProvider}"
IsEnabled="{Binding IsAutoCompletionEnabled}"
TokenizerProvider="{Binding TokenizerProvider}" />
<behaviors:TextEditorToolTipBehavior IsEnabled="False" TokenizerProvider="{Binding TokenizerProvider}" />
</i:Interaction.Behaviors>
</avaloniaEdit:TextEditor>
</Border>
</Grid>-->
</Grid>
</controls:Card>
</ControlTemplate>

96
StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml

@ -0,0 +1,96 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
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:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
xmlns:local="clr-namespace:StabilityMatrix.Avalonia"
x:DataType="inference:PromptExpansionCardViewModel">
<Design.PreviewWith>
<Panel Width="400" Height="200">
<StackPanel Width="300" VerticalAlignment="Center">
<controls:PromptExpansionCard />
</StackPanel>
</Panel>
</Design.PreviewWith>
<Style Selector="controls|PromptExpansionCard">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="12">
<sg:SpacedGrid
ColumnDefinitions="Auto,*"
ColumnSpacing="8"
RowDefinitions="*,*,*,*"
RowSpacing="0">
<!-- Model -->
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Model}"
TextAlignment="Left" />
<ui:FAComboBox
x:Name="PART_ModelComboBox"
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Stretch"
ItemContainerTheme="{StaticResource FAComboBoxItemHybridModelTheme}"
ItemsSource="{Binding ClientManager.PromptExpansionModels}"
SelectedItem="{Binding SelectedModel}">
<ui:FAComboBox.Resources>
<input:StandardUICommand x:Key="RemoteDownloadCommand"
Command="{Binding RemoteDownloadCommand}" />
</ui:FAComboBox.Resources>
<ui:FAComboBox.DataTemplates>
<controls:HybridModelTemplateSelector>
<DataTemplate x:Key="{x:Static models:HybridModelType.Downloadable}" DataType="models:HybridModelFile">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Foreground="{DynamicResource ThemeGreyColor}" Text="{Binding ShortDisplayName}" />
<Button
Grid.Column="1"
Margin="8,0,0,0"
Padding="0"
Classes="transparent-full">
<fluentIcons:SymbolIcon
VerticalAlignment="Center"
FontSize="18"
Foreground="{DynamicResource ThemeGreyColor}"
IsFilled="True"
Symbol="CloudArrowDown" />
</Button>
</Grid>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:HybridModelType.None}" DataType="models:HybridModelFile">
<TextBlock Text="{Binding ShortDisplayName}" />
</DataTemplate>
</controls:HybridModelTemplateSelector>
</ui:FAComboBox.DataTemplates>
</ui:FAComboBox>
<!--<controls:BetterComboBox
Grid.Row="0"
Grid.Column="1"
Padding="8,6,4,6"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Upscalers}"
SelectedItem="{Binding SelectedModel}"
Theme="{StaticResource BetterComboBoxHybridModelTheme}" />-->
</sg:SpacedGrid>
</controls:Card>
</ControlTemplate>
</Setter>
</Style>
</Styles>

52
StabilityMatrix.Avalonia/Controls/Inference/PromptExpansionCard.axaml.cs

@ -0,0 +1,52 @@
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class PromptExpansionCard : TemplatedControl
{
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var upscalerComboBox = e.NameScope.Find("PART_ModelComboBox") as FAComboBox;
upscalerComboBox!.SelectionChanged += UpscalerComboBox_OnSelectionChanged;
}
private void UpscalerComboBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 0)
return;
var item = e.AddedItems[0];
if (item is HybridModelFile { IsDownloadable: true })
{
// Reset the selection
e.Handled = true;
if (
e.RemovedItems.Count > 0
&& e.RemovedItems[0] is HybridModelFile { IsDownloadable: false } removedItem
)
{
(sender as FAComboBox)!.SelectedItem = removedItem;
}
else
{
(sender as FAComboBox)!.SelectedItem = null;
}
// Show dialog to download the model
(DataContext as PromptExpansionCardViewModel)!
.RemoteDownloadCommand.ExecuteAsync(item)
.SafeFireAndForget();
}
}
}

4
StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml

@ -39,7 +39,7 @@
Padding="12,4"
IsEnabled="{Binding !!Cards.Count}"
IsChecked="{Binding IsEditEnabled}">
<fluentIcons:SymbolIcon Symbol="Edit" />
<fluentIcons:SymbolIcon Symbol="Edit" FontSize="14" />
</ToggleButton>
<!-- Add button -->
<Button Grid.Column="3"
@ -47,7 +47,7 @@
VerticalAlignment="Bottom"
CornerRadius="4"
Padding="12,4" >
<fluentIcons:SymbolIcon Symbol="Add" IsFilled="True" />
<fluentIcons:SymbolIcon Symbol="Add" IsFilled="True" FontSize="14" />
</Button>
</sg:SpacedGrid>

65
StabilityMatrix.Avalonia/Controls/TreeFileExplorer.axaml

@ -0,0 +1,65 @@
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:StabilityMatrix.Avalonia.Controls"
xmlns:system="using:System"
xmlns:treeFileExplorer="clr-namespace:StabilityMatrix.Avalonia.Models.TreeFileExplorer"
xmlns:mock="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:sg="clr-namespace:SpacedGridControl.Avalonia;assembly=SpacedGridControl.Avalonia"
xmlns:fluentIcons="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent">
<Design.PreviewWith>
<StackPanel Spacing="16">
<Panel Height="300" Margin="4">
<controls:TreeFileExplorer
RootPath="{x:Static mock:DesignData.CurrentDirectory}" />
</Panel>
<Panel Height="300" Margin="4">
<controls:TreeFileExplorer
IndexFiles="False"
CanSelectFiles="False"
RootPath="{x:Static mock:DesignData.CurrentDirectory}" />
</Panel>
</StackPanel>
</Design.PreviewWith>
<Style Selector="controls|TreeFileExplorer">
<!-- Set Defaults -->
<Setter Property="Template">
<ControlTemplate>
<Grid>
<TreeView
ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=RootItem.Children}">
<TreeView.DataTemplates>
<DataTemplate
DataType="treeFileExplorer:TreeFileExplorerFile">
<sg:SpacedGrid ColumnDefinitions="Auto,*" RowSpacing="0" ColumnSpacing="4">
<fluentIcons:SymbolIcon
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Symbol="Document" />
<TextBlock
Grid.Column="1"
Text="{Binding Path.Name}" />
</sg:SpacedGrid>
</DataTemplate>
<TreeDataTemplate
DataType="treeFileExplorer:TreeFileExplorerDirectory"
ItemsSource="{Binding Children}">
<sg:SpacedGrid ColumnDefinitions="Auto,*" RowSpacing="0" ColumnSpacing="4">
<fluentIcons:SymbolIcon
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
IsFilled="True"
Symbol="Folder" />
<TextBlock
Grid.Column="1"
Text="{Binding Path.Name}" />
</sg:SpacedGrid>
</TreeDataTemplate>
</TreeView.DataTemplates>
</TreeView>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>

135
StabilityMatrix.Avalonia/Controls/TreeFileExplorer.axaml.cs

@ -0,0 +1,135 @@
using Avalonia;
using Avalonia.Controls.Primitives;
using StabilityMatrix.Avalonia.Models.TreeFileExplorer;
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Avalonia.Controls;
public class TreeFileExplorer : TemplatedControl
{
public static readonly StyledProperty<TreeFileExplorerDirectory?> RootItemProperty =
AvaloniaProperty.Register<TreeFileExplorer, TreeFileExplorerDirectory?>("RootItem");
public TreeFileExplorerDirectory? RootItem
{
get => GetValue(RootItemProperty);
set => SetValue(RootItemProperty, value);
}
public static readonly StyledProperty<string?> RootPathProperty = AvaloniaProperty.Register<
TreeFileExplorer,
string?
>("RootPath");
public string? RootPath
{
get => GetValue(RootPathProperty);
set => SetValue(RootPathProperty, value);
}
public static readonly StyledProperty<IPathObject?> SelectedPathProperty = AvaloniaProperty.Register<
TreeFileExplorer,
IPathObject?
>("SelectedPath");
public IPathObject? SelectedPath
{
get => GetValue(SelectedPathProperty);
set => SetValue(SelectedPathProperty, value);
}
public static readonly StyledProperty<bool> CanSelectFilesProperty = AvaloniaProperty.Register<
TreeFileExplorer,
bool
>("CanSelectFiles", true);
public bool CanSelectFiles
{
get => GetValue(CanSelectFilesProperty);
set => SetValue(CanSelectFilesProperty, value);
}
public static readonly StyledProperty<bool> CanSelectFoldersProperty = AvaloniaProperty.Register<
TreeFileExplorer,
bool
>("CanSelectFolders", true);
public bool CanSelectFolders
{
get => GetValue(CanSelectFoldersProperty);
set => SetValue(CanSelectFoldersProperty, value);
}
public static readonly StyledProperty<bool> IndexFilesProperty = AvaloniaProperty.Register<
TreeFileExplorer,
bool
>("IndexFiles", true);
public bool IndexFiles
{
get => GetValue(IndexFilesProperty);
set => SetValue(IndexFilesProperty, value);
}
public static readonly StyledProperty<bool> IndexFoldersProperty = AvaloniaProperty.Register<
TreeFileExplorer,
bool
>("IndexFolders", true);
public bool IndexFolders
{
get => GetValue(IndexFoldersProperty);
set => SetValue(IndexFoldersProperty, value);
}
private TreeFileExplorerOptions GetOptions()
{
var options = TreeFileExplorerOptions.None;
if (CanSelectFiles)
{
options |= TreeFileExplorerOptions.CanSelectFiles;
}
if (CanSelectFolders)
{
options |= TreeFileExplorerOptions.CanSelectFolders;
}
if (IndexFiles)
{
options |= TreeFileExplorerOptions.IndexFiles;
}
if (IndexFolders)
{
options |= TreeFileExplorerOptions.IndexFolders;
}
return options;
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (RootItem is null)
{
RootItem = RootPath is null
? null
: new TreeFileExplorerDirectory(new DirectoryPath(RootPath), GetOptions());
}
}
/// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == RootPathProperty)
{
var path = change.GetNewValue<string?>();
RootItem = path is null
? null
: new TreeFileExplorerDirectory(new DirectoryPath(path), GetOptions());
}
}
}

5
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -192,7 +192,8 @@ public static class DesignData
null,
null,
null,
null
null,
packageFactory
);
ObservableCacheEx.AddOrUpdate(
@ -1007,6 +1008,8 @@ The gallery images are often inpainted, but you will get something very similar
public static ControlNetCardViewModel ControlNetCardViewModel =>
DialogFactory.Get<ControlNetCardViewModel>();
public static string CurrentDirectory => Directory.GetCurrentDirectory();
public static Indexer Types { get; } = new();
public class Indexer

18
StabilityMatrix.Avalonia/DesignData/MockInferenceClientManager.cs

@ -27,6 +27,9 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl
public IObservableCollection<HybridModelFile> ControlNetModels { get; } =
new ObservableCollectionExtended<HybridModelFile>();
public IObservableCollection<HybridModelFile> PromptExpansionModels { get; } =
new ObservableCollectionExtended<HybridModelFile>();
public IObservableCollection<ComfySampler> Samplers { get; } =
new ObservableCollectionExtended<ComfySampler>(ComfySampler.Defaults);
@ -64,28 +67,19 @@ public partial class MockInferenceClientManager : ObservableObject, IInferenceCl
}
/// <inheritdoc />
public Task CopyImageToInputAsync(
FilePath imageFile,
CancellationToken cancellationToken = default
)
public Task CopyImageToInputAsync(FilePath imageFile, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
/// <inheritdoc />
public Task UploadInputImageAsync(
ImageSource image,
CancellationToken cancellationToken = default
)
public Task UploadInputImageAsync(ImageSource image, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
/// <inheritdoc />
public Task WriteImageToInputAsync(
ImageSource imageSource,
CancellationToken cancellationToken = default
)
public Task WriteImageToInputAsync(ImageSource imageSource, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}

24
StabilityMatrix.Avalonia/Helpers/IOCommands.cs

@ -16,4 +16,28 @@ public static class IOCommands
},
url => !string.IsNullOrWhiteSpace(url)
);
public static AsyncRelayCommand<string?> OpenFileBrowserCommand { get; } =
new(
async path =>
{
if (string.IsNullOrWhiteSpace(path))
return;
await ProcessRunner.OpenFileBrowser(path);
},
path => !string.IsNullOrWhiteSpace(path)
);
public static AsyncRelayCommand<string?> OpenFolderBrowserCommand { get; } =
new(
async path =>
{
if (string.IsNullOrWhiteSpace(path))
return;
await ProcessRunner.OpenFolderBrowser(path);
},
path => !string.IsNullOrWhiteSpace(path)
);
}

153
StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
@ -43,6 +45,18 @@ public class UnixPrerequisiteHelper(
private string NpmPath => Path.Combine(NodeDir, "bin", "npm");
private bool IsNodeInstalled => File.Exists(NpmPath);
private DirectoryPath DotnetDir => AssetsDir.JoinDir("dotnet");
private string DotnetPath => Path.Combine(DotnetDir, "dotnet");
private bool IsDotnetInstalled => File.Exists(DotnetPath);
private string Dotnet7DownloadUrlMacOs =>
"https://download.visualstudio.microsoft.com/download/pr/5bb0e0e4-2a8d-4aba-88ad-232e1f65c281/ee6d35f762d81965b4cf336edde1b318/dotnet-sdk-7.0.405-osx-arm64.tar.gz";
private string Dotnet8DownloadUrlMacOs =>
"https://download.visualstudio.microsoft.com/download/pr/ef083c06-7aee-4a4f-b18b-50c9a8990753/e206864e7910e81bbd9cb7e674ff1b4c/dotnet-sdk-8.0.101-osx-arm64.tar.gz";
private string Dotnet7DownloadUrlLinux =>
"https://download.visualstudio.microsoft.com/download/pr/5202b091-2406-445c-b40a-68a5b97c882b/b509f2a7a0eb61aea145b990b40b6d5b/dotnet-sdk-7.0.405-linux-x64.tar.gz";
private string Dotnet8DownloadUrlLinux =>
"https://download.visualstudio.microsoft.com/download/pr/9454f7dc-b98e-4a64-a96d-4eb08c7b6e66/da76f9c6bc4276332b587b771243ae34/dotnet-sdk-8.0.101-linux-x64.tar.gz";
// Cached store of whether or not git is installed
private bool? isGitInstalled;
@ -78,6 +92,28 @@ public class UnixPrerequisiteHelper(
{
await InstallNodeIfNecessary(progress);
}
if (prerequisites.Contains(PackagePrerequisite.Dotnet))
{
await InstallDotnetIfNecessary(progress);
}
}
public async Task InstallDotnetIfNecessary(IProgress<ProgressReport>? progress = null)
{
if (IsDotnetInstalled)
return;
if (Compat.IsMacOS)
{
await DownloadAndExtractPrerequisite(progress, Dotnet7DownloadUrlMacOs, DotnetDir);
await DownloadAndExtractPrerequisite(progress, Dotnet8DownloadUrlMacOs, DotnetDir);
}
else
{
await DownloadAndExtractPrerequisite(progress, Dotnet7DownloadUrlLinux, DotnetDir);
await DownloadAndExtractPrerequisite(progress, Dotnet8DownloadUrlLinux, DotnetDir);
}
}
private async Task InstallVirtualenvIfNecessary(IProgress<ProgressReport>? progress = null)
@ -276,6 +312,7 @@ public class UnixPrerequisiteHelper(
return ProcessRunner.RunBashCommand(args.Prepend("git").ToArray(), workingDirectory ?? "");
}
[Localizable(false)]
[SupportedOSPlatform("Linux")]
[SupportedOSPlatform("macOS")]
public async Task RunNpm(
@ -285,27 +322,73 @@ public class UnixPrerequisiteHelper(
IReadOnlyDictionary<string, string>? envVars = null
)
{
var command = args.Prepend([NpmPath]);
var process = ProcessRunner.StartAnsiProcess(
NpmPath,
args,
workingDirectory,
onProcessOutput,
envVars
);
var result = await ProcessRunner.RunBashCommand(command.ToArray(), workingDirectory ?? "", envVars);
if (result.ExitCode != 0)
{
Logger.Error(
"npm command [{Command}] failed with exit code " + "{ExitCode}:\n{StdOut}\n{StdErr}",
command,
result.ExitCode,
result.StandardOutput,
result.StandardError
);
await process.WaitForExitAsync();
throw new ProcessException(
$"npm command [{command}] failed with exit code"
+ $" {result.ExitCode}:\n{result.StandardOutput}\n{result.StandardError}"
);
}
if (process.ExitCode == 0)
return;
onProcessOutput?.Invoke(ProcessOutput.FromStdOutLine(result.StandardOutput));
onProcessOutput?.Invoke(ProcessOutput.FromStdErrLine(result.StandardError));
var stdOut = await process.StandardOutput.ReadToEndAsync();
var stdErr = await process.StandardError.ReadToEndAsync();
Logger.Error(
"RunNpm with args [{Args}] failed with exit code " + "{ExitCode}:\n{StdOut}\n{StdErr}",
args,
process.ExitCode,
stdOut,
stdErr
);
throw new ProcessException(
$"RunNpm with args [{args}] failed with exit code" + $" {process.ExitCode}:\n{stdOut}\n{stdErr}"
);
}
[SupportedOSPlatform("Linux")]
[SupportedOSPlatform("macOS")]
public async Task<Process> RunDotnet(
ProcessArgs args,
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null,
IReadOnlyDictionary<string, string>? envVars = null,
bool waitForExit = true
)
{
var process = ProcessRunner.StartAnsiProcess(
DotnetPath,
args,
workingDirectory,
onProcessOutput,
envVars
);
if (!waitForExit)
return process;
await process.WaitForExitAsync();
if (process.ExitCode == 0)
return process;
Logger.Error(
"dotnet8 with args [{Args}] failed with exit code " + "{ExitCode}:\n{StdOut}\n{StdErr}",
args,
process.ExitCode,
process.StandardOutput,
process.StandardError
);
throw new ProcessException(
$"dotnet8 with args [{args}] failed with exit code"
+ $" {process.ExitCode}:\n{process.StandardOutput}\n{process.StandardError}"
);
}
[SupportedOSPlatform("Linux")]
@ -353,6 +436,40 @@ public class UnixPrerequisiteHelper(
File.Delete(nodeDownloadPath);
}
private async Task DownloadAndExtractPrerequisite(
IProgress<ProgressReport>? progress,
string downloadUrl,
string extractPath
)
{
Logger.Info($"Downloading {downloadUrl}");
var downloadPath = AssetsDir.JoinFile(Path.GetFileName(downloadUrl));
await downloadService.DownloadToFileAsync(downloadUrl, downloadPath, progress: progress);
Logger.Info("Installing prereq");
progress?.Report(
new ProgressReport(
progress: 0.5f,
isIndeterminate: true,
type: ProgressType.Generic,
message: "Installing prerequisites..."
)
);
Directory.CreateDirectory(extractPath);
// unzip
await ArchiveHelper.Extract7ZAuto(downloadPath, extractPath);
progress?.Report(
new ProgressReport(progress: 1f, message: "Node install complete", type: ProgressType.Generic)
);
File.Delete(downloadPath);
}
[UnsupportedOSPlatform("Linux")]
[UnsupportedOSPlatform("macOS")]
public Task InstallTkinterIfNecessary(IProgress<ProgressReport>? progress = null)

114
StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
@ -7,7 +8,6 @@ using System.Threading.Tasks;
using Microsoft.Win32;
using NLog;
using Octokit;
using PropertyModels.Extensions;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
@ -35,6 +35,12 @@ public class WindowsPrerequisiteHelper(
"https://cdn.lykos.ai/tkinter-cpython-embedded-3.10.11-win-x64.zip";
private const string NodeDownloadUrl = "https://nodejs.org/dist/v20.11.0/node-v20.11.0-win-x64.zip";
private const string Dotnet7DownloadUrl =
"https://download.visualstudio.microsoft.com/download/pr/2133b143-9c4f-4daa-99b0-34fa6035d67b/193ede446d922eb833f1bfe0239be3fc/dotnet-sdk-7.0.405-win-x64.zip";
private const string Dotnet8DownloadUrl =
"https://download.visualstudio.microsoft.com/download/pr/6902745c-34bd-4d66-8e84-d5b61a17dfb7/e61732b00f7e144e162d7e6914291f16/dotnet-sdk-8.0.101-win-x64.zip";
private string HomeDir => settingsManager.LibraryDir;
private string VcRedistDownloadPath => Path.Combine(HomeDir, "vcredist.x64.exe");
@ -59,6 +65,10 @@ public class WindowsPrerequisiteHelper(
private string TkinterExistsPath => Path.Combine(PythonDir, "tkinter");
private string NodeExistsPath => Path.Combine(AssetsDir, "nodejs", "npm.cmd");
private string NodeDownloadPath => Path.Combine(AssetsDir, "nodejs.zip");
private string Dotnet7DownloadPath => Path.Combine(AssetsDir, "dotnet-sdk-7.0.405-win-x64.zip");
private string Dotnet8DownloadPath => Path.Combine(AssetsDir, "dotnet-sdk-8.0.101-win-x64.zip");
private string DotnetExtractPath => Path.Combine(AssetsDir, "dotnet");
private string DotnetExistsPath => Path.Combine(DotnetExtractPath, "dotnet.exe");
public string GitBinPath => Path.Combine(PortableGitInstallDir, "bin");
public bool IsPythonInstalled => File.Exists(PythonDllPath);
@ -155,6 +165,11 @@ public class WindowsPrerequisiteHelper(
await InstallNodeIfNecessary(progress);
}
if (prerequisites.Contains(PackagePrerequisite.Dotnet))
{
await InstallDotnetIfNecessary(progress);
}
if (prerequisites.Contains(PackagePrerequisite.Tkinter))
{
await InstallTkinterIfNecessary(progress);
@ -425,15 +440,86 @@ public class WindowsPrerequisiteHelper(
public async Task InstallNodeIfNecessary(IProgress<ProgressReport>? progress = null)
{
if (File.Exists(NodeExistsPath))
{
Logger.Info("node already installed");
return;
await DownloadAndExtractPrerequisite(progress, NodeDownloadUrl, NodeDownloadPath, AssetsDir);
var extractedNodeDir = Path.Combine(AssetsDir, "node-v20.11.0-win-x64");
if (Directory.Exists(extractedNodeDir))
{
Directory.Move(extractedNodeDir, Path.Combine(AssetsDir, "nodejs"));
}
}
[SupportedOSPlatform("windows")]
public async Task InstallDotnetIfNecessary(IProgress<ProgressReport>? progress = null)
{
if (File.Exists(DotnetExistsPath))
return;
await DownloadAndExtractPrerequisite(
progress,
Dotnet7DownloadUrl,
Dotnet7DownloadPath,
DotnetExtractPath
);
await DownloadAndExtractPrerequisite(
progress,
Dotnet8DownloadUrl,
Dotnet8DownloadPath,
DotnetExtractPath
);
}
public async Task<Process> RunDotnet(
ProcessArgs args,
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null,
IReadOnlyDictionary<string, string>? envVars = null,
bool waitForExit = true
)
{
var process = ProcessRunner.StartAnsiProcess(
DotnetExistsPath,
args,
workingDirectory,
onProcessOutput,
envVars
);
if (!waitForExit)
return process;
Logger.Info("Downloading node");
await downloadService.DownloadToFileAsync(NodeDownloadUrl, NodeDownloadPath, progress: progress);
await process.WaitForExitAsync();
if (process.ExitCode == 0)
return process;
Logger.Error(
"dotnet8 with args [{Args}] failed with exit code " + "{ExitCode}:\n{StdOut}\n{StdErr}",
args,
process.ExitCode,
process.StandardOutput,
process.StandardError
);
Logger.Info("Installing node");
throw new ProcessException(
$"dotnet8 with args [{args}] failed with exit code"
+ $" {process.ExitCode}:\n{process.StandardOutput}\n{process.StandardError}"
);
}
private async Task DownloadAndExtractPrerequisite(
IProgress<ProgressReport>? progress,
string downloadUrl,
string downloadPath,
string extractPath
)
{
Logger.Info($"Downloading {downloadUrl} to {downloadPath}");
await downloadService.DownloadToFileAsync(downloadUrl, downloadPath, progress: progress);
Logger.Info("Extracting prerequisite");
progress?.Report(
new ProgressReport(
progress: 0.5f,
@ -443,18 +529,20 @@ public class WindowsPrerequisiteHelper(
)
);
// unzip
await ArchiveHelper.Extract(NodeDownloadPath, AssetsDir, progress);
Directory.CreateDirectory(extractPath);
// move to assets dir
var existingNodeDir = Path.Combine(AssetsDir, "node-v20.11.0-win-x64");
Directory.Move(existingNodeDir, Path.Combine(AssetsDir, "nodejs"));
// unzip
await ArchiveHelper.Extract(downloadPath, extractPath, progress);
progress?.Report(
new ProgressReport(progress: 1f, message: "Node install complete", type: ProgressType.Generic)
new ProgressReport(
progress: 1f,
message: "Prerequisite install complete",
type: ProgressType.Generic
)
);
File.Delete(NodeDownloadPath);
File.Delete(downloadPath);
}
private async Task UnzipGit(IProgress<ProgressReport>? progress = null)

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

@ -644,6 +644,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Please select a download location..
/// </summary>
public static string Error_PleaseSelectDownloadLocation {
get {
return ResourceManager.GetString("Error_PleaseSelectDownloadLocation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to About.
/// </summary>
@ -887,6 +896,24 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to ComfyUI is required to install this package. Would you like to install it now?.
/// </summary>
public static string Label_ComfyRequiredDetail {
get {
return ResourceManager.GetString("Label_ComfyRequiredDetail", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to ComfyUI Required.
/// </summary>
public static string Label_ComfyRequiredTitle {
get {
return ResourceManager.GetString("Label_ComfyRequiredTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Comments.
/// </summary>
@ -914,6 +941,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Config.
/// </summary>
public static string Label_Config {
get {
return ResourceManager.GetString("Label_Config", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Confirm Delete.
/// </summary>
@ -1653,7 +1689,7 @@ namespace StabilityMatrix.Avalonia.Languages {
}
/// <summary>
/// Looks up a localized string similar to .
/// Looks up a localized string similar to None.
/// </summary>
public static string Label_NotificationOption_None {
get {
@ -2021,6 +2057,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Select Download Location:.
/// </summary>
public static string Label_SelectDownloadLocation {
get {
return ResourceManager.GetString("Label_SelectDownloadLocation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select new Data Directory.
/// </summary>

4
StabilityMatrix.Avalonia/Languages/Resources.de.resx

@ -521,6 +521,7 @@
</data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>Wählen Sie Ihre bevorzugte Schnittstelle und klicken Sie auf Installieren, um loszulegen.</value>
<comment>Fuzzy</comment>
</data>
<data name="Label_Installing" xml:space="preserve">
<value>Installiert</value>
@ -777,6 +778,9 @@
<data name="Label_ApiKey" xml:space="preserve">
<value>API Schlüssel</value>
</data>
<data name="Label_Accounts" xml:space="preserve">
<value>Accounts</value>
</data>
<data name="Label_Preprocessor" xml:space="preserve">
<value>Preprocessor</value>
</data>

181
StabilityMatrix.Avalonia/Languages/Resources.es.resx

@ -522,6 +522,7 @@
</data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>Elige tu interfaz preferida y pulsa Instalar para comenzar</value>
<comment>Fuzzy</comment>
</data>
<data name="Label_Installing" xml:space="preserve">
<value>Instalando</value>
@ -782,4 +783,184 @@
<data name="Label_Accounts" xml:space="preserve">
<value>Cuentas</value>
</data>
<data name="Label_Preprocessor" xml:space="preserve">
<value>Preprocesador</value>
</data>
<data name="Label_Strength" xml:space="preserve">
<value>Fortaleza</value>
</data>
<data name="Label_ControlWeight" xml:space="preserve">
<value>Control de Peso</value>
</data>
<data name="Label_ControlSteps" xml:space="preserve">
<value>Control de Pasos</value>
</data>
<data name="Label_CivitAiLoginRequired" xml:space="preserve">
<value>Debes iniciar sesión para descargar este checkpoint. Ingresa una clave API de CivitAI en la configuración.</value>
</data>
<data name="Label_DownloadFailed" xml:space="preserve">
<value>Descarga Fallida</value>
</data>
<data name="Label_AutoUpdates" xml:space="preserve">
<value>Actualizaciones automáticas</value>
</data>
<data name="Label_UpdatesPreviewChannelDescription" xml:space="preserve">
<value>Para los usuarios primerizos. Las versiones preliminares serán más fiables que las del canal de desarrollo y estarán disponibles antes de las versiones estables. Tus comentarios nos ayudarán enormemente a descubrir problemas y pulir elementos de diseño.</value>
</data>
<data name="Label_UpdatesDevChannelDescription" xml:space="preserve">
<value>Para usuarios técnicos. Sé el primero en acceder a nuestras compilaciones de desarrollo desde las ramas de funciones tan pronto como estén disponibles. Es posible que haya algunas asperezas y errores a medida que experimentamos con nuevas funciones.</value>
</data>
<data name="Label_Updates" xml:space="preserve">
<value>Actualizaciones</value>
</data>
<data name="Label_YouAreUpToDate" xml:space="preserve">
<value>Estás al día</value>
</data>
<data name="TextTemplate_LastChecked" xml:space="preserve">
<value>Última comprobación: {0}</value>
</data>
<data name="Action_CopyTriggerWords" xml:space="preserve">
<value>Copiar palabras desencadenantes</value>
</data>
<data name="Label_TriggerWords" xml:space="preserve">
<value>Palabras desencadenantes:</value>
</data>
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>Aquí se pueden habilitar carpetas adicionales como IPAdapters y TextualInversions (embeddings)</value>
</data>
<data name="Action_OpenOnHuggingFace" xml:space="preserve">
<value>Abrir en Hugging Face</value>
</data>
<data name="Action_UpdateExistingMetadata" xml:space="preserve">
<value>Actualizar metadatos existentes</value>
</data>
<data name="Label_General" xml:space="preserve">
<value>General</value>
<comment>A general settings category</comment>
</data>
<data name="Label_Inference" xml:space="preserve">
<value>Inferencia</value>
<comment>The Inference feature page</comment>
</data>
<data name="Label_Prompt" xml:space="preserve">
<value>Prompt</value>
<comment>A settings category for Inference generation prompts</comment>
</data>
<data name="Label_OutputImageFiles" xml:space="preserve">
<value>Archivos de imágenes resultantes</value>
</data>
<data name="Label_ImageViewer" xml:space="preserve">
<value>Visor de Imágenes</value>
</data>
<data name="Label_AutoCompletion" xml:space="preserve">
<value>Auto Completado</value>
</data>
<data name="Label_CompletionReplaceUnderscoresWithSpaces" xml:space="preserve">
<value>Reemplazar guiones bajos con espacios al insertar terminaciones</value>
</data>
<data name="Label_PromptTags" xml:space="preserve">
<value>Etiquetas de Prompt</value>
<comment>Tags for image generation prompts</comment>
</data>
<data name="Label_PromptTagsImport" xml:space="preserve">
<value>Importar Etiquetas de Prompt</value>
</data>
<data name="Label_PromptTagsDescription" xml:space="preserve">
<value>Archivo de etiquetas que se utilizarán para sugerir terminaciones (admite el formato .csv a1111-sd-webui-tagcomplete)</value>
</data>
<data name="Label_SystemInformation" xml:space="preserve">
<value>Información del Sistema</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
<data name="Label_HuggingFace" xml:space="preserve">
<value>Hugging Face</value>
</data>
<data name="Label_Addons" xml:space="preserve">
<value>Complementos</value>
<comment>Inference Sampler Addons</comment>
</data>
<data name="Label_SaveIntermediateImage" xml:space="preserve">
<value>Guardar Imagen Intermedia</value>
<comment>Inference module step to save an intermediate image</comment>
</data>
<data name="Label_Settings" xml:space="preserve">
<value>Ajustes</value>
</data>
<data name="Action_SelectFile" xml:space="preserve">
<value>Seleccionar Archivo</value>
</data>
<data name="Action_ReplaceContents" xml:space="preserve">
<value>Reemplazar contenido</value>
</data>
<data name="Label_WipFeature" xml:space="preserve">
<value>No disponible aún</value>
</data>
<data name="Label_WipFeatureDescription" xml:space="preserve">
<value>La función estará disponible en una actualización futura</value>
</data>
<data name="Label_MissingImageFile" xml:space="preserve">
<value>Archivo de Imagen Faltante</value>
</data>
<data name="Label_HolidayMode" xml:space="preserve">
<value>Modo Vacaciones</value>
</data>
<data name="Label_CLIPSkip" xml:space="preserve">
<value>Saltar CLIP</value>
</data>
<data name="Label_ImageToVideo" xml:space="preserve">
<value>Imagen a Vídeo</value>
</data>
<data name="Label_Fps" xml:space="preserve">
<value>Frames por Segundo</value>
</data>
<data name="Label_MinCfg" xml:space="preserve">
<value>CFG Mínimo</value>
</data>
<data name="Label_Lossless" xml:space="preserve">
<value>Sin pérdidas</value>
</data>
<data name="Label_Frames" xml:space="preserve">
<value>Frames</value>
</data>
<data name="Label_MotionBucketId" xml:space="preserve">
<value>Motion Bucket ID</value>
</data>
<data name="Label_AugmentationLevel" xml:space="preserve">
<value>Nivel de aumento</value>
</data>
<data name="Label_VideoOutputMethod" xml:space="preserve">
<value>Método</value>
</data>
<data name="Label_VideoQuality" xml:space="preserve">
<value>Calidad</value>
</data>
<data name="Label_FindInModelBrowser" xml:space="preserve">
<value>Buscar en el navegador de modelos</value>
</data>
<data name="Label_Installed" xml:space="preserve">
<value>Instalado</value>
</data>
<data name="Label_NoExtensionsFound" xml:space="preserve">
<value>No se encuentran extensiones.</value>
</data>
<data name="Action_Hide" xml:space="preserve">
<value>Ocultar</value>
</data>
<data name="Action_CopyDetails" xml:space="preserve">
<value>Copiar detalles</value>
</data>
<data name="Action_Download" xml:space="preserve">
<value>Descargar</value>
</data>
<data name="TeachingTip_DownloadsExplanation" xml:space="preserve">
<value>Verifica el progreso de instalación de tus paquetes y descargas de modelos aquí.</value>
</data>
<data name="Label_RecommendedModels" xml:space="preserve">
<value>Modelos recomendados</value>
</data>
<data name="Label_RecommendedModelsSubText" xml:space="preserve">
<value>Mientras se instala tu paquete, aquí hay algunos modelos que recomendamos para ayudarte a comenzar.</value>
</data>
</root>

38
StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx

@ -319,7 +319,7 @@
<value>Recherche</value>
</data>
<data name="Label_Sort" xml:space="preserve">
<value>Trier</value>
<value>Tri</value>
</data>
<data name="Label_TimePeriod" xml:space="preserve">
<value>Période</value>
@ -810,6 +810,9 @@
<data name="TextTemplate_LastChecked" xml:space="preserve">
<value>Dernière vérification: {0}</value>
</data>
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>Les dossiers additionnels comme IPAdapters ou TextualInversions (embeddings) peuvent être activés ici</value>
</data>
<data name="Action_OpenOnHuggingFace" xml:space="preserve">
<value>Ouvrir sur Hugging Face</value>
</data>
@ -834,6 +837,9 @@
<data name="Label_AutoCompletion" xml:space="preserve">
<value>Auto-complétion</value>
</data>
<data name="Label_CompletionReplaceUnderscoresWithSpaces" xml:space="preserve">
<value>Remplacer les underscores par des espaces lors de l&apos;auto-complétion.</value>
</data>
<data name="Label_SystemInformation" xml:space="preserve">
<value>Informations système</value>
</data>
@ -888,10 +894,40 @@
<data name="Label_MotionBucketId" xml:space="preserve">
<value>Motion Bucket ID</value>
</data>
<data name="Label_AugmentationLevel" xml:space="preserve">
<value>Niveau d&apos;augmentation</value>
</data>
<data name="Label_VideoOutputMethod" xml:space="preserve">
<value>Méthode</value>
</data>
<data name="Label_VideoQuality" xml:space="preserve">
<value>Qualité</value>
</data>
<data name="Label_FindInModelBrowser" xml:space="preserve">
<value>Trouver dans l&apos;explorateur de modèle</value>
</data>
<data name="Label_Installed" xml:space="preserve">
<value>Installé</value>
</data>
<data name="Label_NoExtensionsFound" xml:space="preserve">
<value>Pas d&apos;extensions trouvés.</value>
</data>
<data name="Action_Hide" xml:space="preserve">
<value>Masquer</value>
</data>
<data name="Action_CopyDetails" xml:space="preserve">
<value>Copier les détails</value>
</data>
<data name="Action_Download" xml:space="preserve">
<value>Télécharger</value>
</data>
<data name="TeachingTip_DownloadsExplanation" xml:space="preserve">
<value>Vérifiez la progression de l&apos;installation de votre paquet ainsi que le téléchargement de votre modèle ici.</value>
</data>
<data name="Label_RecommendedModels" xml:space="preserve">
<value>Modèles recommandés</value>
</data>
<data name="Label_RecommendedModelsSubText" xml:space="preserve">
<value>Pendant que votre paquet s&apos;installe, voici quelques modèles que nous recommandons pour vous aidez à démarrer.</value>
</data>
</root>

31
StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx

@ -530,6 +530,7 @@
</data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>好きなUIをインストールして始めよう</value>
<comment>Fuzzy</comment>
</data>
<data name="Label_Installing" xml:space="preserve">
<value>インストール中</value>
@ -706,6 +707,24 @@
<data name="Action_Refresh" xml:space="preserve">
<value>更新</value>
</data>
<data name="Action_OpenGithub" xml:space="preserve">
<value>GitHubで開く</value>
</data>
<data name="Label_Username" xml:space="preserve">
<value>ユーザ名</value>
</data>
<data name="Label_Password" xml:space="preserve">
<value>パスワード</value>
</data>
<data name="Action_Login" xml:space="preserve">
<value>ログイン</value>
</data>
<data name="Action_Signup" xml:space="preserve">
<value>サインアップ</value>
</data>
<data name="Label_ConfirmPassword" xml:space="preserve">
<value>パスワード再確認</value>
</data>
<data name="Label_CivitAiLoginRequired" xml:space="preserve">
<value>ダウンロードにはCivitAIのログインが必要です。SettingからAPIキーを入力してください。</value>
</data>
@ -715,6 +734,9 @@
<data name="Label_UpdatesDevChannelDescription" xml:space="preserve">
<value>Devビルドはテクニカルなユーザ向けです。新機能をいち早く利用できます。荒削りな部分やバグがあるかもしれません。</value>
</data>
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>IPAdaptersやTextualInversions(embeddings)などのフォルダは、ここで有効にできます。</value>
</data>
<data name="Action_OpenOnHuggingFace" xml:space="preserve">
<value>Hugging Faceで開く</value>
</data>
@ -734,4 +756,13 @@
<data name="Label_Lossless" xml:space="preserve">
<value>非圧縮</value>
</data>
<data name="TeachingTip_DownloadsExplanation" xml:space="preserve">
<value>Packageのインストール・Modelのダウンロードの進捗状況はこちらで確認できます。</value>
</data>
<data name="Label_RecommendedModels" xml:space="preserve">
<value>推奨Model</value>
</data>
<data name="Label_RecommendedModelsSubText" xml:space="preserve">
<value>Packageのインストール中に、推奨Modelをいくつかご紹介しましょう。</value>
</data>
</root>

24
StabilityMatrix.Avalonia/Languages/Resources.pt-PT.resx

@ -196,7 +196,7 @@
<value>Passos - Refinamento</value>
</data>
<data name="Label_CFGScale" xml:space="preserve">
<value>Escale de CFG</value>
<value>Escala de CFG</value>
</data>
<data name="Label_DenoisingStrength" xml:space="preserve">
<value>Nível de Denoising</value>
@ -391,7 +391,7 @@
<value>Pasta de Modelos</value>
</data>
<data name="Label_Categories" xml:space="preserve">
<value>Catagorias</value>
<value>Categorias</value>
</data>
<data name="Label_LetsGetStarted" xml:space="preserve">
<value>Vamos começar!</value>
@ -409,7 +409,7 @@
<value>Exibir imagens dos Modelos</value>
</data>
<data name="Label_Appearance" xml:space="preserve">
<value>Aparencia</value>
<value>Aparência</value>
</data>
<data name="Label_Theme" xml:space="preserve">
<value>Tema</value>
@ -521,6 +521,7 @@
</data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>Escolha sua interface preferida e clique em Instalar para começar</value>
<comment>Fuzzy</comment>
</data>
<data name="Label_Installing" xml:space="preserve">
<value>Instalando</value>
@ -840,7 +841,7 @@
<comment>The Inference feature page</comment>
</data>
<data name="Label_Prompt" xml:space="preserve">
<value>Incitar</value>
<value>Prompt</value>
<comment>A settings category for Inference generation prompts</comment>
</data>
<data name="Label_OutputImageFiles" xml:space="preserve">
@ -853,17 +854,17 @@
<value>Preenchimento automático</value>
</data>
<data name="Label_CompletionReplaceUnderscoresWithSpaces" xml:space="preserve">
<value>Substitua sublinhados por espaços ao inserir conclusões</value>
<value>Substitua caracteres sublinhados por espaços ao inserir conclusões</value>
</data>
<data name="Label_PromptTags" xml:space="preserve">
<value>Etiquetas de prompt</value>
<value>Etiquetas de Prompt</value>
<comment>Tags for image generation prompts</comment>
</data>
<data name="Label_PromptTagsImport" xml:space="preserve">
<value>Tags de prompt de importação</value>
<value>Importar Tags de Prompt</value>
</data>
<data name="Label_PromptTagsDescription" xml:space="preserve">
<value>Arquivo de tags a ser usado para sugerir conclusões (suporta o formato .csv a1111-sd-webui-tagcomplete)</value>
<value>Arquivo de tags a ser usado para Sugestão de Completions (suporta o formato .csv a1111-sd-webui-tagcomplete)</value>
</data>
<data name="Label_SystemInformation" xml:space="preserve">
<value>Informação do sistema</value>
@ -872,7 +873,7 @@
<value>CivitAI</value>
</data>
<data name="Label_HuggingFace" xml:space="preserve">
<value>Abraçando o rosto</value>
<value>Hugging Face</value>
</data>
<data name="Label_Addons" xml:space="preserve">
<value>Complementos</value>
@ -901,7 +902,7 @@
<value>Arquivo de imagem não encontrado</value>
</data>
<data name="Label_HolidayMode" xml:space="preserve">
<value>Modo Férias</value>
<value>Modo Feriado</value>
</data>
<data name="Label_CLIPSkip" xml:space="preserve">
<value>Pular CLIPE</value>
@ -930,4 +931,7 @@
<data name="Label_VideoOutputMethod" xml:space="preserve">
<value>Método</value>
</data>
<data name="Label_VideoQuality" xml:space="preserve">
<value>Qualidade</value>
</data>
</root>

17
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -946,7 +946,7 @@
<value>Notifications</value>
</data>
<data name="Label_NotificationOption_None" xml:space="preserve">
<value />
<value>None</value>
</data>
<data name="Action_Download" xml:space="preserve">
<value>Download</value>
@ -960,4 +960,19 @@
<data name="Label_RecommendedModelsSubText" xml:space="preserve">
<value>While your package is installing, here are some models we recommend to help you get started.</value>
</data>
<data name="Label_ComfyRequiredTitle" xml:space="preserve">
<value>ComfyUI Required</value>
</data>
<data name="Label_ComfyRequiredDetail" xml:space="preserve">
<value>ComfyUI is required to install this package. Would you like to install it now?</value>
</data>
<data name="Error_PleaseSelectDownloadLocation" xml:space="preserve">
<value>Please select a download location.</value>
</data>
<data name="Label_SelectDownloadLocation" xml:space="preserve">
<value>Select Download Location:</value>
</data>
<data name="Label_Config" xml:space="preserve">
<value>Config</value>
</data>
</root>

58
StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx

@ -133,7 +133,7 @@
<value>Dil</value>
</data>
<data name="Text_RelaunchRequiredToApplyLanguage" xml:space="preserve">
<value>Yeni dil seçeneğinin etkili olması için yeniden başlatma gerekiyor</value>
<value>Yeni dil seçeneğinin etkili olması için yeniden başlatma gerekir.</value>
</data>
<data name="Action_Relaunch" xml:space="preserve">
<value>Yeniden başlat</value>
@ -166,7 +166,7 @@
<value>Dallar</value>
</data>
<data name="Label_DragAndDropCheckpointsHereToImport" xml:space="preserve">
<value>İçe aktarmak için chekpoints&apos;leri buraya sürükleyip bırakın</value>
<value>İçe aktarmak için chekpoints&apos;leri buraya sürükleyip bırakın.</value>
</data>
<data name="Label_Emphasis" xml:space="preserve">
<value>Vurgu</value>
@ -235,7 +235,7 @@
<value>Sponsor Ol</value>
</data>
<data name="Label_JoinDiscord" xml:space="preserve">
<value>Discord Sunucusuna Katıl</value>
<value>Discord Sunucumuza Katıl</value>
</data>
<data name="Label_Downloads" xml:space="preserve">
<value>İndirmeler</value>
@ -313,7 +313,7 @@
<value>Tüm Sürümler</value>
</data>
<data name="Label_ModelSearchWatermark" xml:space="preserve">
<value>Modelleri, #etiketleri veya @kullanıcıları ara</value>
<value>Modelleri,Tagları (#tag) veya Kullanıcıları (@user) Buradan Arayabilirsin</value>
</data>
<data name="Action_Search" xml:space="preserve">
<value>Ara</value>
@ -322,13 +322,13 @@
<value>Sırala</value>
</data>
<data name="Label_TimePeriod" xml:space="preserve">
<value>Süre</value>
<value>Sıralama Tarihi</value>
</data>
<data name="Label_ModelType" xml:space="preserve">
<value>Model Türü</value>
</data>
<data name="Label_BaseModel" xml:space="preserve">
<value>Temel Model</value>
<value>Model Altyapısı</value>
</data>
<data name="Label_ShowNsfwContent" xml:space="preserve">
<value>NSFW İçerik Göster</value>
@ -376,10 +376,10 @@
<value>Klasör</value>
</data>
<data name="Label_DropFileToImport" xml:space="preserve">
<value>İçe aktarma için dosyayı buraya bırakın</value>
<value>Dosyayı içeri aktarmak için lütfen sürükleyip işaretlenen alana bırakın.</value>
</data>
<data name="Label_ImportAsConnected" xml:space="preserve">
<value>Metadata ile içeri aktar</value>
<value>Dosyaları metadata ile içeri aktar</value>
</data>
<data name="Label_ImportAsConnectedExplanation" xml:space="preserve">
<value>Yeni yerel içe aktarmalar için bağlı meta veri arayın</value>
@ -448,7 +448,7 @@
<value>Entegrasyonlar</value>
</data>
<data name="Label_DiscordRichPresence" xml:space="preserve">
<value>Discord Zengin Varlık</value>
<value>Discord Etkinliğini StabilityMatrix Olarak Göster.</value>
</data>
<data name="Label_System" xml:space="preserve">
<value>Sistem</value>
@ -469,10 +469,10 @@
<value>Tüm Kullanıcılar İçin Ekle</value>
</data>
<data name="Label_SelectNewDataDirectory" xml:space="preserve">
<value>Yeni Veri Dizini Seç</value>
<value>Yeni Dosya Dizinini Seç</value>
</data>
<data name="Label_SelectNewDataDirectory_Details" xml:space="preserve">
<value>Mevcut veriyi taşımaz</value>
<value>Bu işlem mevcut veriyi yeni dizine taşımaz.</value>
</data>
<data name="Action_SelectDirectory" xml:space="preserve">
<value>Dizin Seçin</value>
@ -490,7 +490,7 @@
<value>Başlamak için Başlat&apos;a tıklayın!</value>
</data>
<data name="Action_Stop" xml:space="preserve">
<value>Dur</value>
<value>Durdur</value>
</data>
<data name="Action_SendInput" xml:space="preserve">
<value>Giriş Gönder</value>
@ -521,6 +521,7 @@
</data>
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve">
<value>Tercih ettiğiniz arayüzü seçin ve başlamak için Yükle&apos;ye tıklayın</value>
<comment>Fuzzy</comment>
</data>
<data name="Label_Installing" xml:space="preserve">
<value>Yükleniyor</value>
@ -694,10 +695,10 @@
<value>{0} resim seçildi</value>
</data>
<data name="Label_OutputFolder" xml:space="preserve">
<value>Çıkış Klasörü</value>
<value>Çıktı Klasörü</value>
</data>
<data name="Label_OutputType" xml:space="preserve">
<value>Çıkış Türü</value>
<value>Çıktı Türü</value>
</data>
<data name="Action_ClearSelection" xml:space="preserve">
<value>Seçimi Temizle</value>
@ -721,7 +722,7 @@
<value>Upscale</value>
</data>
<data name="Label_OutputsPageTitle" xml:space="preserve">
<value>Çıkış Tarayıcısı</value>
<value>Medya Kitaplığı</value>
</data>
<data name="Label_OneImageSelected" xml:space="preserve">
<value>1 resim seçildi</value>
@ -933,4 +934,31 @@
<data name="Label_VideoQuality" xml:space="preserve">
<value>Kalite</value>
</data>
<data name="Label_FindInModelBrowser" xml:space="preserve">
<value>Model Tarayıcıda Bul</value>
</data>
<data name="Label_Installed" xml:space="preserve">
<value>Kurulmuş</value>
</data>
<data name="Label_NoExtensionsFound" xml:space="preserve">
<value>Uzantı bulunamadı.</value>
</data>
<data name="Action_Hide" xml:space="preserve">
<value>Gizle</value>
</data>
<data name="Action_CopyDetails" xml:space="preserve">
<value>Ayrıntıları Kopyala</value>
</data>
<data name="Action_Download" xml:space="preserve">
<value>İndir</value>
</data>
<data name="TeachingTip_DownloadsExplanation" xml:space="preserve">
<value>Paket kurulumlarınızın ve model indirmelerinizin ilerlemesini buradan kontrol edin.</value>
</data>
<data name="Label_RecommendedModels" xml:space="preserve">
<value>Önerilen Modeller</value>
</data>
<data name="Label_RecommendedModelsSubText" xml:space="preserve">
<value>Paketiniz yüklenirken başlamanıza yardımcı olması için önerdiğimiz bazı modeller aşağıda verilmiştir.</value>
</data>
</root>

7
StabilityMatrix.Avalonia/Models/AppArgs.cs

@ -77,6 +77,13 @@ public class AppArgs
[Option("data-dir", HelpText = "Override data directory")]
public string? DataDirectoryOverride { get; set; }
/// <summary>
/// Launch an installed package on startup
/// Can use package ID or name
/// </summary>
[Option("launch-package", HelpText = "Package ID or name to launch on startup")]
public string? LaunchPackageName { get; set; }
/// <summary>
/// Custom Uri protocol handler
/// This will send the Uri to the running instance of the app via IPC and exit

42
StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs

@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using System.IO;
using System.IO.Hashing;
using System.Text;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
namespace StabilityMatrix.Avalonia.Models.Inference;
@ -17,19 +20,40 @@ public class ModuleApplyStepEventArgs : EventArgs
public ModuleApplyStepTemporaryArgs Temp { get; } = new();
/// <summary>
/// Index of the step in the pipeline.
/// Generation overrides (like hires fix generate, current seed generate, etc.)
/// </summary>
public int StepIndex { get; init; }
public IReadOnlyDictionary<Type, bool> IsEnabledOverrides { get; init; } = new Dictionary<Type, bool>();
/// <summary>
/// Index
/// </summary>
public int StepTypeIndex { get; init; }
public List<(string SourcePath, string DestinationRelativePath)> FilesToTransfer { get; init; } = [];
public void AddFileTransfer(string sourcePath, string destinationRelativePath)
{
FilesToTransfer.Add((sourcePath, destinationRelativePath));
}
/// <summary>
/// Generation overrides (like hires fix generate, current seed generate, etc.)
/// Adds a file transfer to `models/configs`
/// </summary>
public IReadOnlyDictionary<Type, bool> IsEnabledOverrides { get; init; } = new Dictionary<Type, bool>();
/// <returns>The destination relative path</returns>
public string AddFileTransferToConfigs(string sourcePath)
{
// To avoid conflicts, we'll add the file name's crc32 before the extension
var sourceNameWithoutExtension = Path.GetFileNameWithoutExtension(sourcePath);
var sourceExtension = Path.GetExtension(sourcePath);
var sourceNameCrc = Crc32.Hash(Encoding.UTF8.GetBytes(sourceNameWithoutExtension));
var sourceNameCrcShort = BitConverter
.ToString(sourceNameCrc)
.ToLowerInvariant()
.Replace("-", string.Empty)[..4];
var destFileName = $"{sourceNameWithoutExtension}_{sourceNameCrcShort}{sourceExtension}";
var destPath = Path.Combine("models", "configs", destFileName);
FilesToTransfer.Add((sourcePath, destPath));
return destPath;
}
public class ModuleApplyStepTemporaryArgs
{

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

@ -1,10 +1,10 @@
using System.Text.Json.Serialization;
using System.Text.Json.Nodes;
namespace StabilityMatrix.Avalonia.Models.Inference;
[JsonSerializable(typeof(PromptCardModel))]
public class PromptCardModel
{
public string? Prompt { get; set; }
public string? NegativePrompt { get; set; }
public string? Prompt { get; init; }
public string? NegativePrompt { get; init; }
public JsonObject? ModulesCardState { get; init; }
}

50
StabilityMatrix.Avalonia/Models/TreeFileExplorer/TreeFileExplorerDirectory.cs

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Avalonia.Models.TreeFileExplorer;
public class TreeFileExplorerDirectory(IPathObject path, TreeFileExplorerOptions options)
: TreeFileExplorerItem(path, options)
{
public IEnumerable<TreeFileExplorerItem> Children =>
GetChildren(Path, Options)
.OrderByDescending(item => item.Path is DirectoryPath)
.ThenBy(item => item.Path.Name);
private static IEnumerable<TreeFileExplorerItem> GetChildren(
IPathObject pathObject,
TreeFileExplorerOptions options
)
{
return pathObject switch
{
FilePath => Enumerable.Empty<TreeFileExplorerItem>(),
DirectoryPath directoryPath => GetChildren(directoryPath, options),
_ => throw new NotSupportedException()
};
}
private static IEnumerable<TreeFileExplorerItem> GetChildren(
DirectoryPath directoryPath,
TreeFileExplorerOptions options
)
{
if (options.HasFlag(TreeFileExplorerOptions.IndexFiles))
{
foreach (var file in directoryPath.EnumerateFiles())
{
yield return new TreeFileExplorerFile(file, options);
}
}
if (options.HasFlag(TreeFileExplorerOptions.IndexFolders))
{
foreach (var directory in directoryPath.EnumerateDirectories())
{
yield return new TreeFileExplorerDirectory(directory, options);
}
}
}
}

6
StabilityMatrix.Avalonia/Models/TreeFileExplorer/TreeFileExplorerFile.cs

@ -0,0 +1,6 @@
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Avalonia.Models.TreeFileExplorer;
public class TreeFileExplorerFile(IPathObject path, TreeFileExplorerOptions options)
: TreeFileExplorerItem(path, options);

10
StabilityMatrix.Avalonia/Models/TreeFileExplorer/TreeFileExplorerItem.cs

@ -0,0 +1,10 @@
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Avalonia.Models.TreeFileExplorer;
public class TreeFileExplorerItem(IPathObject path, TreeFileExplorerOptions options)
{
public IPathObject Path { get; } = path;
public TreeFileExplorerOptions Options { get; } = options;
}

15
StabilityMatrix.Avalonia/Models/TreeFileExplorer/TreeFileExplorerOptions.cs

@ -0,0 +1,15 @@
using System;
namespace StabilityMatrix.Avalonia.Models.TreeFileExplorer;
[Flags]
public enum TreeFileExplorerOptions
{
None = 0,
IndexFiles = 1 << 5,
IndexFolders = 1 << 6,
CanSelectFiles = 1 << 10,
CanSelectFolders = 1 << 11,
}

8
StabilityMatrix.Avalonia/Models/TreeFileExplorer/TreeFileExplorerType.cs

@ -0,0 +1,8 @@
namespace StabilityMatrix.Avalonia.Models.TreeFileExplorer;
public enum TreeFileExplorerType
{
None,
File,
Directory
}

11
StabilityMatrix.Avalonia/Services/IInferenceClientManager.cs

@ -12,10 +12,7 @@ using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Avalonia.Services;
public interface IInferenceClientManager
: IDisposable,
INotifyPropertyChanged,
INotifyPropertyChanging
public interface IInferenceClientManager : IDisposable, INotifyPropertyChanged, INotifyPropertyChanging
{
ComfyClient? Client { get; set; }
@ -43,6 +40,7 @@ public interface IInferenceClientManager
IObservableCollection<HybridModelFile> Models { get; }
IObservableCollection<HybridModelFile> VaeModels { get; }
IObservableCollection<HybridModelFile> ControlNetModels { get; }
IObservableCollection<HybridModelFile> PromptExpansionModels { get; }
IObservableCollection<ComfySampler> Samplers { get; }
IObservableCollection<ComfyUpscaler> Upscalers { get; }
IObservableCollection<ComfyScheduler> Schedulers { get; }
@ -51,10 +49,7 @@ public interface IInferenceClientManager
Task UploadInputImageAsync(ImageSource image, CancellationToken cancellationToken = default);
Task WriteImageToInputAsync(
ImageSource imageSource,
CancellationToken cancellationToken = default
);
Task WriteImageToInputAsync(ImageSource imageSource, CancellationToken cancellationToken = default);
Task ConnectAsync(CancellationToken cancellationToken = default);

46
StabilityMatrix.Avalonia/Services/InferenceClientManager.cs

@ -74,6 +74,14 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
public IObservableCollection<HybridModelFile> ControlNetModels { get; } =
new ObservableCollectionExtended<HybridModelFile>();
private readonly SourceCache<HybridModelFile, string> promptExpansionModelsSource = new(p => p.GetId());
private readonly SourceCache<HybridModelFile, string> downloadablePromptExpansionModelsSource =
new(p => p.GetId());
public IObservableCollection<HybridModelFile> PromptExpansionModels { get; } =
new ObservableCollectionExtended<HybridModelFile>();
private readonly SourceCache<ComfySampler, string> samplersSource = new(p => p.Name);
public IObservableCollection<ComfySampler> Samplers { get; } =
@ -130,6 +138,18 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
.Bind(ControlNetModels)
.Subscribe();
promptExpansionModelsSource
.Connect()
.Or(downloadablePromptExpansionModelsSource.Connect())
.Sort(
SortExpressionComparer<HybridModelFile>
.Ascending(f => f.Type)
.ThenByAscending(f => f.ShortDisplayName)
)
.DeferUntilLoaded()
.Bind(PromptExpansionModels)
.Subscribe();
vaeModelsDefaults.AddOrUpdate(HybridModelFile.Default);
vaeModelsDefaults.Connect().Or(vaeModelsSource.Connect()).Bind(VaeModels).Subscribe();
@ -199,6 +219,8 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
);
}
// Prompt Expansion indexing is local only
// Fetch sampler names from KSampler node
if (await Client.GetSamplerNamesAsync() is { } samplerNames)
{
@ -277,6 +299,22 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
);
downloadableControlNetModelsSource.EditDiff(downloadableControlNets, HybridModelFile.Comparer);
// Load local prompt expansion models
promptExpansionModelsSource.EditDiff(
modelIndexService
.GetFromModelIndex(SharedFolderType.PromptExpansion)
.Select(HybridModelFile.FromLocal),
HybridModelFile.Comparer
);
// Downloadable PromptExpansion models
downloadablePromptExpansionModelsSource.EditDiff(
RemoteModels.PromptExpansionModels.Where(
u => !promptExpansionModelsSource.Lookup(u.GetId()).HasValue
),
HybridModelFile.Comparer
);
// Load local VAE models
vaeModelsSource.EditDiff(
modelIndexService.GetFromModelIndex(SharedFolderType.VAE).Select(HybridModelFile.FromLocal),
@ -369,6 +407,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
inferenceInputs.Create();
}
[MemberNotNull(nameof(Client))]
private async Task ConnectAsyncImpl(Uri uri, CancellationToken cancellationToken = default)
{
if (IsConnected)
@ -480,11 +519,8 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient
await ConnectAsyncImpl(uri, cancellationToken);
var packageDir = new DirectoryPath(packagePair.InstalledPackage.FullPath);
// Set package paths
Client!.OutputImagesDir = packageDir.JoinDir("output");
Client!.InputImagesDir = packageDir.JoinDir("input");
Client.LocalServerPackage = packagePair;
Client.LocalServerPath = packagePair.InstalledPackage.FullPath!;
}
public async Task CloseAsync()

33
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -17,7 +17,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>./Assets/Icon.ico</ApplicationIcon>
<Version>2.9.0-dev.999</Version>
<Version>2.10.0-dev.999</Version>
<InformationalVersion>$(Version)</InformationalVersion>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@ -39,13 +39,13 @@
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.2.1" />
<PackageReference Include="AutoComplete.Net" Version="1.2211.2014.42"/>
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.7" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.9" />
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0.2" />
<PackageReference Include="Avalonia" Version="11.0.7" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.7" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.7" />
<PackageReference Include="Avalonia" Version="11.0.9" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.9" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.9" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.7" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.9" />
<PackageReference Include="Avalonia.HtmlRenderer" Version="11.0.0" />
<PackageReference Include="Avalonia.Labs.Controls" Version="11.0.3" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.6" />
@ -65,8 +65,8 @@
<PackageReference Include="Exceptionless.DateTimeExtensions" Version="3.4.3" />
<PackageReference Include="FluentAvalonia.BreadcrumbBar" Version="2.0.2" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.5" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.225" />
<PackageReference Include="FluentIcons.Avalonia.Fluent" Version="1.1.225" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.230" />
<PackageReference Include="FluentIcons.Avalonia.Fluent" Version="1.1.230" />
<PackageReference Include="FuzzySharp" Version="2.0.2" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Markdown.Avalonia" Version="11.0.2" />
@ -77,23 +77,26 @@
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NLog" Version="5.2.8" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.8" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="Polly" Version="8.2.1" />
<PackageReference Include="OneOf" Version="3.0.263" />
<PackageReference Include="Polly" Version="8.3.0" />
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.1.1" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.0.1" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.1.1" />
<PackageReference Include="RockLib.Reflection.Optimized" Version="3.0.0" />
<PackageReference Include="Sentry" Version="3.41.3" />
<PackageReference Include="Sentry.NLog" Version="3.41.3" />
<PackageReference Include="Sentry" Version="4.1.2" />
<PackageReference Include="Sentry.NLog" Version="4.1.2" />
<PackageReference Include="SpacedGrid-Avalonia" Version="11.0.0" />
<PackageReference Include="Sylvan.Common" Version="0.4.3" />
<PackageReference Include="Sylvan.Data" Version="0.2.12" />
<PackageReference Include="Sylvan.Data.Csv" Version="1.3.5" />
<PackageReference Include="Sylvan.Data" Version="0.2.13" />
<PackageReference Include="Sylvan.Data.Csv" Version="1.3.7" />
<PackageReference Include="System.Drawing.Common" Version="8.0.2" />
<PackageReference Include="System.IO.Hashing" Version="8.0.0" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.56" />
<PackageReference Include="URISchemeTools" Version="1.0.2" />
</ItemGroup>

2
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings

@ -1,3 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeEditing/Localization/Localizable/@EntryValue">Yes</s:String>
<s:String x:Key="/Default/CodeEditing/Localization/LocalizableInspector/@EntryValue">Pessimistic</s:String>
<s:String x:Key="/Default/CodeInspection/Daemon/ConfigureAwaitAnalysisMode/@EntryValue">UI</s:String>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls_005Cpropertygrid/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

33
StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml

@ -10,7 +10,10 @@
<Design.PreviewWith>
<Panel Width="450" Height="600">
<StackPanel Margin="8" Spacing="4" Width="250">
<StackPanel
Width="250"
Margin="8"
Spacing="4">
<controls:BetterComboBox
HorizontalAlignment="Stretch"
ItemsSource="{x:Static mocks:DesignData.SampleHybridModels}"
@ -83,7 +86,7 @@
</Template>
</Setter>
</ControlTheme>
<!-- ReSharper disable once Xaml.StaticResourceNotResolved -->
<ControlTheme
x:Key="{x:Type controls:BetterComboBox}"
@ -139,11 +142,16 @@
<sg:SpacedGrid
Grid.Row="1"
Grid.Column="1"
ColumnDefinitions="Auto,*"
RowDefinitions="Auto,Auto,Auto"
RowSpacing="1">
<TextBlock Text="{Binding Local.DisplayModelName}" TextTrimming="CharacterEllipsis" />
<TextBlock
Grid.ColumnSpan="2"
Text="{Binding Local.DisplayModelName}"
TextTrimming="CharacterEllipsis" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
FontSize="12"
FontWeight="Regular"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
@ -151,10 +159,27 @@
TextTrimming="CharacterEllipsis" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
FontSize="11"
FontWeight="Normal"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding Local.DisplayModelFileName}" />
<!-- Badges -->
<StackPanel
Grid.Row="1"
Grid.RowSpan="2"
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Orientation="Horizontal">
<fluentIcons:SymbolIcon
Margin="4"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
IsVisible="{Binding Local.ConfigFullPath, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=False}"
Symbol="BeakerSettings"
ToolTip.Tip="{Binding Local.DisplayConfigFileName}" />
</StackPanel>
</sg:SpacedGrid>
</sg:SpacedGrid>
</DataTemplate>
@ -236,7 +261,7 @@
<Setter Property="SelectionBoxItemTemplate" Value="{StaticResource HybridModelSelectionBoxTemplateSelector}" />
<Setter Property="ItemTemplate" Value="{StaticResource HybridModelTemplateSelector}" />
<Setter Property="ItemContainerTheme" Value="{StaticResource BetterComboBoxItemHybridModelTheme}" />
<Style Selector="^ /template/ Popup#PART_Popup">
<Setter Property="Width" Value="400" />
<Setter Property="Placement" Value="Bottom" />

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

@ -15,11 +15,15 @@ using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input;
using ExifLibrary;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.DependencyInjection;
using Nito.Disposables.Internals;
using NLog;
using Refit;
using SkiaSharp;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
@ -35,6 +39,8 @@ 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.Models.PackageModification;
using StabilityMatrix.Core.Models.Packages.Extensions;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
using Notification = DesktopNotifications.Notification;
@ -197,6 +203,26 @@ public abstract partial class InferenceGenerationViewModelBase
/// </summary>
protected virtual void BuildPrompt(BuildPromptEventArgs args) { }
/// <summary>
/// Uploads files required for the prompt
/// </summary>
protected virtual async Task UploadPromptFiles(
IEnumerable<(string SourcePath, string DestinationRelativePath)> files,
ComfyClient client
)
{
foreach (var (sourcePath, destinationRelativePath) in files)
{
Logger.Debug(
"Uploading prompt file {SourcePath} to relative path {DestinationPath}",
sourcePath,
destinationRelativePath
);
await client.UploadFileAsync(sourcePath, destinationRelativePath);
}
}
/// <summary>
/// Gets ImageSources that need to be uploaded as inputs
/// </summary>
@ -252,9 +278,21 @@ public abstract partial class InferenceGenerationViewModelBase
if (client.OutputImagesDir is null)
throw new InvalidOperationException("OutputImagesDir is null");
// Only check extensions for first batch index
if (args.BatchIndex == 0)
{
if (!await CheckPromptExtensionsInstalled(args.Nodes))
{
throw new ValidationException("Prompt extensions not installed");
}
}
// Upload input images
await UploadInputImages(client);
// Upload required files
await UploadPromptFiles(args.FilesToTransfer, client);
// Connect preview image handler
client.PreviewImageReceived += OnPreviewImageReceived;
@ -598,6 +636,121 @@ public abstract partial class InferenceGenerationViewModelBase
return ClientManager.IsConnected;
}
/// <summary>
/// Shows a dialog and return false if prompt required extensions not installed
/// </summary>
private async Task<bool> CheckPromptExtensionsInstalled(NodeDictionary nodeDictionary)
{
// Get prompt required extensions
// Just static for now but could do manifest lookup when we support custom workflows
var requiredExtensions = nodeDictionary
.ClassTypeRequiredExtensions.Values.SelectMany(x => x)
.ToHashSet();
// Skip if no extensions required
if (requiredExtensions.Count == 0)
{
return true;
}
// Get installed extensions
var localPackagePair = ClientManager.Client?.LocalServerPackage.Unwrap()!;
var manager = localPackagePair.BasePackage.ExtensionManager.Unwrap();
var localExtensions = (
await ((GitPackageExtensionManager)manager).GetInstalledExtensionsLiteAsync(
localPackagePair.InstalledPackage
)
).ToImmutableArray();
var missingExtensions = requiredExtensions
.Except(localExtensions.Select(ext => ext.GitRepositoryUrl).WhereNotNull())
.ToImmutableArray();
if (missingExtensions.Length == 0)
{
return true;
}
var dialog = DialogHelper.CreateMarkdownDialog(
$"#### The following extensions are required for this workflow:\n"
+ $"{string.Join("\n- ", missingExtensions)}",
"Install Required Extensions?"
);
dialog.IsPrimaryButtonEnabled = true;
dialog.DefaultButton = ContentDialogButton.Primary;
dialog.PrimaryButtonText =
$"{Resources.Action_Install} ({localPackagePair.InstalledPackage.DisplayName.ToRepr()} will restart)";
dialog.CloseButtonText = Resources.Action_Cancel;
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
var manifestExtensionsMap = await manager.GetManifestExtensionsMapAsync(
manager.GetManifests(localPackagePair.InstalledPackage)
);
var steps = new List<IPackageStep>();
foreach (var missingExtensionUrl in missingExtensions)
{
if (!manifestExtensionsMap.TryGetValue(missingExtensionUrl, out var extension))
{
Logger.Warn(
"Extension {MissingExtensionUrl} not found in manifests",
missingExtensionUrl
);
continue;
}
steps.Add(new InstallExtensionStep(manager, localPackagePair.InstalledPackage, extension));
}
var runner = new PackageModificationRunner
{
ShowDialogOnStart = true,
ModificationCompleteTitle = "Extensions Installed",
ModificationCompleteMessage = "Finished installing required extensions"
};
EventManager.Instance.OnPackageInstallProgressAdded(runner);
runner
.ExecuteSteps(steps)
.ContinueWith(async _ =>
{
if (runner.Failed)
return;
// Restart Package
// TODO: This should be handled by some DI package manager service
var launchPage = App.Services.GetRequiredService<LaunchPageViewModel>();
try
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
await launchPage.Stop();
await launchPage.LaunchAsync();
});
}
catch (Exception e)
{
Logger.Error(e, "Error while restarting package");
notificationService.ShowPersistent(
new AppException(
"Could not restart package",
"Please manually restart the package for extension changes to take effect"
)
);
}
})
.SafeFireAndForget();
}
return false;
}
/// <summary>
/// Handles the preview image received event from the websocket.
/// Updates the preview image in the image gallery.
@ -660,9 +813,11 @@ public abstract partial class InferenceGenerationViewModelBase
public required ComfyClient Client { get; init; }
public required NodeDictionary Nodes { get; init; }
public required IReadOnlyList<string> OutputNodeNames { get; init; }
public int BatchIndex { get; init; }
public GenerationParameters? Parameters { get; init; }
public InferenceProjectDocument? Project { get; init; }
public bool ClearOutputImages { get; init; } = true;
public List<(string SourcePath, string DestinationRelativePath)> FilesToTransfer { get; init; } = [];
}
public class BuildPromptEventArgs : EventArgs
@ -670,17 +825,28 @@ public abstract partial class InferenceGenerationViewModelBase
public ComfyNodeBuilder Builder { get; } = new();
public GenerateOverrides Overrides { get; init; } = new();
public long? SeedOverride { get; init; }
public List<(string SourcePath, string DestinationRelativePath)> FilesToTransfer { get; init; } = [];
public static implicit operator ModuleApplyStepEventArgs(BuildPromptEventArgs args)
public ModuleApplyStepEventArgs ToModuleApplyStepEventArgs()
{
var overrides = new Dictionary<Type, bool>();
if (args.Overrides.IsHiresFixEnabled.HasValue)
if (Overrides.IsHiresFixEnabled.HasValue)
{
overrides[typeof(HiresFixModule)] = args.Overrides.IsHiresFixEnabled.Value;
overrides[typeof(HiresFixModule)] = Overrides.IsHiresFixEnabled.Value;
}
return new ModuleApplyStepEventArgs { Builder = args.Builder, IsEnabledOverrides = overrides };
return new ModuleApplyStepEventArgs
{
Builder = Builder,
IsEnabledOverrides = overrides,
FilesToTransfer = FilesToTransfer
};
}
public static implicit operator ModuleApplyStepEventArgs(BuildPromptEventArgs args)
{
return args.ToModuleApplyStepEventArgs();
}
}
}

14
StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs

@ -19,11 +19,13 @@ namespace StabilityMatrix.Avalonia.ViewModels.Base;
[JsonDerivedType(typeof(FreeUCardViewModel), FreeUCardViewModel.ModuleKey)]
[JsonDerivedType(typeof(UpscalerCardViewModel), UpscalerCardViewModel.ModuleKey)]
[JsonDerivedType(typeof(ControlNetCardViewModel), ControlNetCardViewModel.ModuleKey)]
[JsonDerivedType(typeof(PromptExpansionCardViewModel), PromptExpansionCardViewModel.ModuleKey)]
[JsonDerivedType(typeof(FreeUModule))]
[JsonDerivedType(typeof(HiresFixModule))]
[JsonDerivedType(typeof(UpscalerModule))]
[JsonDerivedType(typeof(ControlNetModule))]
[JsonDerivedType(typeof(SaveImageModule))]
[JsonDerivedType(typeof(PromptExpansionModule))]
public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@ -32,7 +34,8 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
private static readonly string[] SerializerIgnoredNames = { nameof(HasErrors) };
private static readonly JsonSerializerOptions SerializerOptions = new() { IgnoreReadOnlyProperties = true };
private static readonly JsonSerializerOptions SerializerOptions =
new() { IgnoreReadOnlyProperties = true };
private static bool ShouldIgnoreProperty(PropertyInfo property)
{
@ -243,7 +246,11 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
}
else
{
Logger.ConditionalTrace("Serializing {Property} ({Type})", property.Name, property.PropertyType);
Logger.ConditionalTrace(
"Serializing {Property} ({Type})",
property.Name,
property.PropertyType
);
var value = property.GetValue(this);
if (value is not null)
{
@ -266,7 +273,8 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
protected static JsonObject SerializeModel<T>(T model)
{
var node = JsonSerializer.SerializeToNode(model);
return node?.AsObject() ?? throw new NullReferenceException("Failed to serialize state to JSON object.");
return node?.AsObject()
?? throw new NullReferenceException("Failed to serialize state to JSON object.");
}
/// <summary>

37
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs

@ -190,13 +190,6 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
IsFavorite = settingsManager.Settings.FavoriteModels.Contains(CivitModel.Id);
}
[RelayCommand]
private async Task Import(CivitModel model)
{
await DoImport(model);
CheckIfInstalled();
}
[RelayCommand]
private async Task ShowVersionDialog(CivitModel model)
{
@ -220,7 +213,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
MaxDialogWidth = 750,
MaxDialogHeight = 850,
MaxDialogHeight = 950,
};
var prunedDescription = PruneDescription(model);
@ -229,6 +222,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
viewModel.Dialog = dialog;
viewModel.Title = model.Name;
viewModel.Description = prunedDescription;
viewModel.CivitModel = model;
viewModel.Versions = versions
.Select(
version =>
@ -252,8 +246,21 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
var selectedVersion = viewModel?.SelectedVersionViewModel?.ModelVersion;
var selectedFile = viewModel?.SelectedFile?.CivitFile;
DirectoryPath downloadPath;
if (viewModel?.IsCustomSelected is true)
{
downloadPath = viewModel.CustomInstallLocation;
}
else
{
var subFolder =
viewModel?.SelectedInstallLocation
?? Path.Combine("Models", model.Type.ConvertTo<SharedFolderType>().GetStringValue());
downloadPath = Path.Combine(settingsManager.LibraryDir, subFolder);
}
await Task.Delay(100);
await DoImport(model, selectedVersion, selectedFile);
await DoImport(model, downloadPath, selectedVersion, selectedFile);
}
private static string PruneDescription(CivitModel model)
@ -322,6 +329,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
private async Task DoImport(
CivitModel model,
DirectoryPath downloadFolder,
CivitModelVersion? selectedVersion = null,
CivitFile? selectedFile = null
)
@ -362,18 +370,13 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
return;
}
var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory);
var downloadDirectory = rootModelsDirectory.JoinDir(
model.Type.ConvertTo<SharedFolderType>().GetStringValue()
);
// Folders might be missing if user didn't install any packages yet
downloadDirectory.Create();
downloadFolder.Create();
var downloadPath = downloadDirectory.JoinFile(modelFile.Name);
var downloadPath = downloadFolder.JoinFile(modelFile.Name);
// Download model info and preview first
var cmInfoPath = await SaveCmInfo(model, modelVersion, modelFile, downloadDirectory);
var cmInfoPath = await SaveCmInfo(model, modelVersion, modelFile, downloadFolder);
var previewImagePath = await SavePreviewImage(modelVersion, downloadPath);
// Create tracked download

12
StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs

@ -111,6 +111,10 @@ public partial class CheckpointFolder : ViewModelBase
public IObservableCollection<string> BaseModelOptions { get; } =
new ObservableCollectionExtended<string>();
private IEnumerable<string> allModelOptions = Enum.GetValues<CivitBaseModelType>()
.Where(x => x != CivitBaseModelType.All)
.Select(x => x.GetStringValue());
public CheckpointFolder(
ISettingsManager settingsManager,
IDownloadService downloadService,
@ -175,11 +179,7 @@ public partial class CheckpointFolder : ViewModelBase
SubFoldersCache.Refresh();
});
BaseModelOptionsCache.AddOrUpdate(
Enum.GetValues<CivitBaseModelType>()
.Where(x => x != CivitBaseModelType.All)
.Select(x => x.GetStringValue())
);
BaseModelOptionsCache.AddOrUpdate(allModelOptions);
CheckpointFiles.CollectionChanged += OnCheckpointFilesChanged;
// DisplayedCheckpointFiles = CheckpointFiles;
@ -187,7 +187,7 @@ public partial class CheckpointFolder : ViewModelBase
private bool BaseModelFilter(CheckpointFile file)
{
return file.IsConnectedModel
return file.IsConnectedModel && allModelOptions.Contains(file.ConnectedModel!.BaseModel)
? BaseModelOptions.Contains(file.ConnectedModel!.BaseModel)
: BaseModelOptions.Contains("Other");
}

49
StabilityMatrix.Avalonia/ViewModels/Dialogs/DownloadResourceViewModel.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@ -8,8 +9,10 @@ using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
@ -18,15 +21,17 @@ namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[View(typeof(DownloadResourceDialog))]
[ManagedService]
[Transient]
public partial class DownloadResourceViewModel : ContentDialogViewModelBase
public partial class DownloadResourceViewModel(
IDownloadService downloadService,
ISettingsManager settingsManager,
ITrackedDownloadService trackedDownloadService
) : ContentDialogViewModelBase
{
private readonly IDownloadService downloadService;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FileNameWithHash))]
private string? fileName;
public string FileNameWithHash => $"{FileName} [{Resource.HashSha256.ToLowerInvariant()[..7]}]";
public string FileNameWithHash => $"{FileName} [{Resource.HashSha256?.ToLowerInvariant()[..7]}]";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FileSizeText))]
@ -37,12 +42,7 @@ public partial class DownloadResourceViewModel : ContentDialogViewModelBase
public string? FileSizeText => FileSize == 0 ? null : Size.FormatBase10Bytes(FileSize);
public string ShortHash => Resource.HashSha256.ToLowerInvariant();
public DownloadResourceViewModel(IDownloadService downloadService)
{
this.downloadService = downloadService;
}
public string? ShortHash => Resource.HashSha256?.ToLowerInvariant()[..7];
/// <inheritdoc />
public override async Task OnLoadedAsync()
@ -65,6 +65,33 @@ public partial class DownloadResourceViewModel : ContentDialogViewModelBase
}
}
public TrackedDownload StartDownload()
{
var sharedFolderType =
Resource.ContextType as SharedFolderType?
?? throw new InvalidOperationException("ContextType is not SharedFolderType");
var modelsDir = new DirectoryPath(settingsManager.ModelsDirectory).JoinDir(
sharedFolderType.GetStringValue()
);
var download = trackedDownloadService.NewDownload(
Resource.Url,
modelsDir.JoinFile(Resource.FileName)
);
// Set extraction properties
download.AutoExtractArchive = Resource.AutoExtractArchive;
download.ExtractRelativePath = Resource.ExtractRelativePath;
download.ContextAction = new ModelPostDownloadContextAction();
download.Start();
EventManager.Instance.OnToggleProgressFlyout();
return download;
}
public BetterContentDialog GetDialog()
{
return new BetterContentDialog

111
StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs

@ -6,6 +6,7 @@ using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
@ -15,7 +16,10 @@ using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
@ -23,17 +27,20 @@ namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
[ManagedService]
[Transient]
public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
public partial class SelectModelVersionViewModel(
ISettingsManager settingsManager,
IDownloadService downloadService,
IModelIndexService modelIndexService,
INotificationService notificationService
) : ContentDialogViewModelBase
{
private readonly ISettingsManager settingsManager;
private readonly IDownloadService downloadService;
private readonly IModelIndexService modelIndexService;
private readonly INotificationService notificationService;
private readonly IDownloadService downloadService = downloadService;
public required ContentDialog Dialog { get; set; }
public required IReadOnlyList<ModelVersionViewModel> Versions { get; set; }
public required string Description { get; set; }
public required string Title { get; set; }
public required CivitModel CivitModel { get; set; }
[ObservableProperty]
private Bitmap? previewImage;
@ -63,25 +70,28 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
[ObservableProperty]
private string importTooltip = string.Empty;
public int DisplayedPageNumber => SelectedImageIndex + 1;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsCustomSelected))]
[NotifyPropertyChangedFor(nameof(ShowEmptyPathWarning))]
private string selectedInstallLocation = string.Empty;
public SelectModelVersionViewModel(
ISettingsManager settingsManager,
IDownloadService downloadService,
IModelIndexService modelIndexService,
INotificationService notificationService
)
{
this.settingsManager = settingsManager;
this.downloadService = downloadService;
this.modelIndexService = modelIndexService;
this.notificationService = notificationService;
}
[ObservableProperty]
private ObservableCollection<string> availableInstallLocations = [];
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowEmptyPathWarning))]
private string customInstallLocation = string.Empty;
public bool IsCustomSelected => SelectedInstallLocation == "Custom...";
public bool ShowEmptyPathWarning => IsCustomSelected && string.IsNullOrWhiteSpace(CustomInstallLocation);
public int DisplayedPageNumber => SelectedImageIndex + 1;
public override void OnLoaded()
{
SelectedVersionViewModel = Versions[0];
CanGoToNextImage = true;
LoadInstallLocations();
}
partial void OnSelectedVersionViewModelChanged(ModelVersionViewModel? value)
@ -138,7 +148,26 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
ImportTooltip = "Please set the library directory in settings";
}
IsImportEnabled = value?.CivitFile != null && canImport;
IsImportEnabled = value?.CivitFile != null && canImport && !ShowEmptyPathWarning;
}
partial void OnSelectedInstallLocationChanged(string value)
{
if (value.Equals("Custom...", StringComparison.OrdinalIgnoreCase))
{
Dispatcher.UIThread.InvokeAsync(SelectCustomFolder);
}
else
{
CustomInstallLocation = string.Empty;
}
IsImportEnabled = !ShowEmptyPathWarning;
}
partial void OnCustomInstallLocationChanged(string value)
{
IsImportEnabled = !ShowEmptyPathWarning;
}
public void Cancel()
@ -250,4 +279,48 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
CanGoToPreviousImage = SelectedImageIndex > 0;
CanGoToNextImage = SelectedImageIndex < ImageUrls.Count - 1;
}
public async Task SelectCustomFolder()
{
var files = await App.StorageProvider.OpenFolderPickerAsync(
new FolderPickerOpenOptions
{
Title = "Select Download Folder",
AllowMultiple = false,
SuggestedStartLocation = await App.StorageProvider.TryGetFolderFromPathAsync(
Path.Combine(
settingsManager.ModelsDirectory,
CivitModel.Type.ConvertTo<SharedFolderType>().GetStringValue()
)
)
}
);
if (files.FirstOrDefault()?.TryGetLocalPath() is { } path)
{
CustomInstallLocation = path;
}
}
private void LoadInstallLocations()
{
var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory);
var downloadDirectory = rootModelsDirectory.JoinDir(
CivitModel.Type.ConvertTo<SharedFolderType>().GetStringValue()
);
var installLocations = new ObservableCollection<string>
{
downloadDirectory.ToString().Replace(rootModelsDirectory, "Models")
};
foreach (var directory in downloadDirectory.EnumerateDirectories())
{
installLocations.Add(directory.ToString().Replace(rootModelsDirectory, "Models"));
}
installLocations.Add("Custom...");
AvailableInstallLocations = installLocations;
SelectedInstallLocation = installLocations.FirstOrDefault();
}
}

27
StabilityMatrix.Avalonia/ViewModels/Inference/ControlNetCardViewModel.cs

@ -24,8 +24,6 @@ public partial class ControlNetCardViewModel : LoadableViewModelBase
{
public const string ModuleKey = "ControlNet";
private readonly ITrackedDownloadService trackedDownloadService;
private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> vmFactory;
[ObservableProperty]
@ -56,14 +54,10 @@ public partial class ControlNetCardViewModel : LoadableViewModelBase
public IInferenceClientManager ClientManager { get; }
public ControlNetCardViewModel(
ITrackedDownloadService trackedDownloadService,
ISettingsManager settingsManager,
IInferenceClientManager clientManager,
ServiceManager<ViewModelBase> vmFactory
)
{
this.trackedDownloadService = trackedDownloadService;
this.settingsManager = settingsManager;
this.vmFactory = vmFactory;
ClientManager = clientManager;
@ -76,30 +70,13 @@ public partial class ControlNetCardViewModel : LoadableViewModelBase
if (modelFile?.DownloadableResource is not { } resource)
return;
var sharedFolderType =
resource.ContextType as SharedFolderType?
?? throw new InvalidOperationException("ContextType is not SharedFolderType");
var confirmDialog = vmFactory.Get<DownloadResourceViewModel>();
confirmDialog.Resource = resource;
confirmDialog.FileName = modelFile.FileName;
if (await confirmDialog.GetDialog().ShowAsync() != ContentDialogResult.Primary)
if (await confirmDialog.GetDialog().ShowAsync() == ContentDialogResult.Primary)
{
return;
confirmDialog.StartDownload();
}
var modelsDir = new DirectoryPath(settingsManager.ModelsDirectory).JoinDir(
sharedFolderType.GetStringValue()
);
var download = trackedDownloadService.NewDownload(
resource.Url,
modelsDir.JoinFile(modelFile.FileName)
);
download.ContextAction = new ModelPostDownloadContextAction();
download.Start();
EventManager.Instance.OnToggleProgressFlyout();
}
}

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

@ -199,6 +199,8 @@ public partial class InferenceImageToVideoViewModel
OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(),
Parameters = SaveStateToParameters(new GenerationParameters()),
Project = InferenceProjectDocument.FromLoadable(this),
FilesToTransfer = buildPromptArgs.FilesToTransfer,
BatchIndex = i,
// Only clear output images on the first batch
ClearOutputImages = i == 0
};

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

@ -213,6 +213,8 @@ public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase, I
OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(),
Parameters = SaveStateToParameters(new GenerationParameters()),
Project = InferenceProjectDocument.FromLoadable(this),
FilesToTransfer = buildPromptArgs.FilesToTransfer,
BatchIndex = i,
// Only clear output images on the first batch
ClearOutputImages = i == 0
};

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

@ -1,9 +1,11 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
@ -53,6 +55,28 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
public IInferenceClientManager ClientManager { get; } = clientManager;
[RelayCommand]
private static async Task OnConfigClickAsync()
{
await DialogHelper
.CreateMarkdownDialog(
"""
You can use a config (.yaml) file to load a model with specific settings.
Place the config file next to the model file with the same name:
```md
Models/
StableDiffusion/
my_model.safetensors
my_model.yaml <-
```
""",
"Using Model Configs",
TextEditorPreset.Console
)
.ShowAsync();
}
public async Task<bool> ValidateModel()
{
if (SelectedModel != null)
@ -66,16 +90,41 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
return false;
}
private static ComfyTypedNodeBase<
ModelNodeConnection,
ClipNodeConnection,
VAENodeConnection
> GetModelLoader(ModuleApplyStepEventArgs e, string nodeName, HybridModelFile model)
{
// Check if config
if (model.Local?.ConfigFullPath is { } configPath)
{
// We'll need to upload the config file to `models/configs` later
var uploadConfigPath = e.AddFileTransferToConfigs(configPath);
return new ComfyNodeBuilder.CheckpointLoader
{
Name = nodeName,
// Only the file name is needed
ConfigName = Path.GetFileName(uploadConfigPath),
CkptName = model.RelativePath
};
}
// Simple loader if no config
return new ComfyNodeBuilder.CheckpointLoaderSimple { Name = nodeName, CkptName = model.RelativePath };
}
/// <inheritdoc />
public virtual void ApplyStep(ModuleApplyStepEventArgs e)
{
// Load base checkpoint
var baseLoader = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.CheckpointLoaderSimple
{
Name = "CheckpointLoader_Base",
CkptName = SelectedModel?.RelativePath ?? throw new ValidationException("Model not selected")
}
GetModelLoader(
e,
"CheckpointLoader_Base",
SelectedModel ?? throw new ValidationException("Model not selected")
)
);
e.Builder.Connections.Base.Model = baseLoader.Output1;
@ -86,13 +135,11 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
if (IsRefinerSelectionEnabled && SelectedRefiner is { IsNone: false })
{
var refinerLoader = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.CheckpointLoaderSimple
{
Name = "CheckpointLoader_Refiner",
CkptName =
SelectedRefiner?.RelativePath
?? throw new ValidationException("Refiner Model enabled but not selected")
}
GetModelLoader(
e,
"CheckpointLoader_Refiner",
SelectedRefiner ?? throw new ValidationException("Refiner Model enabled but not selected")
)
);
e.Builder.Connections.Refiner.Model = refinerLoader.Output1;

40
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/PromptExpansionModule.cs

@ -0,0 +1,40 @@
using System;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
[ManagedService]
[Transient]
public class PromptExpansionModule : ModuleBase
{
public PromptExpansionModule(ServiceManager<ViewModelBase> vmFactory)
: base(vmFactory)
{
Title = "Prompt Expansion";
AddCards(vmFactory.Get<PromptExpansionCardViewModel>());
}
protected override void OnApplyStep(ModuleApplyStepEventArgs e)
{
var promptExpansionCard = GetCard<PromptExpansionCardViewModel>();
var model =
promptExpansionCard.SelectedModel
?? throw new InvalidOperationException($"{Title}: Model not selected");
e.Builder.Connections.PositivePrompt = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.PromptExpansion
{
Name = e.Nodes.GetUniqueName("PromptExpansion_Positive"),
ModelName = model.RelativePath,
Text = e.Builder.Connections.PositivePrompt,
Seed = e.Builder.Connections.Seed,
LogPrompt = promptExpansionCard.IsLogOutputEnabled
}
).Output;
}
}

50
StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs

@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
@ -13,12 +12,13 @@ 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;
using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Services;
@ -43,6 +43,8 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
public TextDocument PromptDocument { get; } = new();
public TextDocument NegativePromptDocument { get; } = new();
public StackEditableCardViewModel ModulesCardViewModel { get; }
[ObservableProperty]
private bool isAutoCompletionEnabled;
@ -52,6 +54,7 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
ITokenizerProvider tokenizerProvider,
ISettingsManager settingsManager,
IModelIndexService modelIndexService,
ServiceManager<ViewModelBase> vmFactory,
SharedState sharedState
)
{
@ -60,6 +63,12 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
TokenizerProvider = tokenizerProvider;
SharedState = sharedState;
ModulesCardViewModel = vmFactory.Get<StackEditableCardViewModel>(vm =>
{
vm.Title = "Styles";
vm.AvailableModules = [typeof(PromptExpansionModule)];
});
settingsManager.RelayPropertyFor(
this,
vm => vm.IsAutoCompletionEnabled,
@ -84,8 +93,14 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
// Load prompts
var positivePrompt = GetPrompt();
positivePrompt.Process();
e.Builder.Connections.PositivePrompt = positivePrompt.ProcessedText;
var negativePrompt = GetNegativePrompt();
negativePrompt.Process();
e.Builder.Connections.NegativePrompt = negativePrompt.ProcessedText;
// Apply modules / styles that may modify the prompt
ModulesCardViewModel.ApplyStep(e);
foreach (var modelConnections in e.Builder.Connections.Models.Values)
{
@ -98,7 +113,12 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
var loras = positivePrompt.GetExtraNetworksAsLocalModels(modelIndexService).ToList();
// Add group to load loras onto model and clip in series
var lorasGroup = e.Builder.Group_LoraLoadMany($"Loras_{modelConnections.Name}", model, clip, loras);
var lorasGroup = e.Builder.Group_LoraLoadMany(
$"Loras_{modelConnections.Name}",
model,
clip,
loras
);
// Set last outputs as model and clip
modelConnections.Model = lorasGroup.Output1;
@ -111,7 +131,7 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
{
Name = $"PositiveCLIP_{modelConnections.Name}",
Clip = e.Builder.Connections.Base.Clip!,
Text = positivePrompt.ProcessedText
Text = e.Builder.Connections.PositivePrompt
}
);
var negativeClip = e.Nodes.AddTypedNode(
@ -119,7 +139,7 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
{
Name = $"NegativeCLIP_{modelConnections.Name}",
Clip = e.Builder.Connections.Base.Clip!,
Text = negativePrompt.ProcessedText
Text = e.Builder.Connections.NegativePrompt
}
);
@ -319,7 +339,12 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
public override JsonObject SaveStateToJsonObject()
{
return SerializeModel(
new PromptCardModel { Prompt = PromptDocument.Text, NegativePrompt = NegativePromptDocument.Text }
new PromptCardModel
{
Prompt = PromptDocument.Text,
NegativePrompt = NegativePromptDocument.Text,
ModulesCardState = ModulesCardViewModel.SaveStateToJsonObject()
}
);
}
@ -330,6 +355,11 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
PromptDocument.Text = model.Prompt ?? "";
NegativePromptDocument.Text = model.NegativePrompt ?? "";
if (model.ModulesCardState is not null)
{
ModulesCardViewModel.LoadStateFromJsonObject(model.ModulesCardState);
}
}
/// <inheritdoc />
@ -342,6 +372,10 @@ public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoa
/// <inheritdoc />
public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
{
return parameters with { PositivePrompt = PromptDocument.Text, NegativePrompt = NegativePromptDocument.Text };
return parameters with
{
PositivePrompt = PromptDocument.Text,
NegativePrompt = NegativePromptDocument.Text
};
}
}

47
StabilityMatrix.Avalonia/ViewModels/Inference/PromptExpansionCardViewModel.cs

@ -0,0 +1,47 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(PromptExpansionCard))]
[ManagedService]
[Transient]
public partial class PromptExpansionCardViewModel(
IInferenceClientManager clientManager,
ServiceManager<ViewModelBase> vmFactory
) : LoadableViewModelBase
{
public const string ModuleKey = "PromptExpansion";
public IInferenceClientManager ClientManager { get; } = clientManager;
[ObservableProperty]
private HybridModelFile? selectedModel;
[ObservableProperty]
private bool isLogOutputEnabled = true;
[RelayCommand]
private async Task RemoteDownload(HybridModelFile? model)
{
if (model?.DownloadableResource is not { } resource)
return;
var confirmDialog = vmFactory.Get<DownloadResourceViewModel>();
confirmDialog.Resource = resource;
confirmDialog.FileName = resource.FileName;
if (await confirmDialog.GetDialog().ShowAsync() == ContentDialogResult.Primary)
{
confirmDialog.StartDownload();
}
}
}

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

@ -90,9 +90,18 @@ public partial class SelectImageCardViewModel(INotificationService notificationS
partial void OnImageSourceChanged(ImageSource? value)
{
// Cache the hash for later upload use
if (value?.LocalFile is { Exists: true })
if (value?.LocalFile is { Exists: true } localFile)
{
value.GetBlake3HashAsync().SafeFireAndForget();
value
.GetBlake3HashAsync()
.SafeFireAndForget(ex =>
{
Logger.Warn(ex, "Error getting hash for image {Path}", localFile.Name);
notificationService.ShowPersistent(
$"Error getting hash for image {localFile.Name}",
$"{ex.GetType().Name}: {ex.Message}"
);
});
}
}

12
StabilityMatrix.Avalonia/ViewModels/Inference/StackEditableCardViewModel.cs

@ -5,6 +5,7 @@ using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
@ -16,7 +17,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(StackEditableCard))]
[ManagedService]
[Transient]
public partial class StackEditableCardViewModel : StackViewModelBase
public partial class StackEditableCardViewModel : StackViewModelBase, IComfyStep
{
private readonly ServiceManager<ViewModelBase> vmFactory;
@ -68,6 +69,15 @@ public partial class StackEditableCardViewModel : StackViewModelBase
}
}
/// <inheritdoc />
public void ApplyStep(ModuleApplyStepEventArgs e)
{
foreach (var module in Cards.OfType<IComfyStep>())
{
module.ApplyStep(e);
}
}
/// <inheritdoc />
protected override void OnCardAdded(LoadableViewModelBase item)
{

31
StabilityMatrix.Avalonia/ViewModels/Inference/UpscalerCardViewModel.cs

@ -27,8 +27,6 @@ public partial class UpscalerCardViewModel : LoadableViewModelBase
public const string ModuleKey = "Upscaler";
private readonly INotificationService notificationService;
private readonly ITrackedDownloadService trackedDownloadService;
private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> vmFactory;
[ObservableProperty]
@ -42,14 +40,10 @@ public partial class UpscalerCardViewModel : LoadableViewModelBase
public UpscalerCardViewModel(
IInferenceClientManager clientManager,
INotificationService notificationService,
ITrackedDownloadService trackedDownloadService,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> vmFactory
)
{
this.notificationService = notificationService;
this.trackedDownloadService = trackedDownloadService;
this.settingsManager = settingsManager;
this.vmFactory = vmFactory;
ClientManager = clientManager;
@ -61,31 +55,14 @@ public partial class UpscalerCardViewModel : LoadableViewModelBase
if (upscaler?.DownloadableResource is not { } resource)
return;
var sharedFolderType =
resource.ContextType as SharedFolderType?
?? throw new InvalidOperationException("ContextType is not SharedFolderType");
var confirmDialog = vmFactory.Get<DownloadResourceViewModel>();
confirmDialog.Resource = resource;
confirmDialog.FileName = upscaler.Value.Name;
if (await confirmDialog.GetDialog().ShowAsync() != ContentDialogResult.Primary)
if (await confirmDialog.GetDialog().ShowAsync() == ContentDialogResult.Primary)
{
return;
confirmDialog.StartDownload();
}
var modelsDir = new DirectoryPath(settingsManager.ModelsDirectory).JoinDir(
sharedFolderType.GetStringValue()
);
var download = trackedDownloadService.NewDownload(
resource.Url,
modelsDir.JoinFile(upscaler.Value.Name)
);
download.ContextAction = new ModelPostDownloadContextAction();
download.Start();
EventManager.Instance.OnToggleProgressFlyout();
}
/// <inheritdoc />
@ -100,8 +77,6 @@ public partial class UpscalerCardViewModel : LoadableViewModelBase
/// <inheritdoc />
public override JsonObject SaveStateToJsonObject()
{
return SerializeModel(
new UpscalerCardModel { Scale = Scale, SelectedUpscaler = SelectedUpscaler }
);
return SerializeModel(new UpscalerCardModel { Scale = Scale, SelectedUpscaler = SelectedUpscaler });
}
}

36
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

@ -181,6 +181,26 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
}
}
protected override Task OnInitialLoadedAsync()
{
if (string.IsNullOrWhiteSpace(Program.Args.LaunchPackageName))
return base.OnInitialLoadedAsync();
var package = InstalledPackages.FirstOrDefault(x => x.DisplayName == Program.Args.LaunchPackageName);
if (package is not null)
{
SelectedPackage = package;
return LaunchAsync();
}
package = InstalledPackages.FirstOrDefault(x => x.Id.ToString() == Program.Args.LaunchPackageName);
if (package is null)
return base.OnInitialLoadedAsync();
SelectedPackage = package;
return LaunchAsync();
}
public override void OnLoaded()
{
// Load installed packages
@ -203,10 +223,12 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
// Load active package
SelectedPackage = settingsManager.Settings.ActiveInstalledPackage;
AutoScrollToEnd = settingsManager.Settings.AutoScrollLaunchConsoleToEnd;
base.OnLoaded();
}
[RelayCommand]
private async Task LaunchAsync(string? command = null)
public async Task LaunchAsync(string? command = null)
{
await notificationService.TryAsync(LaunchImpl(command));
}
@ -272,13 +294,19 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
settingsManager.SaveLaunchArgs(activeInstall.Id, args);
}
await pyRunner.Initialize();
if (basePackage is not StableSwarm)
{
await pyRunner.Initialize();
}
// Get path from package
var packagePath = new DirectoryPath(settingsManager.LibraryDir, activeInstall.LibraryPath!);
// Unpack sitecustomize.py to venv
await UnpackSiteCustomize(packagePath.JoinDir("venv"));
if (basePackage is not StableSwarm)
{
// Unpack sitecustomize.py to venv
await UnpackSiteCustomize(packagePath.JoinDir("venv"));
}
basePackage.Exited += OnProcessExited;
basePackage.StartupComplete += RunningPackageOnStartupComplete;

6
StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallBrowserViewModel.cs

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
@ -25,6 +26,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.PackageManager;
[Transient, ManagedService]
public partial class PackageInstallBrowserViewModel : PageViewModelBase
{
private readonly IPackageFactory packageFactory;
private readonly INavigationService<NewPackageManagerViewModel> packageNavigationService;
private readonly ISettingsManager settingsManager;
private readonly INotificationService notificationService;
@ -56,6 +58,7 @@ public partial class PackageInstallBrowserViewModel : PageViewModelBase
IPrerequisiteHelper prerequisiteHelper
)
{
this.packageFactory = packageFactory;
this.packageNavigationService = packageNavigationService;
this.settingsManager = settingsManager;
this.notificationService = notificationService;
@ -127,7 +130,8 @@ public partial class PackageInstallBrowserViewModel : PageViewModelBase
logger,
pyRunner,
prerequisiteHelper,
packageNavigationService
packageNavigationService,
packageFactory
);
Dispatcher.UIThread.Post(

47
StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs

@ -11,6 +11,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
@ -18,6 +19,7 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
@ -38,7 +40,8 @@ public partial class PackageInstallDetailViewModel(
ILogger<PackageInstallDetailViewModel> logger,
IPyRunner pyRunner,
IPrerequisiteHelper prerequisiteHelper,
INavigationService<NewPackageManagerViewModel> packageNavigationService
INavigationService<NewPackageManagerViewModel> packageNavigationService,
IPackageFactory packageFactory
) : PageViewModelBase
{
public BasePackage SelectedPackage { get; } = package;
@ -126,6 +129,48 @@ public partial class PackageInstallDetailViewModel(
return;
}
if (SelectedPackage is StableSwarm)
{
var comfy = settingsManager.Settings.InstalledPackages.FirstOrDefault(
x => x.PackageName == nameof(ComfyUI)
);
if (comfy == null)
{
// show dialog to install comfy
var dialog = new BetterContentDialog
{
Title = Resources.Label_ComfyRequiredTitle,
Content = Resources.Label_ComfyRequiredDetail,
PrimaryButtonText = Resources.Action_Yes,
CloseButtonText = Resources.Label_No,
DefaultButton = ContentDialogButton.Primary
};
var result = await dialog.ShowAsync();
if (result != ContentDialogResult.Primary)
return;
packageNavigationService.GoBack();
var comfyPackage = packageFactory.FindPackageByName(nameof(ComfyUI));
if (comfyPackage is null)
return;
var vm = new PackageInstallDetailViewModel(
comfyPackage,
settingsManager,
notificationService,
logger,
pyRunner,
prerequisiteHelper,
packageNavigationService,
packageFactory
);
packageNavigationService.NavigateTo(vm);
return;
}
}
InstallName = InstallName.Trim();
var setPackageInstallingStep = new SetPackageInstallingStep(settingsManager, InstallName);

2
StabilityMatrix.Avalonia/Views/Dialogs/DownloadResourceDialog.axaml

@ -20,7 +20,7 @@
<ui:SettingsExpander
Command="{Binding OpenInfoUrlCommand}"
Header="{Binding FileNameWithHash}"
Description="{Binding Resource.ByAuthor}"
Description="{Binding Resource.Author, StringFormat={}by {0}}"
Footer="{Binding FileSizeText}">
</ui:SettingsExpander>

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

@ -59,7 +59,7 @@
<!-- Tag is not used but sets TemplateKey which is used to select the DataTemplate later -->
<ContentPresenter Grid.Row="0"
Tag="{Binding TemplateKeyAsync^}"
DataContext="{Binding ImageSource}"
DataContext="{Binding ImageSource}"
Content="{Binding }">
<ContentPresenter.ContentTemplate>
<controls:DataTemplateSelector x:TypeArguments="models:ImageSourceTemplateType">
@ -72,6 +72,7 @@
<DataTemplate x:Key="{x:Static models:ImageSourceTemplateType.Image}" DataType="models:ImageSource">
<controls:AdvancedImageBox
Name="ImageBox"
Focusable="False"
CornerRadius="4"
Image="{Binding BitmapAsync^}"
SizeMode="Fit">

58
StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml

@ -21,7 +21,7 @@
Margin="8"
MinHeight="450"
MinWidth="700"
RowDefinitions="Auto, *, Auto"
RowDefinitions="Auto, Auto, *, Auto"
ColumnDefinitions="*,Auto,*">
<Grid.Resources>
@ -86,8 +86,12 @@
</StackPanel>
</Grid>
<Border Grid.Row="0" Grid.Column="1"
MaxHeight="360"
Background="#33000000"
CornerRadius="4"/>
<ListBox Grid.Row="0" Grid.Column="1"
Margin="8"
Margin="0, 8, 0, 8"
MaxHeight="360"
VerticalAlignment="Top"
ItemsSource="{Binding Versions}"
@ -120,15 +124,22 @@
<ListBox.Template>
<ControlTemplate>
<ScrollViewer>
<ItemsPresenter />
<ItemsPresenter Margin="8" />
</ScrollViewer>
</ControlTemplate>
</ListBox.Template>
</ListBox>
<Border Grid.Row="0" Grid.Column="2"
MaxHeight="360"
Background="#33000000"
Margin="8,0"
CornerRadius="4"/>
<ListBox Grid.Row="0" Grid.Column="2"
MaxHeight="360"
Margin="8,8,8,0"
VerticalAlignment="Top"
Background="#AA000000"
ItemsSource="{Binding SelectedVersionViewModel.CivitFileViewModels}"
SelectedItem="{Binding SelectedFile}">
<ListBox.Template>
@ -179,20 +190,53 @@
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Margin="0,8"
Orientation="Vertical"
HorizontalAlignment="Center"
IsVisible="{Binding !SelectedFile.IsInstalled}">
<TextBlock Text="{x:Static lang:Resources.Label_SelectDownloadLocation}"
TextAlignment="Center"
HorizontalAlignment="Center"
Margin="0,4"/>
<ComboBox ItemsSource="{Binding AvailableInstallLocations}"
SelectedItem="{Binding SelectedInstallLocation}"
HorizontalAlignment="Center"
MinWidth="300"/>
<StackPanel Orientation="Horizontal"
Margin="0,4"
HorizontalAlignment="Center"
IsVisible="{Binding IsCustomSelected}">
<TextBox Text="{Binding CustomInstallLocation}"
MinWidth="260"/>
<Button Margin="4,0,0,0"
VerticalAlignment="Stretch"
Command="{Binding SelectCustomFolder}">
<controls1:SymbolIcon Symbol="OpenFolder" />
</Button>
</StackPanel>
<TextBlock Text="{x:Static lang:Resources.Error_PleaseSelectDownloadLocation}"
IsVisible="{Binding ShowEmptyPathWarning}"
Foreground="{DynamicResource ThemeRedColor}"
TextAlignment="Center"/>
</StackPanel>
<Expander Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="1"
Grid.Row="2"
Header="{x:Static lang:Resources.Label_ModelDescription}"
ExpandDirection="Down"
Margin="8, 16">
Margin="8, 8">
<ScrollViewer MaxHeight="200">
<TextBlock Text="{Binding Description}"
TextWrapping="Wrap"
Margin="4"/>
</ScrollViewer>
</Expander>
<StackPanel Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="2"
<StackPanel Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="3"
Orientation="Horizontal"
Margin="0,8,0,0"
HorizontalAlignment="Center">

10
StabilityMatrix.Core/Api/ApiFactory.cs

@ -6,7 +6,7 @@ public class ApiFactory : IApiFactory
{
private readonly IHttpClientFactory httpClientFactory;
public RefitSettings? RefitSettings { get; init; }
public ApiFactory(IHttpClientFactory httpClientFactory)
{
this.httpClientFactory = httpClientFactory;
@ -18,4 +18,12 @@ public class ApiFactory : IApiFactory
httpClient.BaseAddress = baseAddress;
return RestService.For<T>(httpClient, RefitSettings);
}
public T CreateRefitClient<T>(Uri baseAddress, RefitSettings refitSettings)
{
var httpClient = httpClientFactory.CreateClient(nameof(T));
httpClient.BaseAddress = baseAddress;
return RestService.For<T>(httpClient, refitSettings);
}
}

6
StabilityMatrix.Core/Api/IApiFactory.cs

@ -1,6 +1,10 @@
namespace StabilityMatrix.Core.Api;
using Refit;
namespace StabilityMatrix.Core.Api;
public interface IApiFactory
{
public T CreateRefitClient<T>(Uri baseAddress);
public T CreateRefitClient<T>(Uri baseAddress, RefitSettings refitSettings);
}

14
StabilityMatrix.Core/Attributes/TypedNodeOptionsAttribute.cs

@ -0,0 +1,14 @@
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Core.Attributes;
/// <summary>
/// Options for <see cref="ComfyTypedNodeBase{TOutput}"/>
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class TypedNodeOptionsAttribute : Attribute
{
public string? Name { get; init; }
public string[]? RequiredExtensions { get; init; }
}

49
StabilityMatrix.Core/Converters/Json/NodeConnectionBaseJsonConverter.cs

@ -0,0 +1,49 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
namespace StabilityMatrix.Core.Converters.Json;
public class NodeConnectionBaseJsonConverter : JsonConverter<NodeConnectionBase>
{
/// <inheritdoc />
public override NodeConnectionBase Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options
)
{
// Read as Data array
reader.Read();
var data = new object[2];
reader.Read();
data[0] = reader.GetString() ?? throw new JsonException("Expected string for node name");
reader.Read();
data[1] = reader.GetInt32();
reader.Read();
if (Activator.CreateInstance(typeToConvert) is not NodeConnectionBase instance)
{
throw new JsonException($"Failed to create instance of {typeToConvert}");
}
var propertyInfo =
typeToConvert.GetProperty("Data", BindingFlags.Public | BindingFlags.Instance)
?? throw new JsonException($"Failed to get Data property of {typeToConvert}");
propertyInfo.SetValue(instance, data);
return instance;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, NodeConnectionBase value, JsonSerializerOptions options)
{
// Write as Data array
writer.WriteStartArray();
writer.WriteStringValue(value.Data?[0] as string);
writer.WriteNumberValue((int)value.Data?[1]!);
writer.WriteEndArray();
}
}

63
StabilityMatrix.Core/Converters/Json/OneOfJsonConverter.cs

@ -0,0 +1,63 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using OneOf;
namespace StabilityMatrix.Core.Converters.Json;
public class OneOfJsonConverter<T1, T2> : JsonConverter<OneOf<T1, T2>>
{
/// <inheritdoc />
public override OneOf<T1, T2> Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options
)
{
// Not sure how else to do this without polymorphic type markers but that would not serialize into T1/T2
// So just try to deserialize T1, if it fails, try T2
Exception? t1Exception = null;
Exception? t2Exception = null;
try
{
if (JsonSerializer.Deserialize<T1>(ref reader, options) is { } t1)
{
return t1;
}
}
catch (JsonException e)
{
t1Exception = e;
}
try
{
if (JsonSerializer.Deserialize<T2>(ref reader, options) is { } t2)
{
return t2;
}
}
catch (JsonException e)
{
t2Exception = e;
}
throw new JsonException(
$"Failed to deserialize OneOf<{typeof(T1)}, {typeof(T2)}> as either {typeof(T1)} or {typeof(T2)}",
new AggregateException([t1Exception, t2Exception])
);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, OneOf<T1, T2> value, JsonSerializerOptions options)
{
if (value.IsT0)
{
JsonSerializer.Serialize(writer, value.AsT0, options);
}
else
{
JsonSerializer.Serialize(writer, value.AsT1, options);
}
}
}

13
StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs

@ -1,4 +1,5 @@
using System.Runtime.Versioning;
using System.Diagnostics;
using System.Runtime.Versioning;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
@ -167,4 +168,14 @@ public interface IPrerequisiteHelper
List<PackagePrerequisite> prerequisites,
IProgress<ProgressReport>? progress = null
);
Task InstallDotnetIfNecessary(IProgress<ProgressReport>? progress = null);
Task<Process> RunDotnet(
ProcessArgs args,
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null,
IReadOnlyDictionary<string, string>? envVars = null,
bool waitForExit = true
);
}

22
StabilityMatrix.Core/Helper/RemoteModels.cs

@ -132,8 +132,7 @@ public static class RemoteModels
}
};
private static Uri ControlNetRoot { get; } =
new("https://huggingface.co/lllyasviel/ControlNet/");
private static Uri ControlNetRoot { get; } = new("https://huggingface.co/lllyasviel/ControlNet/");
private static RemoteResource ControlNetCommon(string path, string sha256)
{
@ -170,4 +169,23 @@ public static class RemoteModels
public static IReadOnlyList<HybridModelFile> ControlNetModels { get; } =
ControlNets.Select(HybridModelFile.FromDownloadable).ToImmutableArray();
private static IEnumerable<RemoteResource> PromptExpansions =>
[
new RemoteResource
{
Url = new Uri("https://cdn.lykos.ai/models/GPT-Prompt-Expansion-Fooocus-v2.zip"),
HashSha256 = "82e69311787c0bb6736389710d80c0a2b653ed9bbe6ea6e70c6b90820fe42d88",
InfoUrl = new Uri("https://huggingface.co/LykosAI/GPT-Prompt-Expansion-Fooocus-v2"),
Author = "lllyasviel, LykosAI",
LicenseType = "GPLv3",
LicenseUrl = new Uri("https://github.com/lllyasviel/Fooocus/blob/main/LICENSE"),
ContextType = SharedFolderType.PromptExpansion,
AutoExtractArchive = true,
ExtractRelativePath = "GPT-Prompt-Expansion-Fooocus-v2"
}
];
public static IEnumerable<HybridModelFile> PromptExpansionModels =>
PromptExpansions.Select(HybridModelFile.FromDownloadable);
}

107
StabilityMatrix.Core/Inference/ComfyClient.cs

@ -6,10 +6,13 @@ using NLog;
using Polly.Contrib.WaitAndRetry;
using Refit;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Converters.Json;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using StabilityMatrix.Core.Models.Api.Comfy.WebSocketData;
using StabilityMatrix.Core.Models.FileInterfaces;
using Websocket.Client;
@ -26,8 +29,16 @@ public class ComfyClient : InferenceClientBase
private readonly IComfyApi comfyApi;
private bool isDisposed;
private JsonSerializerOptions jsonSerializerOptions =
new() { PropertyNamingPolicy = JsonNamingPolicies.SnakeCaseLower, };
private readonly JsonSerializerOptions jsonSerializerOptions =
new()
{
PropertyNamingPolicy = JsonNamingPolicies.SnakeCaseLower,
Converters =
{
new NodeConnectionBaseJsonConverter(),
new OneOfJsonConverter<string, StringNodeConnection>()
}
};
// ReSharper disable once MemberCanBePrivate.Global
public string ClientId { get; } = Guid.NewGuid().ToString();
@ -35,14 +46,24 @@ public class ComfyClient : InferenceClientBase
public Uri BaseAddress { get; }
/// <summary>
/// Optional local path to output images.
/// If available, the local path to the server root directory.
/// </summary>
public DirectoryPath? OutputImagesDir { get; set; }
public DirectoryPath? LocalServerPath { get; set; }
/// <summary>
/// Optional local path to input images.
/// If available, the local server package pair
/// </summary>
public DirectoryPath? InputImagesDir { get; set; }
public PackagePair? LocalServerPackage { get; set; }
/// <summary>
/// Path to the "output" folder from LocalServerPath
/// </summary>
public DirectoryPath? OutputImagesDir => LocalServerPath?.JoinDir("output");
/// <summary>
/// Path to the "input" folder from LocalServerPath
/// </summary>
public DirectoryPath? InputImagesDir => LocalServerPath?.JoinDir("input");
/// <summary>
/// Dictionary of ongoing prompt execution tasks
@ -76,7 +97,13 @@ public class ComfyClient : InferenceClientBase
public ComfyClient(IApiFactory apiFactory, Uri baseAddress)
{
comfyApi = apiFactory.CreateRefitClient<IComfyApi>(baseAddress);
comfyApi = apiFactory.CreateRefitClient<IComfyApi>(
baseAddress,
new RefitSettings
{
ContentSerializer = new SystemTextJsonContentSerializer(jsonSerializerOptions),
}
);
BaseAddress = baseAddress;
// Setup websocket client
@ -144,9 +171,7 @@ public class ComfyClient : InferenceClientBase
if (json.Type == ComfyWebSocketResponseType.Executing)
{
var executingData = json.GetDataAsType<ComfyWebSocketExecutingData>(
jsonSerializerOptions
);
var executingData = json.GetDataAsType<ComfyWebSocketExecutingData>(jsonSerializerOptions);
if (executingData?.PromptId is null)
{
Logger.Warn($"Could not parse executing data {json.Data}, skipping");
@ -165,9 +190,7 @@ public class ComfyClient : InferenceClientBase
}
else
{
Logger.Warn(
$"Could not find task for prompt {executingData.PromptId}, skipping"
);
Logger.Warn($"Could not find task for prompt {executingData.PromptId}, skipping");
}
}
// Otherwise set the task's active node to the one received
@ -194,9 +217,7 @@ public class ComfyClient : InferenceClientBase
}
else if (json.Type == ComfyWebSocketResponseType.Progress)
{
var progressData = json.GetDataAsType<ComfyWebSocketProgressData>(
jsonSerializerOptions
);
var progressData = json.GetDataAsType<ComfyWebSocketProgressData>(jsonSerializerOptions);
if (progressData is null)
{
Logger.Warn($"Could not parse progress data {json.Data}, skipping");
@ -224,11 +245,7 @@ public class ComfyClient : InferenceClientBase
{
task.RunningNode = null;
task.SetException(
new ComfyNodeException
{
ErrorData = errorData,
JsonData = json.Data.ToString()
}
new ComfyNodeException { ErrorData = errorData, JsonData = json.Data.ToString() }
);
currentPromptTask = null;
}
@ -306,9 +323,7 @@ public class ComfyClient : InferenceClientBase
public override async Task CloseAsync(CancellationToken cancellationToken = default)
{
await webSocketClient
.Stop(WebSocketCloseStatus.NormalClosure, string.Empty)
.ConfigureAwait(false);
await webSocketClient.Stop(WebSocketCloseStatus.NormalClosure, string.Empty).ConfigureAwait(false);
}
public async Task<ComfyTask> QueuePromptAsync(
@ -349,13 +364,39 @@ public class ComfyClient : InferenceClientBase
)
{
var streamPart = new StreamPart(image, fileName);
return comfyApi.PostUploadImage(
streamPart,
"true",
"input",
"Inference",
cancellationToken
);
return comfyApi.PostUploadImage(streamPart, "true", "input", "Inference", cancellationToken);
}
/// <summary>
/// Upload a file to the server at the given relative path from server's root
/// </summary>
public async Task UploadFileAsync(
string sourcePath,
string destinationRelativePath,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
// Currently there is no api, so we do a local file copy
if (LocalServerPath is null)
{
throw new InvalidOperationException("LocalServerPath is not set");
}
var sourceFile = new FilePath(sourcePath);
var destFile = LocalServerPath.JoinFile(destinationRelativePath);
Logger.Info("Copying file from {Source} to {Dest}", sourcePath, destFile);
if (!sourceFile.Exists)
{
throw new FileNotFoundException("Source file does not exist", sourcePath);
}
destFile.Directory?.Create();
await sourceFile.CopyToAsync(destFile, true).ConfigureAwait(false);
}
public async Task<Dictionary<string, List<ComfyImage>?>> GetImagesForExecutedPromptAsync(
@ -413,9 +454,7 @@ public class ComfyClient : InferenceClientBase
CancellationToken cancellationToken = default
)
{
var response = await comfyApi
.GetObjectInfo(nodeName, cancellationToken)
.ConfigureAwait(false);
var response = await comfyApi.GetObjectInfo(nodeName, cancellationToken).ConfigureAwait(false);
var info = response[nodeName];
return info.Input.GetRequiredValueAsNestedList(optionName);

12
StabilityMatrix.Core/Models/Api/CivitBaseModelType.cs

@ -26,6 +26,18 @@ public enum CivitBaseModelType
[StringValue("SDXL 1.0 LCM")]
Sdxl10Lcm,
[StringValue("SDXL Distilled")]
SdxlDistilled,
[StringValue("SDXL Lightning")]
SdxlLightning,
[StringValue("SVD")]
SVD,
[StringValue("Stable Cascade")]
StableCascade,
[StringValue("SDXL Turbo")]
SdxlTurbo,
Other,

18
StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnectionBase.cs

@ -1,12 +1,14 @@
namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using System.Text.Json.Serialization;
using StabilityMatrix.Core.Converters.Json;
namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
[JsonConverter(typeof(NodeConnectionBaseJsonConverter))]
public abstract class NodeConnectionBase
{
public object[]? Data { get; set; }
// Implicit conversion to object[]
public static implicit operator object[](NodeConnectionBase nodeConnection)
{
return nodeConnection.Data ?? Array.Empty<object>();
}
/// <summary>
/// Array data for the connection.
/// [(string) Node Name, (int) Connection Index]
/// </summary>
public object[]? Data { get; init; }
}

26
StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs

@ -1,25 +1,27 @@
namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
public class LatentNodeConnection : NodeConnectionBase { }
public class LatentNodeConnection : NodeConnectionBase;
public class VAENodeConnection : NodeConnectionBase { }
public class VAENodeConnection : NodeConnectionBase;
public class ImageNodeConnection : NodeConnectionBase { }
public class ImageNodeConnection : NodeConnectionBase;
public class ImageMaskConnection : NodeConnectionBase { }
public class ImageMaskConnection : NodeConnectionBase;
public class UpscaleModelNodeConnection : NodeConnectionBase { }
public class UpscaleModelNodeConnection : NodeConnectionBase;
public class ModelNodeConnection : NodeConnectionBase { }
public class ModelNodeConnection : NodeConnectionBase;
public class ConditioningNodeConnection : NodeConnectionBase { }
public class ConditioningNodeConnection : NodeConnectionBase;
public class ClipNodeConnection : NodeConnectionBase { }
public class ClipNodeConnection : NodeConnectionBase;
public class ControlNetNodeConnection : NodeConnectionBase { }
public class ControlNetNodeConnection : NodeConnectionBase;
public class ClipVisionNodeConnection : NodeConnectionBase { }
public class ClipVisionNodeConnection : NodeConnectionBase;
public class SamplerNodeConnection : NodeConnectionBase { }
public class SamplerNodeConnection : NodeConnectionBase;
public class SigmasNodeConnection : NodeConnectionBase { }
public class SigmasNodeConnection : NodeConnectionBase;
public class StringNodeConnection : NodeConnectionBase;

44
StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs

@ -1,8 +1,8 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using OneOf;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
@ -14,6 +14,7 @@ namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes;
/// Builder functions for comfy nodes
/// </summary>
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
[Localizable(false)]
public class ComfyNodeBuilder
{
public NodeDictionary Nodes { get; } = new();
@ -226,6 +227,13 @@ public class ComfyNodeBuilder
};
}
public record CheckpointLoader
: ComfyTypedNodeBase<ModelNodeConnection, ClipNodeConnection, VAENodeConnection>
{
public required string ConfigName { get; init; }
public required string CkptName { get; init; }
}
public record CheckpointLoaderSimple
: ComfyTypedNodeBase<ModelNodeConnection, ClipNodeConnection, VAENodeConnection>
{
@ -251,20 +259,7 @@ public class ComfyNodeBuilder
public record CLIPTextEncode : ComfyTypedNodeBase<ConditioningNodeConnection>
{
public required ClipNodeConnection Clip { get; init; }
public required string Text { get; init; }
}
public static NamedComfyNode<ConditioningNodeConnection> ClipTextEncode(
string name,
ClipNodeConnection clip,
string text
)
{
return new NamedComfyNode<ConditioningNodeConnection>(name)
{
ClassType = "CLIPTextEncode",
Inputs = new Dictionary<string, object?> { ["clip"] = clip.Data, ["text"] = text }
};
public required OneOf<string, StringNodeConnection> Text { get; init; }
}
public record LoadImage : ComfyTypedNodeBase<ImageNodeConnection, ImageMaskConnection>
@ -335,6 +330,18 @@ public class ComfyNodeBuilder
public required string Method { get; init; }
}
[TypedNodeOptions(
Name = "Inference_Core_PromptExpansion",
RequiredExtensions = ["https://github.com/LykosAI/ComfyUI-Inference-Core-Nodes"]
)]
public record PromptExpansion : ComfyTypedNodeBase<StringNodeConnection>
{
public required string ModelName { get; init; }
public required OneOf<string, StringNodeConnection> Text { get; init; }
public required ulong Seed { get; init; }
public bool LogPrompt { get; init; }
}
public ImageNodeConnection Lambda_LatentToImage(LatentNodeConnection latent, VAENodeConnection vae)
{
var name = GetUniqueName("VAEDecode");
@ -794,6 +801,9 @@ public class ComfyNodeBuilder
public int BatchSize { get; set; } = 1;
public int? BatchIndex { get; set; }
public OneOf<string, StringNodeConnection> PositivePrompt { get; set; }
public OneOf<string, StringNodeConnection> NegativePrompt { get; set; }
public ClipNodeConnection? BaseClip { get; set; }
public ClipVisionNodeConnection? BaseClipVision { get; set; }

25
StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyTypedNodeBase.cs

@ -1,4 +1,5 @@
using System.Reflection;
using System.ComponentModel;
using System.Reflection;
using System.Text.Json.Serialization;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
@ -8,8 +9,28 @@ namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes;
public abstract record ComfyTypedNodeBase
{
protected virtual string ClassType => GetType().Name;
[Localizable(false)]
protected virtual string ClassType
{
get
{
var type = GetType();
// Use options name if available
if (type.GetCustomAttribute<TypedNodeOptionsAttribute>() is { } options)
{
if (!string.IsNullOrEmpty(options.Name))
{
return options.Name;
}
}
// Otherwise use class name
return type.Name;
}
}
[Localizable(false)]
[JsonIgnore]
public required string Name { get; init; }

33
StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs

@ -1,4 +1,9 @@
using StabilityMatrix.Core.Helper;
using System.ComponentModel;
using System.Reflection;
using System.Text.Json.Serialization;
using OneOf;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes;
@ -10,10 +15,16 @@ public class NodeDictionary : Dictionary<string, ComfyNode>
/// </summary>
private readonly Dictionary<string, int> _baseNameIndex = new();
/// <summary>
/// When inserting TypedNodes, this holds a mapping of ClassType to required extensions
/// </summary>
[JsonIgnore]
public Dictionary<string, string[]> ClassTypeRequiredExtensions { get; } = new();
/// <summary>
/// Finds a unique node name given a base name, by appending _2, _3, etc.
/// </summary>
public string GetUniqueName(string nameBase)
public string GetUniqueName([Localizable(false)] string nameBase)
{
if (_baseNameIndex.TryGetValue(nameBase, out var index))
{
@ -43,7 +54,19 @@ public class NodeDictionary : Dictionary<string, ComfyNode>
public TTypedNode AddTypedNode<TTypedNode>(TTypedNode node)
where TTypedNode : ComfyTypedNodeBase
{
Add(node.Name, node);
var namedNode = (NamedComfyNode)node;
Add(node.Name, namedNode);
// Check statically annotated stuff for TypedNodeOptionsAttribute
if (node.GetType().GetCustomAttribute<TypedNodeOptionsAttribute>() is { } options)
{
if (options.RequiredExtensions != null)
{
ClassTypeRequiredExtensions[namedNode.ClassType] = options.RequiredExtensions;
}
}
return node;
}
@ -62,6 +85,10 @@ public class NodeDictionary : Dictionary<string, ComfyNode>
{
node.Inputs[key] = connection.Data;
}
else if (input is IOneOf { Value: NodeConnectionBase oneOfConnection })
{
node.Inputs[key] = oneOfConnection.Data;
}
}
}
}

31
StabilityMatrix.Core/Models/Database/LocalModelFile.cs

@ -34,6 +34,11 @@ public record LocalModelFile
/// </summary>
public string? PreviewImageFullPath { get; set; }
/// <summary>
/// Optional full path to the model's configuration (.yaml) file.
/// </summary>
public string? ConfigFullPath { get; set; }
/// <summary>
/// Whether or not an update is available for this model
/// </summary>
@ -88,6 +93,9 @@ public record LocalModelFile
[BsonIgnore]
public string DisplayModelFileName => FileName;
[BsonIgnore]
public string DisplayConfigFileName => Path.GetFileName(ConfigFullPath) ?? string.Empty;
public string GetFullPath(string rootModelDirectory)
{
return Path.Combine(rootModelDirectory, RelativePath);
@ -103,29 +111,6 @@ public record LocalModelFile
: Path.Combine(rootModelDirectory, PreviewImageRelativePath);
}
/*protected bool Equals(LocalModelFile other)
{
return RelativePath == other.RelativePath;
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj))
return false;
if (ReferenceEquals(this, obj))
return true;
if (obj.GetType() != this.GetType())
return false;
return Equals((LocalModelFile)obj);
}
/// <inheritdoc />
public override int GetHashCode()
{
return RelativePath.GetHashCode();
}*/
public static readonly HashSet<string> SupportedCheckpointExtensions =
[
".safetensors",

58
StabilityMatrix.Core/Models/FDS/ComfyUiSelfStartSettings.cs

@ -0,0 +1,58 @@
// The MIT License (MIT)
//
// Copyright (c) 2024 Stability AI
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// from https://github.com/Stability-AI/StableSwarmUI/blob/master/src/BuiltinExtensions/ComfyUIBackend/ComfyUISelfStartBackend.cs
using FreneticUtilities.FreneticDataSyntax;
namespace StabilityMatrix.Core.Models.FDS;
public class ComfyUiSelfStartSettings : AutoConfiguration
{
[ConfigComment(
"The location of the 'main.py' file. Can be an absolute or relative path, but must end with 'main.py'.\nIf you used the installer, this should be 'dlbackend/ComfyUI/main.py'."
)]
public string StartScript = "";
[ConfigComment("Any arguments to include in the launch script.")]
public string ExtraArgs = "";
[ConfigComment(
"If unchecked, the system will automatically add some relevant arguments to the comfy launch. If checked, automatic args (other than port) won't be added."
)]
public bool DisableInternalArgs = false;
[ConfigComment("If checked, will automatically keep the comfy backend up to date when launching.")]
public bool AutoUpdate = true;
[ConfigComment(
"If checked, tells Comfy to generate image previews. If unchecked, previews will not be generated, and images won't show up until they're done."
)]
public bool EnablePreviews = true;
[ConfigComment("Which GPU to use, if multiple are available.")]
public int GPU_ID = 0;
[ConfigComment("How many extra requests may queue up on this backend while one is processing.")]
public int OverQueue = 1;
}

380
StabilityMatrix.Core/Models/FDS/StableSwarmSettings.cs

@ -0,0 +1,380 @@
// The MIT License (MIT)
//
// Copyright (c) 2024 Stability AI
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// from https://raw.githubusercontent.com/Stability-AI/StableSwarmUI/master/src/Core/Settings.cs
using FreneticUtilities.FreneticDataSyntax;
namespace StabilityMatrix.Core.Models.FDS;
/// <summary>Central default settings list.</summary>
public class StableSwarmSettings : AutoConfiguration
{
[ConfigComment("Settings related to file paths.")]
public PathsData Paths = new();
[ConfigComment("Settings related to networking and the webserver.")]
public NetworkData Network = new();
[ConfigComment("Restrictions to apply to default users.")]
public UserRestriction DefaultUserRestriction = new();
[ConfigComment(
"Default settings for users (unless the user modifies them, if so permitted).\n(NOTE: Usually, don't edit this. Go to the 'User' tab to edit your User-Settings)."
)]
public User DefaultUser = new();
[ConfigComment("Settings related to backends.")]
public BackendData Backends = new();
[ConfigComment(
"If this is set to 'true', hides the installer page. If 'false', the installer page will be shown."
)]
public bool IsInstalled = false;
[ConfigComment(
"Ratelimit, in milliseconds, between Nvidia GPU status queries. Default is 1000 ms (1 second)."
)]
public long NvidiaQueryRateLimitMS = 1000;
[ConfigComment(
"How to launch the UI. If 'none', just quietly launch.\nIf 'web', launch your web-browser to the page.\nIf 'webinstall', launch web-browser to the install page.\nIf 'electron', launch the UI in an electron window (NOT YET IMPLEMENTED)."
)]
[ManualSettingsOptions(Impl = null, Vals = new string[] { "none", "web", "webinstall", "electron" })]
public string LaunchMode = "webinstall";
[ConfigComment("The minimum tier of logs that should be visible in the console.\nDefault is 'info'.")]
public string LogLevel = "Info";
[ConfigComment("Settings related to the User Interface.")]
public UIData UI = new();
[ConfigComment("Settings related to webhooks.")]
public WebHooksData WebHooks = new();
/// <summary>Settings related to backends.</summary>
public class BackendData : AutoConfiguration
{
[ConfigComment("How many times to retry initializing a backend before giving up. Default is 3.")]
public int MaxBackendInitAttempts = 3;
[ConfigComment(
"Safety check, the maximum duration all requests can be waiting for a backend before the system declares a backend handling failure."
)]
public int MaxTimeoutMinutes = 20;
[ConfigComment(
"The maximum duration an individual request can be waiting on a backend to be available before giving up.\n"
+ "Not to be confused with 'MaxTimeoutMinutes' which requires backends be unresponsive for that duration, this duration includes requests that are merely waiting because other requests are queued."
+ "\nDefaults to 60 * 24 * 7 = 1 week (ultra-long max queue duration)."
)]
public int PerRequestTimeoutMinutes = 60 * 24 * 7;
[ConfigComment(
"The maximum number of pending requests to continue forcing orderly processing of.\nOver this limit, requests may start going out of order."
)]
public int MaxRequestsForcedOrder = 20;
[ConfigComment(
"How many minutes to wait after the last generation before automatically freeing up VRAM (to prevent issues with other programs).\nThis has the downside of a small added bit of time to load back onto VRAM at next usage.\nUse a decimal number to free after seconds.\nDefaults to 10 minutes."
)]
public double ClearVRAMAfterMinutes = 10;
[ConfigComment(
"How many minutes to wait after the last generation before automatically freeing up system RAM (to prevent issues with other programs).\nThis has the downside of causing models to fully load from data drive at next usage.\nUse a decimal number to free after seconds.\nDefaults to 60 minutes (one hour)."
)]
public double ClearSystemRAMAfterMinutes = 60;
}
/// <summary>Settings related to networking and the webserver.</summary>
public class NetworkData : AutoConfiguration
{
[ConfigComment(
"What web host address to use. `localhost` means your PC only."
+ "\nLinux users may use `0.0.0.0` to mean accessible to anyone that can connect to your PC (ie LAN users, or the public if your firewall is open)."
+ "\nWindows users may use `*` for that, though it may require additional Windows firewall configuration."
+ "\nAdvanced server users may wish to manually specify a host bind address here."
)]
public string Host = "localhost";
[ConfigComment("What web port to use. Default is '7801'.")]
public int Port = 7801;
[ConfigComment(
"If true, if the port is already in use, the server will try to find another port to use instead.\nIf false, the server will fail to start if the port is already in use."
)]
public bool PortCanChange = true;
[ConfigComment(
"Backends are automatically assigned unique ports. This value selects which port number to start the assignment from.\nDefault is '7820'."
)]
public int BackendStartingPort = 7820;
}
/// <summary>Settings related to file paths.</summary>
public class PathsData : AutoConfiguration
{
[ConfigComment(
"Root path for model files. Use a full-formed path (starting with '/' or a Windows drive like 'C:') to use an absolute path.\nDefaults to 'Models'."
)]
public string ModelRoot = "Models";
[ConfigComment(
"The model folder to use within 'ModelRoot'.\nDefaults to 'Stable-Diffusion'.\nAbsolute paths work too."
)]
public string SDModelFolder = "Stable-Diffusion";
[ConfigComment(
"The LoRA (or related adapter type) model folder to use within 'ModelRoot'.\nDefaults to 'Lora'.\nAbsolute paths work too."
)]
public string SDLoraFolder = "Lora";
[ConfigComment(
"The VAE (autoencoder) model folder to use within 'ModelRoot'.\nDefaults to 'VAE'.\nAbsolute paths work too."
)]
public string SDVAEFolder = "VAE";
[ConfigComment(
"The Embedding (eg textual inversion) model folder to use within 'ModelRoot'.\nDefaults to 'Embeddings'.\nAbsolute paths work too."
)]
public string SDEmbeddingFolder = "Embeddings";
[ConfigComment(
"The ControlNets model folder to use within 'ModelRoot'.\nDefaults to 'controlnet'.\nAbsolute paths work too."
)]
public string SDControlNetsFolder = "controlnet";
[ConfigComment(
"The CLIP Vision model folder to use within 'ModelRoot'.\nDefaults to 'clip_vision'.\nAbsolute paths work too."
)]
public string SDClipVisionFolder = "clip_vision";
[ConfigComment("Root path for data (user configs, etc).\nDefaults to 'Data'")]
public string DataPath = "Data";
[ConfigComment("Root path for output files (images, etc).\nDefaults to 'Output'")]
public string OutputPath = "Output";
[ConfigComment("The folder for wildcard (.txt) files, under Data.\nDefaults to 'Wildcards'")]
public string WildcardsFolder = "Wildcards";
[ConfigComment(
"When true, output paths always have the username as a folder.\nWhen false, this will be skipped.\nKeep this on in multi-user environments."
)]
public bool AppendUserNameToOutputPath = true;
}
/// <summary>Settings to control restrictions on users.</summary>
public class UserRestriction : AutoConfiguration
{
[ConfigComment("How many directories deep a user's custom OutPath can be.\nDefault is 5.")]
public int MaxOutPathDepth = 5;
[ConfigComment("Which user-settings the user is allowed to modify.\nDefault is all of them.")]
public List<string> AllowedSettings = new() { "*" };
[ConfigComment(
"If true, the user is treated as a full admin.\nThis includes the ability to modify these settings."
)]
public bool Admin = false;
[ConfigComment("If true, user may load models.\nIf false, they may only use already-loaded models.")]
public bool CanChangeModels = true;
[ConfigComment(
"What models are allowed, as a path regex.\nDirectory-separator is always '/'. Can be '.*' for all, 'MyFolder/.*' for only within that folder, etc.\nDefault is all."
)]
public string AllowedModels = ".*";
[ConfigComment("Generic permission flags. '*' means all.\nDefault is all.")]
public List<string> PermissionFlags = new() { "*" };
[ConfigComment("How many images can try to be generating at the same time on this user.")]
public int MaxT2ISimultaneous = 32;
}
/// <summary>Settings per-user.</summary>
public class User : AutoConfiguration
{
public class OutPath : AutoConfiguration
{
[ConfigComment(
"Builder for output file paths. Can use auto-filling placeholders like '[model]' for the model name, '[prompt]' for a snippet of prompt text, etc.\n"
+ "Full details in the docs: https://github.com/Stability-AI/StableSwarmUI/blob/master/docs/User%20Settings.md#path-format"
)]
public string Format = "raw/[year]-[month]-[day]/[hour][minute]-[prompt]-[model]-[seed]";
[ConfigComment("How long any one part can be.\nDefault is 40 characters.")]
public int MaxLenPerPart = 40;
}
[ConfigComment("Settings related to output path building.")]
public OutPath OutPathBuilder = new();
public class FileFormatData : AutoConfiguration
{
[ConfigComment("What format to save images in.\nDefault is '.jpg' (at 100% quality).")]
public string ImageFormat = "JPG";
[ConfigComment("Whether to store metadata into saved images.\nDefaults enabled.")]
public bool SaveMetadata = true;
[ConfigComment(
"If set to non-0, adds DPI metadata to saved images.\n'72' is a good value for compatibility with some external software."
)]
public int DPI = 0;
[ConfigComment(
"If set to true, a '.txt' file will be saved alongside images with the image metadata easily viewable.\nThis can work even if saving in the image is disabled. Defaults disabled."
)]
public bool SaveTextFileMetadata = false;
}
[ConfigComment("Settings related to saved file format.")]
public FileFormatData FileFormat = new();
[ConfigComment("Whether your files save to server data drive or not.")]
public bool SaveFiles = true;
[ConfigComment("If true, folders will be discard from starred image paths.")]
public bool StarNoFolders = false;
[ConfigComment("What theme to use. Default is 'dark_dreams'.")]
public string Theme = "dark_dreams";
[ConfigComment(
"If enabled, batch size will be reset to 1 when parameters are loaded.\nThis can prevent accidents that might thrash your GPU or cause compatibility issues, especially for example when importing a comfy workflow.\nYou can still set the batch size at will in the GUI."
)]
public bool ResetBatchSizeToOne = false;
public enum HintFormatOptions
{
BUTTON,
HOVER,
NONE
}
[ConfigComment("The format for parameter hints to display as.\nDefault is 'BUTTON'.")]
[SettingsOptions(Impl = typeof(SettingsOptionsAttribute.ForEnum<HintFormatOptions>))]
public string HintFormat = "BUTTON";
public class VAEsData : AutoConfiguration
{
[ConfigComment(
"What VAE to use with SDXL models by default. Use 'None' to use the one in the model."
)]
[ManualSettingsOptions(Impl = null, Vals = new string[] { "None" })]
public string DefaultSDXLVAE = "None";
[ConfigComment(
"What VAE to use with SDv1 models by default. Use 'None' to use the one in the model."
)]
[ManualSettingsOptions(Impl = null, Vals = new string[] { "None" })]
public string DefaultSDv1VAE = "None";
}
[ConfigComment("Options to override default VAEs with.")]
public VAEsData VAEs = new();
[ConfigComment(
"When generating live previews, this is how many simultaneous generation requests can be waiting at one time."
)]
public int MaxSimulPreviews = 1;
[ConfigComment(
"Set to a number above 1 to allow generations of multiple images to automatically generate square mini-grids when they're done."
)]
public int MaxImagesInMiniGrid = 1;
[ConfigComment("How many images the history view should stop trying to load after.")]
public int MaxImagesInHistory = 1000;
[ConfigComment(
"If true, the Image History view will cache small preview thumbnails of images.\nThis should make things run faster. You can turn it off if you don't want that."
)]
public bool ImageHistoryUsePreviews = true;
[ConfigComment(
"Delay, in seconds, betweeen Generate Forever updates.\nIf the delay hits and a generation is still waiting, it will be skipped.\nDefault is 0.1 seconds."
)]
public double GenerateForeverDelay = 0.1;
[ConfigComment("What language to display the UI in.\nDefault is 'en' (English).")]
public string Language = "en";
}
/// <summary>UI-related settings.</summary>
public class UIData : AutoConfiguration
{
[ConfigComment(
"Optionally specify a (raw HTML) welcome message here. If specified, will override the automatic welcome messages."
)]
public string OverrideWelcomeMessage = "";
}
/// <summary>Webhook settings.</summary>
public class WebHooksData : AutoConfiguration
{
[ConfigComment(
"Webhook to call (empty JSON POST) when queues are starting up from idle.\nLeave empty to disable any webhook.\nCall must return before the first generation starts."
)]
public string QueueStartWebhook = "";
[ConfigComment(
"Webhook to call (empty JSON POST) when all queues are done and the server is going idle.\nLeave empty to disable any webhook.\nCall must return before queuing may restart."
)]
public string QueueEndWebhook = "";
[ConfigComment(
"How long to wait (in seconds) after all queues are done before sending the queue end webhook.\nThis is useful to prevent rapid start+end calls."
)]
public double QueueEndDelay = 1;
}
}
[AttributeUsage(AttributeTargets.Field)]
public class SettingsOptionsAttribute : Attribute
{
public abstract class AbstractImpl
{
public abstract string[] GetOptions { get; }
}
public class ForEnum<T> : AbstractImpl
where T : Enum
{
public override string[] GetOptions => Enum.GetNames(typeof(T));
}
public Type Impl;
public virtual string[] Options => (Activator.CreateInstance(Impl) as AbstractImpl).GetOptions;
}
[AttributeUsage(AttributeTargets.Field)]
public class ManualSettingsOptionsAttribute : SettingsOptionsAttribute
{
public string[] Vals;
public override string[] Options => Vals;
}

15
StabilityMatrix.Core/Models/FileInterfaces/DirectoryPath.cs

@ -1,4 +1,5 @@
using System.Collections;
using System.ComponentModel;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using StabilityMatrix.Core.Converters.Json;
@ -6,6 +7,7 @@ using StabilityMatrix.Core.Converters.Json;
namespace StabilityMatrix.Core.Models.FileInterfaces;
[PublicAPI]
[Localizable(false)]
[JsonConverter(typeof(StringJsonConverter<DirectoryPath>))]
public class DirectoryPath : FileSystemPath, IPathObject, IEnumerable<FileSystemPath>
{
@ -43,7 +45,7 @@ public class DirectoryPath : FileSystemPath, IPathObject, IEnumerable<FileSystem
[JsonIgnore]
public DirectoryPath? Parent => Info.Parent == null ? null : new DirectoryPath(Info.Parent);
public DirectoryPath(string path)
public DirectoryPath([Localizable(false)] string path)
: base(path) { }
public DirectoryPath(FileSystemPath path)
@ -56,7 +58,7 @@ public class DirectoryPath : FileSystemPath, IPathObject, IEnumerable<FileSystem
this.info = info;
}
public DirectoryPath(params string[] paths)
public DirectoryPath([Localizable(false)] params string[] paths)
: base(paths) { }
public DirectoryPath RelativeTo(DirectoryPath path)
@ -226,13 +228,13 @@ public class DirectoryPath : FileSystemPath, IPathObject, IEnumerable<FileSystem
/// <summary>
/// Join with other paths to form a new directory path.
/// </summary>
public DirectoryPath JoinDir(params DirectoryPath[] paths) =>
public DirectoryPath JoinDir([Localizable(false)] params DirectoryPath[] paths) =>
new(Path.Combine(FullPath, Path.Combine(paths.Select(path => path.FullPath).ToArray())));
/// <summary>
/// Join with other paths to form a new file path.
/// </summary>
public FilePath JoinFile(params FilePath[] paths) =>
public FilePath JoinFile([Localizable(false)] params FilePath[] paths) =>
new(Path.Combine(FullPath, Path.Combine(paths.Select(path => path.FullPath).ToArray())));
/// <summary>
@ -305,12 +307,13 @@ public class DirectoryPath : FileSystemPath, IPathObject, IEnumerable<FileSystem
new(Path.Combine(path, other.FullName));
// DirectoryPath + string = string
public static string operator +(DirectoryPath path, string other) => Path.Combine(path, other);
public static string operator +(DirectoryPath path, [Localizable(false)] string other) =>
Path.Combine(path, other);
// Implicit conversions to and from string
public static implicit operator string(DirectoryPath path) => path.FullPath;
public static implicit operator DirectoryPath(string path) => new(path);
public static implicit operator DirectoryPath([Localizable(false)] string path) => new(path);
// Implicit conversions to and from DirectoryInfo
public static implicit operator DirectoryInfo(DirectoryPath path) => path.Info;

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

@ -1,11 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using System.ComponentModel;
using System.Text;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using StabilityMatrix.Core.Converters.Json;
namespace StabilityMatrix.Core.Models.FileInterfaces;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
[PublicAPI]
[Localizable(false)]
[JsonConverter(typeof(StringJsonConverter<FilePath>))]
public partial class FilePath : FileSystemPath, IPathObject
{
@ -59,7 +61,7 @@ public partial class FilePath : FileSystemPath, IPathObject
}
}
public FilePath(string path)
public FilePath([Localizable(false)] string path)
: base(path) { }
public FilePath(FileInfo fileInfo)
@ -71,7 +73,7 @@ public partial class FilePath : FileSystemPath, IPathObject
public FilePath(FileSystemPath path)
: base(path) { }
public FilePath(params string[] paths)
public FilePath([Localizable(false)] params string[] paths)
: base(paths) { }
public FilePath RelativeTo(DirectoryPath path)
@ -151,7 +153,7 @@ public partial class FilePath : FileSystemPath, IPathObject
/// <summary>
/// Rename the file.
/// </summary>
public FilePath Rename(string fileName)
public FilePath Rename([Localizable(false)] string fileName)
{
if (Path.GetDirectoryName(FullPath) is { } directory && !string.IsNullOrWhiteSpace(directory))
{
@ -245,5 +247,5 @@ public partial class FilePath : FileSystemPath, IPathObject
// Implicit conversions to and from string
public static implicit operator string(FilePath path) => path.FullPath;
public static implicit operator FilePath(string path) => new(path);
public static implicit operator FilePath([Localizable(false)] string path) => new(path);
}

4
StabilityMatrix.Core/Models/FileInterfaces/FileSystemPath.cs

@ -1,9 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
namespace StabilityMatrix.Core.Models.FileInterfaces;
[PublicAPI]
[Localizable(false)]
public class FileSystemPath : IEquatable<FileSystemPath>, IFormattable
{
public string FullPath { get; }

13
StabilityMatrix.Core/Models/HybridModelFile.cs

@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Core.Models;
@ -72,6 +71,7 @@ public record HybridModelFile
if (
!fileName.Equals("diffusion_pytorch_model", StringComparison.OrdinalIgnoreCase)
&& !fileName.Equals("pytorch_model", StringComparison.OrdinalIgnoreCase)
&& !fileName.Equals("ip_adapter", StringComparison.OrdinalIgnoreCase)
)
{
@ -125,9 +125,14 @@ public record HybridModelFile
if (x.GetType() != y.GetType())
return false;
return Equals(x.RelativePath, y.RelativePath)
&& x.IsNone == y.IsNone
&& x.IsDefault == y.IsDefault;
if (!Equals(x.RelativePath, y.RelativePath))
return false;
// This equality affects replacements of remote over local models
// We want local and remote models to be considered equal if they have the same relative path
// But 2 local models with the same path but different config paths should be considered different
return !(x.Type == y.Type && x.Local?.ConfigFullPath != y.Local?.ConfigFullPath);
}
public int GetHashCode(HybridModelFile obj)

5
StabilityMatrix.Core/Models/PackagePrerequisite.cs

@ -6,7 +6,6 @@ public enum PackagePrerequisite
VcRedist,
Git,
Node,
Dotnet7,
Dotnet8,
Tkinter,
Dotnet,
Tkinter
}

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

@ -195,7 +195,9 @@ public class A3WebUI(
var venvPath = Path.Combine(installLocation, "venv");
var exists = Directory.Exists(venvPath);
var venvRunner = await SetupVenv(installLocation, forceRecreate: true).ConfigureAwait(false);
await using var venvRunner = new PyVenvRunner(venvPath);
venvRunner.WorkingDirectory = installLocation;
await venvRunner.Setup(true, onConsoleOutput).ConfigureAwait(false);
await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true));

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

@ -68,6 +68,7 @@ public class ComfyUI(
[SharedFolderType.InvokeIpAdapters15] = new[] { "models/ipadapter/sd15" },
[SharedFolderType.InvokeIpAdaptersXl] = new[] { "models/ipadapter/sdxl" },
[SharedFolderType.T2IAdapter] = new[] { "models/controlnet/T2IAdapter" },
[SharedFolderType.PromptExpansion] = new[] { "models/prompt_expansion" }
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
@ -373,6 +374,7 @@ public class ComfyUI(
Path.Combine(modelsDir, "InvokeIpAdapters15"),
Path.Combine(modelsDir, "InvokeIpAdaptersXl")
);
nodeValue.Children["prompt_expansion"] = Path.Combine(modelsDir, "PromptExpansion");
}
else
{
@ -410,7 +412,8 @@ public class ComfyUI(
Path.Combine(modelsDir, "InvokeIpAdapters15"),
Path.Combine(modelsDir, "InvokeIpAdaptersXl")
)
}
},
{ "prompt_expansion", Path.Combine(modelsDir, "PromptExpansion") }
}
);
}
@ -476,9 +479,8 @@ public class ComfyUI(
public override IEnumerable<ExtensionManifest> DefaultManifests =>
[
new ExtensionManifest(
new Uri("https://cdn.jsdelivr.net/gh/ltdrdata/ComfyUI-Manager/custom-node-list.json")
)
"https://cdn.jsdelivr.net/gh/ltdrdata/ComfyUI-Manager/custom-node-list.json",
"https://cdn.jsdelivr.net/gh/LykosAI/ComfyUI-Extensions-Index/custom-node-list.json"
];
public override async Task<IEnumerable<PackageExtension>> GetManifestExtensionsAsync(

5
StabilityMatrix.Core/Models/Packages/Extensions/ExtensionManifest.cs

@ -1,3 +1,6 @@
namespace StabilityMatrix.Core.Models.Packages.Extensions;
public record ExtensionManifest(Uri Uri);
public record ExtensionManifest(Uri Uri)
{
public static implicit operator ExtensionManifest(string uri) => new(new Uri(uri, UriKind.Absolute));
}

78
StabilityMatrix.Core/Models/Packages/Extensions/GitPackageExtensionManager.cs

@ -1,4 +1,6 @@
using System.Text.RegularExpressions;
using KGySoft.CoreLibraries;
using Microsoft.Extensions.Caching.Memory;
using NLog;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
@ -8,11 +10,14 @@ using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Core.Models.Packages.Extensions;
public abstract class GitPackageExtensionManager(IPrerequisiteHelper prerequisiteHelper)
public abstract partial class GitPackageExtensionManager(IPrerequisiteHelper prerequisiteHelper)
: IPackageExtensionManager
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
// Cache checks of installed extensions
private readonly MemoryCache installedExtensionsCache = new(new MemoryCacheOptions());
public abstract string RelativeInstallDirectory { get; }
public virtual IEnumerable<ExtensionManifest> DefaultManifests { get; } =
@ -128,6 +133,74 @@ public abstract class GitPackageExtensionManager(IPrerequisiteHelper prerequisit
return extensions;
}
/// <summary>
/// Like <see cref="GetInstalledExtensionsAsync"/>, but does not check git version and repository url.
/// </summary>
public virtual async Task<IEnumerable<InstalledPackageExtension>> GetInstalledExtensionsLiteAsync(
InstalledPackage installedPackage,
CancellationToken cancellationToken = default
)
{
if (installedPackage.FullPath is not { } packagePath)
{
return Enumerable.Empty<InstalledPackageExtension>();
}
var extensions = new List<InstalledPackageExtension>();
// Search for installed extensions in the package's index directories.
foreach (
var indexDirectory in IndexRelativeDirectories.Select(
path => new DirectoryPath(packagePath, path)
)
)
{
cancellationToken.ThrowIfCancellationRequested();
// Skip directory if not exists
if (!indexDirectory.Exists)
{
continue;
}
// Check subdirectories of the index directory
foreach (var subDirectory in indexDirectory.EnumerateDirectories())
{
cancellationToken.ThrowIfCancellationRequested();
// Skip if not valid git repository
if (!subDirectory.JoinDir(".git").Exists)
{
continue;
}
// Get remote url with manual parsing
string? remoteUrl = null;
var gitConfigPath = subDirectory.JoinDir(".git").JoinFile("config");
if (
gitConfigPath.Exists
&& await gitConfigPath.ReadAllTextAsync(cancellationToken).ConfigureAwait(false)
is { } gitConfigText
)
{
var pattern = GitConfigRemoteOriginUrlRegex();
var match = pattern.Match(gitConfigText);
if (match.Success)
{
remoteUrl = match.Groups[1].Value;
}
}
extensions.Add(
new InstalledPackageExtension { Paths = [subDirectory], GitRepositoryUrl = remoteUrl }
);
}
}
return extensions;
}
/// <inheritdoc />
public virtual async Task InstallExtensionAsync(
PackageExtension extension,
@ -242,4 +315,7 @@ public abstract class GitPackageExtensionManager(IPrerequisiteHelper prerequisit
progress?.Report(new ProgressReport(1f, message: "Uninstalled extension"));
}
[GeneratedRegex("""\[remote "origin"\][\s\S]*?url\s*=\s*(.+)""")]
private static partial Regex GitConfigRemoteOriginUrlRegex();
}

29
StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs

@ -52,6 +52,35 @@ public interface IPackageExtensionManager
return extensions;
}
/// <summary>
/// Get unique extensions from all provided manifests. As a mapping of their reference.
/// </summary>
async Task<IDictionary<string, PackageExtension>> GetManifestExtensionsMapAsync(
IEnumerable<ExtensionManifest> manifests,
CancellationToken cancellationToken = default
)
{
var result = new Dictionary<string, PackageExtension>();
foreach (
var extension in await GetManifestExtensionsAsync(manifests, cancellationToken)
.ConfigureAwait(false)
)
{
cancellationToken.ThrowIfCancellationRequested();
var key = extension.Reference.ToString();
if (!result.TryAdd(key, extension))
{
// Replace
result[key] = extension;
}
}
return result;
}
/// <summary>
/// Get all installed extensions for the provided package.
/// </summary>

9
StabilityMatrix.Core/Models/Packages/Extensions/VladExtensionItem.cs

@ -0,0 +1,9 @@
namespace StabilityMatrix.Core.Models.Packages.Extensions;
public class VladExtensionItem
{
public required string Name { get; set; }
public required Uri Url { get; set; }
public string? Long { get; set; }
public string? Description { get; set; }
}

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

@ -298,7 +298,7 @@ public class InvokeAI : BaseGitPackage
if (Compat.IsMacOS || Compat.IsLinux)
{
await PrerequisiteHelper
.RunNpm(["i", "vite"], installLocation, envVars: envVars)
.RunNpm(["i", "vite", "--ignore-scripts=true"], installLocation, envVars: envVars)
.ConfigureAwait(false);
}

158
StabilityMatrix.Core/Models/Packages/SDWebForge.cs

@ -0,0 +1,158 @@
using System.Text.Json.Nodes;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Packages.Extensions;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.Packages;
[Singleton(typeof(BasePackage))]
public class SDWebForge(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
) : A3WebUI(githubApi, settingsManager, downloadService, prerequisiteHelper)
{
public override string Name => "stable-diffusion-webui-forge";
public override string DisplayName { get; set; } = "Stable Diffusion WebUI Forge";
public override string Author => "lllyasviel";
public override string Blurb =>
"Stable Diffusion WebUI Forge is a platform on top of Stable Diffusion WebUI (based on Gradio) to make development easier, optimize resource management, and speed up inference.";
public override string LicenseUrl =>
"https://github.com/lllyasviel/stable-diffusion-webui-forge/blob/main/LICENSE.txt";
public override Uri PreviewImageUri =>
new(
"https://github.com/lllyasviel/stable-diffusion-webui-forge/assets/19834515/ca5e05ed-bd86-4ced-8662-f41034648e8c"
);
public override string MainBranch => "main";
public override bool ShouldIgnoreReleases => true;
public override IPackageExtensionManager ExtensionManager => null;
public override string OutputFolderName => "output";
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new()
{
[SharedOutputType.Extras] = new[] { "output/extras-images" },
[SharedOutputType.Saved] = new[] { "log/images" },
[SharedOutputType.Img2Img] = new[] { "output/img2img-images" },
[SharedOutputType.Text2Img] = new[] { "output/txt2img-images" },
[SharedOutputType.Img2ImgGrids] = new[] { "output/img2img-grids" },
[SharedOutputType.Text2ImgGrids] = new[] { "output/txt2img-grids" }
};
public override List<LaunchOptionDefinition> LaunchOptions =>
[
new LaunchOptionDefinition
{
Name = "Always Offload from VRAM",
Type = LaunchOptionType.Bool,
Options = ["--always-offload-from-vram"]
},
new LaunchOptionDefinition
{
Name = "Always GPU",
Type = LaunchOptionType.Bool,
Options = ["--always-gpu"]
},
new LaunchOptionDefinition
{
Name = "Always CPU",
Type = LaunchOptionType.Bool,
Options = ["--always-cpu"]
},
new LaunchOptionDefinition
{
Name = "Use DirectML",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.PreferDirectML(),
Options = ["--directml"]
},
new LaunchOptionDefinition
{
Name = "Skip Torch CUDA Test",
Type = LaunchOptionType.Bool,
InitialValue = Compat.IsMacOS,
Options = ["--skip-torch-cuda-test"]
},
new LaunchOptionDefinition
{
Name = "No half-precision VAE",
Type = LaunchOptionType.Bool,
InitialValue = Compat.IsMacOS,
Options = ["--no-half-vae"]
},
LaunchOptionDefinition.Extras
];
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
new[]
{
TorchVersion.Cpu,
TorchVersion.Cuda,
TorchVersion.DirectMl,
TorchVersion.Rocm,
TorchVersion.Mps
};
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
SharedFolderMethod selectedSharedFolderMethod,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true));
var venvPath = Path.Combine(installLocation, "venv");
await using var venvRunner = new PyVenvRunner(venvPath);
venvRunner.WorkingDirectory = installLocation;
await venvRunner.Setup(true, onConsoleOutput).ConfigureAwait(false);
await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true));
var requirements = new FilePath(installLocation, "requirements_versions.txt");
var pipArgs = new PipInstallArgs();
if (torchVersion is TorchVersion.DirectMl)
{
pipArgs = pipArgs.WithTorchDirectML();
}
else
{
pipArgs = pipArgs
.WithTorch("==2.1.2")
.WithTorchVision("==0.16.2")
.WithTorchExtraIndex(
torchVersion switch
{
TorchVersion.Cpu => "cpu",
TorchVersion.Cuda => "cu121",
TorchVersion.Rocm => "rocm5.6",
TorchVersion.Mps => "nightly/cpu",
_ => throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null)
}
);
}
pipArgs = pipArgs.WithParsedFromRequirementsTxt(
await requirements.ReadAllTextAsync().ConfigureAwait(false),
excludePattern: "torch"
);
await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, "Install complete", isIndeterminate: false));
}
}

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

@ -1,5 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Nodes;
using System.Text.Json;
using System.Text.RegularExpressions;
using NLog;
using StabilityMatrix.Core.Attributes;
@ -7,6 +7,7 @@ using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Packages.Extensions;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python;
@ -41,6 +42,8 @@ public class StableDiffusionUx(
public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced;
public override IPackageExtensionManager? ExtensionManager => new A3WebUiExtensionManager(this);
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{
@ -272,4 +275,46 @@ public class StableDiffusionUx(
)
.ConfigureAwait(false);
}
private class A3WebUiExtensionManager(StableDiffusionUx package)
: GitPackageExtensionManager(package.PrerequisiteHelper)
{
public override string RelativeInstallDirectory => "extensions";
public override IEnumerable<ExtensionManifest> DefaultManifests =>
[
new ExtensionManifest(
new Uri(
"https://raw.githubusercontent.com/AUTOMATIC1111/stable-diffusion-webui-extensions/master/index.json"
)
)
];
public override async Task<IEnumerable<PackageExtension>> GetManifestExtensionsAsync(
ExtensionManifest manifest,
CancellationToken cancellationToken = default
)
{
try
{
// Get json
var content = await package
.DownloadService.GetContentAsync(manifest.Uri.ToString(), cancellationToken)
.ConfigureAwait(false);
// Parse json
var jsonManifest = JsonSerializer.Deserialize<A1111ExtensionManifest>(
content,
A1111ExtensionManifestSerializerContext.Default.Options
);
return jsonManifest?.GetPackageExtensions() ?? Enumerable.Empty<PackageExtension>();
}
catch (Exception e)
{
Logger.Error(e, "Failed to get extensions from manifest");
return Enumerable.Empty<PackageExtension>();
}
}
}
}

386
StabilityMatrix.Core/Models/Packages/StableSwarm.cs

@ -0,0 +1,386 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
using FreneticUtilities.FreneticDataSyntax;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models.FDS;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.Packages;
[Singleton(typeof(BasePackage))]
public class StableSwarm(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
) : BaseGitPackage(githubApi, settingsManager, downloadService, prerequisiteHelper)
{
private Process? dotnetProcess;
public override string Name => "StableSwarmUI";
public override string DisplayName { get; set; } = "StableSwarmUI";
public override string Author => "Stability-AI";
public override string Blurb =>
"A Modular Stable Diffusion Web-User-Interface, with an emphasis on making powertools easily accessible, high performance, and extensibility.";
public override string LicenseType => "MIT";
public override string LicenseUrl =>
"https://github.com/Stability-AI/StableSwarmUI/blob/master/LICENSE.txt";
public override string LaunchCommand => string.Empty;
public override Uri PreviewImageUri =>
new(
"https://raw.githubusercontent.com/Stability-AI/StableSwarmUI/master/.github/images/stableswarmui.jpg"
);
public override string OutputFolderName => "Output";
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods =>
[SharedFolderMethod.Symlink, SharedFolderMethod.Configuration, SharedFolderMethod.None];
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration;
public override bool OfferInOneClickInstaller => false;
public override List<LaunchOptionDefinition> LaunchOptions =>
[
new LaunchOptionDefinition
{
Name = "Host",
Type = LaunchOptionType.String,
DefaultValue = "127.0.0.1",
Options = ["--host"]
},
new LaunchOptionDefinition
{
Name = "Port",
Type = LaunchOptionType.String,
DefaultValue = "7801",
Options = ["--port"]
},
new LaunchOptionDefinition
{
Name = "Ngrok Path",
Type = LaunchOptionType.String,
Options = ["--ngrok-path"]
},
new LaunchOptionDefinition
{
Name = "Ngrok Basic Auth",
Type = LaunchOptionType.String,
Options = ["--ngrok-basic-auth"]
},
new LaunchOptionDefinition
{
Name = "Cloudflared Path",
Type = LaunchOptionType.String,
Options = ["--cloudflared-path"]
},
new LaunchOptionDefinition
{
Name = "Proxy Region",
Type = LaunchOptionType.String,
Options = ["--proxy-region"]
},
new LaunchOptionDefinition
{
Name = "Launch Mode",
Type = LaunchOptionType.Bool,
Options = ["--launch-mode web", "--launch-mode webinstall"]
},
LaunchOptionDefinition.Extras
];
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{
[SharedFolderType.StableDiffusion] = ["Models/Stable-Diffusion"],
[SharedFolderType.Lora] = ["Models/Lora"],
[SharedFolderType.VAE] = ["Models/VAE"],
[SharedFolderType.TextualInversion] = ["Models/Embeddings"],
[SharedFolderType.ControlNet] = ["Models/controlnet"],
[SharedFolderType.InvokeClipVision] = ["Models/clip_vision"]
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>> SharedOutputFolders =>
new() { [SharedOutputType.Text2Img] = [OutputFolderName] };
public override string MainBranch => "master";
public override bool ShouldIgnoreReleases => true;
public override IEnumerable<TorchVersion> AvailableTorchVersions =>
[TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm, TorchVersion.Mps];
public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced;
public override IEnumerable<PackagePrerequisite> Prerequisites =>
[
PackagePrerequisite.Git,
PackagePrerequisite.Dotnet,
PackagePrerequisite.Python310,
PackagePrerequisite.VcRedist
];
private FilePath GetSettingsPath(string installLocation) =>
Path.Combine(installLocation, "Data", "Settings.fds");
private FilePath GetBackendsPath(string installLocation) =>
Path.Combine(installLocation, "Data", "Backends.fds");
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
SharedFolderMethod selectedSharedFolderMethod,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
progress?.Report(new ProgressReport(-1f, "Installing StableSwarmUI...", isIndeterminate: true));
var comfy = settingsManager.Settings.InstalledPackages.FirstOrDefault(
x => x.PackageName == nameof(ComfyUI)
);
if (comfy == null)
{
throw new InvalidOperationException("ComfyUI must be installed to use StableSwarmUI");
}
try
{
await prerequisiteHelper
.RunDotnet(
[
"nuget",
"add",
"source",
"https://api.nuget.org/v3/index.json",
"--name",
"\"NuGet official package source\""
],
workingDirectory: installLocation,
onProcessOutput: onConsoleOutput
)
.ConfigureAwait(false);
}
catch (ProcessException e)
{
// ignore, probably means the source is already there
}
await prerequisiteHelper
.RunDotnet(
[
"build",
"src/StableSwarmUI.csproj",
"--configuration",
"Release",
"-o",
"src/bin/live_release"
],
workingDirectory: installLocation,
onProcessOutput: onConsoleOutput
)
.ConfigureAwait(false);
// set default settings
var settings = new StableSwarmSettings { IsInstalled = true };
if (selectedSharedFolderMethod is SharedFolderMethod.Configuration)
{
settings.Paths = new StableSwarmSettings.PathsData
{
ModelRoot = settingsManager.ModelsDirectory,
SDModelFolder = Path.Combine(
settingsManager.ModelsDirectory,
SharedFolderType.StableDiffusion.ToString()
),
SDLoraFolder = Path.Combine(
settingsManager.ModelsDirectory,
SharedFolderType.Lora.ToString()
),
SDVAEFolder = Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.VAE.ToString()),
SDEmbeddingFolder = Path.Combine(
settingsManager.ModelsDirectory,
SharedFolderType.TextualInversion.ToString()
),
SDControlNetsFolder = Path.Combine(
settingsManager.ModelsDirectory,
SharedFolderType.ControlNet.ToString()
),
SDClipVisionFolder = Path.Combine(
settingsManager.ModelsDirectory,
SharedFolderType.InvokeClipVision.ToString()
)
};
}
settings.Save(true).SaveToFile(GetSettingsPath(installLocation));
var backendsFile = new FDSSection();
var dataSection = new FDSSection();
dataSection.Set("type", "comfyui_selfstart");
dataSection.Set("title", "StabilityMatrix ComfyUI Self-Start");
dataSection.Set("enabled", true);
var launchArgs = comfy.LaunchArgs ?? [];
var comfyArgs = string.Join(
' ',
launchArgs
.Select(arg => arg.ToArgString()?.TrimEnd())
.Where(arg => !string.IsNullOrWhiteSpace(arg))
);
dataSection.Set(
"settings",
new ComfyUiSelfStartSettings
{
StartScript = $"../{comfy.DisplayName}/main.py",
DisableInternalArgs = false,
AutoUpdate = false,
ExtraArgs = comfyArgs
}.Save(true)
);
backendsFile.Set("0", dataSection);
backendsFile.SaveToFile(GetBackendsPath(installLocation));
}
public override async Task RunPackage(
string installedPackagePath,
string command,
string arguments,
Action<ProcessOutput>? onConsoleOutput
)
{
var aspEnvVars = new Dictionary<string, string>
{
["ASPNETCORE_ENVIRONMENT"] = "Production",
["ASPNETCORE_URLS"] = "http://*:7801"
};
void HandleConsoleOutput(ProcessOutput s)
{
onConsoleOutput?.Invoke(s);
if (s.Text.Contains("Starting webserver", StringComparison.OrdinalIgnoreCase))
{
var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)");
var match = regex.Match(s.Text);
if (match.Success)
{
WebUrl = match.Value;
}
OnStartupComplete(WebUrl);
}
}
dotnetProcess = await prerequisiteHelper
.RunDotnet(
args: $"src{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}live_release{Path.DirectorySeparatorChar}StableSwarmUI.dll {arguments.TrimEnd()}",
workingDirectory: installedPackagePath,
envVars: aspEnvVars,
onProcessOutput: HandleConsoleOutput,
waitForExit: false
)
.ConfigureAwait(false);
}
public override Task SetupModelFolders(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
) =>
sharedFolderMethod switch
{
SharedFolderMethod.Symlink
=> base.SetupModelFolders(installDirectory, SharedFolderMethod.Symlink),
SharedFolderMethod.Configuration => SetupModelFoldersConfig(installDirectory), // TODO
_ => Task.CompletedTask
};
public override Task RemoveModelFolderLinks(
DirectoryPath installDirectory,
SharedFolderMethod sharedFolderMethod
) =>
sharedFolderMethod switch
{
SharedFolderMethod.Symlink => base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod),
SharedFolderMethod.Configuration => RemoveModelFoldersConfig(installDirectory),
_ => Task.CompletedTask
};
public override async Task WaitForShutdown()
{
if (dotnetProcess is { HasExited: false })
{
dotnetProcess.Kill(true);
try
{
await dotnetProcess
.WaitForExitAsync(new CancellationTokenSource(5000).Token)
.ConfigureAwait(false);
}
catch (OperationCanceledException e)
{
Console.WriteLine(e);
}
}
dotnetProcess = null;
GC.SuppressFinalize(this);
}
private Task SetupModelFoldersConfig(DirectoryPath installDirectory)
{
var settingsPath = GetSettingsPath(installDirectory);
var existingSettings = new StableSwarmSettings();
var settingsExists = File.Exists(settingsPath);
if (settingsExists)
{
var section = FDSUtility.ReadFile(settingsPath);
existingSettings.Load(section);
}
existingSettings.Paths = new StableSwarmSettings.PathsData
{
ModelRoot = settingsManager.ModelsDirectory,
SDModelFolder = Path.Combine(
settingsManager.ModelsDirectory,
SharedFolderType.StableDiffusion.ToString()
),
SDLoraFolder = Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.Lora.ToString()),
SDVAEFolder = Path.Combine(settingsManager.ModelsDirectory, SharedFolderType.VAE.ToString()),
SDEmbeddingFolder = Path.Combine(
settingsManager.ModelsDirectory,
SharedFolderType.TextualInversion.ToString()
),
SDControlNetsFolder = Path.Combine(
settingsManager.ModelsDirectory,
SharedFolderType.ControlNet.ToString()
),
SDClipVisionFolder = Path.Combine(
settingsManager.ModelsDirectory,
SharedFolderType.InvokeClipVision.ToString()
)
};
existingSettings.Save(true).SaveToFile(settingsPath);
return Task.CompletedTask;
}
private Task RemoveModelFoldersConfig(DirectoryPath installDirectory)
{
var settingsPath = GetSettingsPath(installDirectory);
var existingSettings = new StableSwarmSettings();
var settingsExists = File.Exists(settingsPath);
if (settingsExists)
{
var section = FDSUtility.ReadFile(settingsPath);
existingSettings.Load(section);
}
existingSettings.Paths = new StableSwarmSettings.PathsData();
existingSettings.Save(true).SaveToFile(settingsPath);
return Task.CompletedTask;
}
}

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

@ -9,6 +9,7 @@ using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Packages.Extensions;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python;
@ -79,6 +80,7 @@ public class VladAutomatic(
};
public override string OutputFolderName => "outputs";
public override IPackageExtensionManager ExtensionManager => new VladExtensionManager(this);
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> LaunchOptions =>
@ -231,33 +233,28 @@ public class VladAutomatic(
var installDir = new DirectoryPath(installLocation);
installDir.Create();
if (!string.IsNullOrWhiteSpace(downloadOptions.CommitHash))
if (string.IsNullOrWhiteSpace(downloadOptions.BranchName))
{
await PrerequisiteHelper
.RunGit(
new[] { "clone", "https://github.com/vladmandic/automatic", installDir.Name },
installDir.Parent?.FullPath ?? ""
)
.ConfigureAwait(false);
await PrerequisiteHelper
.RunGit(new[] { "checkout", downloadOptions.CommitHash }, installLocation)
.ConfigureAwait(false);
throw new ArgumentNullException(nameof(downloadOptions));
}
else if (!string.IsNullOrWhiteSpace(downloadOptions.BranchName))
await PrerequisiteHelper
.RunGit(
new[]
{
"clone",
"-b",
downloadOptions.BranchName,
"https://github.com/vladmandic/automatic",
installDir.Name
},
installDir.Parent?.FullPath ?? ""
)
.ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(downloadOptions.CommitHash) && !downloadOptions.IsLatest)
{
await PrerequisiteHelper
.RunGit(
new[]
{
"clone",
"-b",
downloadOptions.BranchName,
"https://github.com/vladmandic/automatic",
installDir.Name
},
installDir.Parent?.FullPath ?? ""
)
.RunGit(new[] { "checkout", downloadOptions.CommitHash }, installLocation)
.ConfigureAwait(false);
}
}
@ -496,4 +493,51 @@ public class VladAutomatic(
return Task.CompletedTask;
}
private class VladExtensionManager(VladAutomatic package)
: GitPackageExtensionManager(package.PrerequisiteHelper)
{
public override string RelativeInstallDirectory => "extensions";
public override IEnumerable<ExtensionManifest> DefaultManifests =>
[new ExtensionManifest(new Uri("https://vladmandic.github.io/sd-data/pages/extensions.json"))];
public override async Task<IEnumerable<PackageExtension>> GetManifestExtensionsAsync(
ExtensionManifest manifest,
CancellationToken cancellationToken = default
)
{
try
{
// Get json
var content = await package
.DownloadService.GetContentAsync(manifest.Uri.ToString(), cancellationToken)
.ConfigureAwait(false);
// Parse json
var jsonManifest = JsonSerializer.Deserialize<IEnumerable<VladExtensionItem>>(
content,
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }
);
return jsonManifest?.Select(
entry =>
new PackageExtension
{
Title = entry.Name,
Author = entry.Long?.Split('/').FirstOrDefault() ?? "Unknown",
Reference = entry.Url,
Files = [entry.Url],
Description = entry.Description,
InstallType = "git-clone"
}
) ?? Enumerable.Empty<PackageExtension>();
}
catch (Exception e)
{
Logger.Error(e, "Failed to get extensions from manifest");
return Enumerable.Empty<PackageExtension>();
}
}
}
}

10
StabilityMatrix.Core/Models/RemoteResource.cs

@ -28,5 +28,13 @@ public readonly record struct RemoteResource
public string? Author { get; init; }
public string? ByAuthor => string.IsNullOrEmpty(Author) ? null : $"by {Author}";
/// <summary>
/// Whether to auto-extract the archive after download
/// </summary>
public bool AutoExtractArchive { get; init; }
/// <summary>
/// Optional relative path to extract the archive to, if AutoExtractArchive is true
/// </summary>
public string? ExtractRelativePath { get; init; }
}

2
StabilityMatrix.Core/Models/SharedFolderType.cs

@ -39,4 +39,6 @@ public enum SharedFolderType
InvokeIpAdapters15 = 1 << 24,
InvokeIpAdaptersXl = 1 << 25,
InvokeClipVision = 1 << 26,
PromptExpansion = 1 << 30
}

54
StabilityMatrix.Core/Models/TrackedDownload.cs

@ -47,6 +47,16 @@ public class TrackedDownload
public string? ExpectedHashSha256 { get; set; }
/// <summary>
/// Whether to auto-extract the archive after download
/// </summary>
public bool AutoExtractArchive { get; set; }
/// <summary>
/// Optional relative path to extract the archive to, if AutoExtractArchive is true
/// </summary>
public string? ExtractRelativePath { get; set; }
[JsonIgnore]
[MemberNotNullWhen(true, nameof(ExpectedHashSha256))]
public bool ValidateHash => ExpectedHashSha256 is not null;
@ -114,13 +124,17 @@ public class TrackedDownload
{
var progress = new Progress<ProgressReport>(OnProgressUpdate);
await downloadService!.ResumeDownloadToFileAsync(
SourceUrl.ToString(),
DownloadDirectory.JoinFile(TempFileName),
resumeFromByte,
progress,
cancellationToken: cancellationToken
);
DownloadDirectory.Create();
await downloadService!
.ResumeDownloadToFileAsync(
SourceUrl.ToString(),
DownloadDirectory.JoinFile(TempFileName),
resumeFromByte,
progress,
cancellationToken: cancellationToken
)
.ConfigureAwait(false);
// If hash validation is enabled, validate the hash
if (ValidateHash)
@ -136,6 +150,26 @@ public class TrackedDownload
);
}
}
// Rename the temp file to the final file
var tempFile = DownloadDirectory.JoinFile(TempFileName);
var finalFile = tempFile.Rename(FileName);
// If auto-extract is enabled, extract the archive
if (AutoExtractArchive)
{
OnProgressUpdate(new ProgressReport(0, isIndeterminate: true, type: ProgressType.Extract));
var extractDirectory = string.IsNullOrWhiteSpace(ExtractRelativePath)
? DownloadDirectory
: DownloadDirectory.JoinDir(ExtractRelativePath);
extractDirectory.Create();
await ArchiveHelper
.Extract7Z(finalFile, extractDirectory, new Progress<ProgressReport>(OnProgressUpdate))
.ConfigureAwait(false);
}
}
public void Start()
@ -334,12 +368,6 @@ public class TrackedDownload
{
DoCleanup();
}
else if (ProgressState == ProgressState.Success)
{
// Move the temp file to the final file
DownloadDirectory.JoinFile(TempFileName).MoveTo(DownloadDirectory.JoinFile(FileName));
}
// For pause, just do nothing
OnProgressStateChanged(ProgressState);

7
StabilityMatrix.Core/Services/DownloadService.cs

@ -1,4 +1,5 @@
using System.Net.Http.Headers;
using System.Net;
using System.Net.Http.Headers;
using Microsoft.Extensions.Logging;
using Polly.Contrib.WaitAndRetry;
using StabilityMatrix.Core.Attributes;
@ -184,6 +185,10 @@ public class DownloadService : IDownloadService
throw new UnauthorizedAccessException();
}
}
else if (noRedirectResponse.StatusCode == HttpStatusCode.Unauthorized)
{
throw new UnauthorizedAccessException();
}
using var redirectRequest = new HttpRequestMessage();
redirectRequest.Method = HttpMethod.Get;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save