Browse Source

Merge branch 'dev' into merge-main-to-dev-70e9b9a

pull/438/head
Ionite 11 months ago committed by GitHub
parent
commit
70c0f0d523
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
  2. 2
      StabilityMatrix.Avalonia/App.axaml
  3. 41
      StabilityMatrix.Avalonia/Controls/BetterComboBox.cs
  4. 158
      StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
  5. 38
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  6. 47
      StabilityMatrix.Avalonia/DialogHelper.cs
  7. 67
      StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs
  8. 2
      StabilityMatrix.Avalonia/Helpers/UriHandler.cs
  9. 9
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  10. 3
      StabilityMatrix.Avalonia/Languages/Resources.resx
  11. 15
      StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs
  12. 11
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  13. 254
      StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml
  14. 50
      StabilityMatrix.Avalonia/Styles/FAComboBoxStyles.axaml
  15. 77
      StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
  16. 26
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs
  17. 31
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/FreeUModule.cs
  18. 9
      StabilityMatrix.Avalonia/ViewModels/Inference/Modules/HiresFixModule.cs
  19. 125
      StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs
  20. 64
      StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs
  21. 47
      StabilityMatrix.Core/Extensions/NullableExtensions.cs
  22. 12
      StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ConditioningConnections.cs
  23. 15
      StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ModelConnections.cs
  24. 43
      StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs
  25. 20
      StabilityMatrix.Core/Models/Database/LocalModelFile.cs
  26. 5
      StabilityMatrix.Core/Models/Packages/BasePackage.cs
  27. 2
      StabilityMatrix.Core/Processes/ProcessRunner.cs
  28. 1
      StabilityMatrix.Core/StabilityMatrix.Core.csproj
  29. 1
      StabilityMatrix.Tests/StabilityMatrix.Tests.csproj
  30. 1
      StabilityMatrix.UITests/StabilityMatrix.UITests.csproj

1
StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj

@ -21,6 +21,7 @@
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.5" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.5" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />

2
StabilityMatrix.Avalonia/App.axaml

@ -4,6 +4,7 @@
xmlns:local="using:StabilityMatrix.Avalonia"
xmlns:idcr="using:Dock.Avalonia.Controls.Recycling"
xmlns:styling="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia"
Name="Stability Matrix"
RequestedThemeVariant="Dark">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
@ -23,6 +24,7 @@
<ResourceInclude Source="Controls/Scroll/BetterScrollViewer.axaml"/>
<ResourceInclude Source="Styles/ControlThemes/HyperlinkIconButtonStyles.axaml"/>
<ResourceInclude Source="Styles/ControlThemes/ListBoxStyles.axaml"/>
<ResourceInclude Source="Styles/ControlThemes/BetterComboBoxStyles.axaml"/>
<ResourceInclude Source="Styles/ListBoxStyles.axaml"/>
<ResourceInclude Source="Styles/FAComboBoxStyles.axaml"/>
<ResourceInclude Source="Controls/Inference/ImageFolderCard.axaml"/>

41
StabilityMatrix.Avalonia/Controls/BetterComboBox.cs

@ -0,0 +1,41 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
namespace StabilityMatrix.Avalonia.Controls;
public class BetterComboBox : ComboBox
{
// protected override Type StyleKeyOverride { get; } = typeof(CheckBox);
public static readonly DirectProperty<BetterComboBox, IDataTemplate?> SelectionBoxItemTemplateProperty =
AvaloniaProperty.RegisterDirect<BetterComboBox, IDataTemplate?>(
nameof(SelectionBoxItemTemplate),
v => v.SelectionBoxItemTemplate,
(x, v) => x.SelectionBoxItemTemplate = v
);
private IDataTemplate? _selectionBoxItemTemplate;
public IDataTemplate? SelectionBoxItemTemplate
{
get => _selectionBoxItemTemplate;
private set => SetAndRaise(SelectionBoxItemTemplateProperty, ref _selectionBoxItemTemplate, value);
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (e.NameScope.Find<ContentControl>("ContentPresenter") is { } contentPresenter)
{
if (SelectionBoxItemTemplate is { } template)
{
contentPresenter.ContentTemplate = template;
}
}
}
}

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

@ -7,6 +7,8 @@
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"
x:DataType="inference:ModelCardViewModel">
<Design.PreviewWith>
<Panel Width="400" Height="200">
@ -21,126 +23,39 @@
<Setter Property="Template">
<ControlTemplate>
<controls:Card Padding="12">
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="*,*,*">
<sg:SpacedGrid
ColumnSpacing="8"
RowSpacing="0"
ColumnDefinitions="Auto,*,Auto"
RowDefinitions="*,*,*,*">
<!-- Model -->
<TextBlock
Grid.Column="0"
MinWidth="60"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Model}"
TextAlignment="Left" />
<ui:FAComboBox
<controls:BetterComboBox
Grid.Row="0"
Grid.Column="1"
Theme="{StaticResource FAComboBoxHybridModelTheme}"
Padding="8,6,4,6"
Theme="{StaticResource BetterComboBoxHybridModelTheme}"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Models}"
SelectedItem="{Binding SelectedModel}">
<!--<ui:FAComboBox.Styles>
<Style Selector="ui|FAComboBoxItem">
<Setter Property="ToolTip.Placement" Value="RightEdgeAlignedTop" />
<Setter Property="ToolTip.Tip">
<Template>
<StackPanel
x:DataType="models:HybridModelFile"
Orientation="Horizontal"
Spacing="6">
~1~ Image @1@
<controls:BetterAdvancedImage
Width="64"
Height="96"
CornerRadius="6"
IsVisible="{Binding Local.PreviewImageFullPathGlobal, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=''}"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue=''}"
Stretch="UniformToFill"
StretchDirection="Both" />
<Grid RowDefinitions="Auto,Auto,*" VerticalAlignment="Stretch" MaxWidth="300">
~1~ Title @1@
<TextBlock
Margin="0,0,0,4"
HorizontalAlignment="Left"
FontSize="14"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.ModelName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
~1~ Version @1@
<TextBlock
Grid.Row="1"
Margin="0,0,0,8"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.VersionName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
~1~ Path @1@
<TextBlock
Grid.Row="2"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding RelativePath}"
TextWrapping="Wrap" />
</Grid>
</StackPanel>
</Template>
</Setter>
</Style>
<Style Selector="ui|FAComboBox /template/ ContentControl#ContentPresenter &gt; StackPanel &gt; TextBlock:nth-child(2)">
<Setter Property="IsVisible" Value="False" />
</Style>
</ui:FAComboBox.Styles>
<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="models:HybridModelFile">
<StackPanel ToolTip.Placement="RightEdgeAlignedTop">
<TextBlock Text="{Binding ShortDisplayName}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ui:FAComboBox.ItemTemplate>-->
<!--<ui:FAComboBox.ItemTemplate>
<DataTemplate DataType="models:HybridModelFile">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*">
<controls:BetterAdvancedImage
Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2"
Margin="0,0,8,0"
Height="32"
Width="32"
Stretch="UniformToFill"
StretchDirection="Both"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal}">
<controls:BetterAdvancedImage.Clip>
<EllipseGeometry Rect="0,0,32,32" />
</controls:BetterAdvancedImage.Clip>
</controls:BetterAdvancedImage>
<TextBlock
Grid.Row="0"
Grid.Column="1"
Text="{Binding Local.ConnectedModelInfo.VersionName}"/>
<TextBlock
FontSize="11"
Grid.Row="1"
Grid.Column="1"
Text="{Binding Local.ConnectedModelInfo.ModelName}"/>
</Grid>
</DataTemplate>
</ui:FAComboBox.ItemTemplate>-->
</ui:FAComboBox>
SelectedItem="{Binding SelectedModel}"/>
<Button
Grid.Row="0"
Grid.Column="2"
Margin="8,0,0,0">
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Margin="0,0,0,0">
<ui:SymbolIcon FontSize="16" Symbol="Setting" />
<Button.Flyout>
<ui:FAMenuFlyout Placement="BottomEdgeAlignedLeft">
<ui:ToggleMenuFlyoutItem IsChecked="{Binding IsRefinerSelectionEnabled}" Text="{x:Static lang:Resources.Label_Refiner}" />
<ui:ToggleMenuFlyoutItem IsChecked="{Binding IsVaeSelectionEnabled}" Text="{x:Static lang:Resources.Label_VAE}" />
<ui:ToggleMenuFlyoutItem IsChecked="{Binding IsClipSkipEnabled}" Text="{x:Static lang:Resources.Label_CLIPSkip}" />
</ui:FAMenuFlyout>
</Button.Flyout>
</Button>
@ -149,30 +64,28 @@
<TextBlock
Grid.Row="1"
Grid.Column="0"
MinWidth="60"
Margin="0,8,0,0"
VerticalAlignment="Center"
IsVisible="{Binding IsRefinerSelectionEnabled}"
Text="{x:Static lang:Resources.Label_Refiner}"
TextAlignment="Left" />
<ui:FAComboBox
<controls:BetterComboBox
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
DisplayMemberBinding="{Binding ShortDisplayName}"
IsTextSearchEnabled="True"
Padding="8,6,4,6"
IsVisible="{Binding IsRefinerSelectionEnabled}"
Theme="{StaticResource BetterComboBoxHybridModelTheme}"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ClientManager.Models}"
SelectedItem="{Binding SelectedRefiner}" />
SelectedItem="{Binding SelectedRefiner}"/>
<!-- VAE -->
<TextBlock
Grid.Row="2"
Grid.Column="0"
MinWidth="60"
Margin="0,8,0,0"
VerticalAlignment="Center"
IsVisible="{Binding IsVaeSelectionEnabled}"
@ -190,8 +103,33 @@
IsVisible="{Binding IsVaeSelectionEnabled}"
ItemsSource="{Binding ClientManager.VaeModels}"
SelectedItem="{Binding SelectedVae}" />
<!-- CLIP Skip -->
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="0,8,0,0"
VerticalAlignment="Center"
IsVisible="{Binding IsClipSkipEnabled}"
Text="{x:Static lang:Resources.Label_CLIPSkip}"
TextAlignment="Left" />
<NumericUpDown
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"
Increment="1"
ParsingNumberStyle="Integer"
Value="{Binding ClipSkip, Converter={x:Static converters:NullableDefaultNumericConverters.IntToDecimal}}"
ClipValueToMinMax="True"/>
</Grid>
</sg:SpacedGrid>
</controls:Card>
</ControlTemplate>
</Setter>

38
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using AvaloniaEdit.Utils;
@ -790,6 +791,43 @@ The gallery images are often inpainted, but you will get something very similar
}
}
public static IEnumerable<HybridModelFile> SampleHybridModels { get; } =
[
HybridModelFile.FromLocal(
new LocalModelFile
{
SharedFolderType = SharedFolderType.StableDiffusion,
RelativePath = "art_shaper_v8.safetensors",
PreviewImageFullPath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dd9b038c-bd15-43ab-86ab-66e145ad7ff2/width=512",
ConnectedModelInfo = new ConnectedModelInfo
{
ModelName = "Art Shaper (very long name example)",
VersionName = "Style v8 (very long name)"
}
}
),
HybridModelFile.FromLocal(
new LocalModelFile
{
SharedFolderType = SharedFolderType.StableDiffusion,
RelativePath = "background_arts.safetensors",
PreviewImageFullPath =
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/71c81ddf-d8c3-46b4-843d-9f8f20a9254a/width=512",
ConnectedModelInfo = new ConnectedModelInfo
{
ModelName = "Background Arts",
VersionName = "Anime Style v10"
}
}
),
HybridModelFile.FromRemote("v1-5-pruned-emaonly.safetensors"),
HybridModelFile.FromRemote("sample-model.pt"),
];
public static HybridModelFile SampleHybridModel => SampleHybridModels.First();
public static ImageViewerViewModel ImageViewerViewModel =>
DialogFactory.Get<ImageViewerViewModel>(vm =>
{

47
StabilityMatrix.Avalonia/DialogHelper.cs

@ -23,15 +23,15 @@ using NLog;
using Refit;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Services;
using TextMateSharp.Grammars;
using Process = FuzzySharp.Process;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Avalonia;
@ -48,11 +48,7 @@ public static class DialogHelper
IReadOnlyList<TextBoxField> textFields
)
{
return CreateTextEntryDialog(
title,
new MarkdownScrollViewer { Markdown = description },
textFields
);
return CreateTextEntryDialog(title, new MarkdownScrollViewer { Markdown = description }, textFields);
}
/// <summary>
@ -80,11 +76,7 @@ public static class DialogHelper
var grid = new Grid
{
RowDefinitions =
{
new RowDefinition(GridLength.Star),
new RowDefinition(GridLength.Auto)
},
RowDefinitions = { new RowDefinition(GridLength.Star), new RowDefinition(GridLength.Auto) },
Children = { markdown, image }
};
@ -109,18 +101,14 @@ public static class DialogHelper
var grid = new Grid
{
RowDefinitions =
{
new RowDefinition(GridLength.Auto),
new RowDefinition(GridLength.Star)
},
RowDefinitions = { new RowDefinition(GridLength.Auto), new RowDefinition(GridLength.Star) },
Children = { content, stackPanel }
};
grid.Loaded += (_, _) =>
{
// Focus first TextBox
var firstTextBox = stackPanel.Children
.OfType<StackPanel>()
var firstTextBox = stackPanel
.Children.OfType<StackPanel>()
.FirstOrDefault()
.FindDescendantOfType<TextBox>();
firstTextBox!.Focus();
@ -254,16 +242,16 @@ public static class DialogHelper
Content = viewer,
CloseButtonText = Resources.Action_Close,
IsPrimaryButtonEnabled = false,
MinDialogWidth = 800,
MaxDialogHeight = 1000,
MaxDialogWidth = 1000
};
}
/// <summary>
/// Create a dialog for displaying an ApiException
/// </summary>
public static BetterContentDialog CreateApiExceptionDialog(
ApiException exception,
string? title = null
)
public static BetterContentDialog CreateApiExceptionDialog(ApiException exception, string? title = null)
{
Dispatcher.UIThread.VerifyAccess();
@ -275,9 +263,7 @@ public static class DialogHelper
Options = { ShowColumnRulers = false, AllowScrollBelowDocument = false }
};
var registryOptions = new RegistryOptions(ThemeName.DarkPlus);
textEditor
.InstallTextMate(registryOptions)
.SetGrammar(registryOptions.GetScopeByLanguageId("json"));
textEditor.InstallTextMate(registryOptions).SetGrammar(registryOptions.GetScopeByLanguageId("json"));
var mainGrid = new StackPanel
{
@ -354,9 +340,7 @@ public static class DialogHelper
Options = { ShowColumnRulers = false, AllowScrollBelowDocument = false }
};
var registryOptions = new RegistryOptions(ThemeName.DarkPlus);
textEditor
.InstallTextMate(registryOptions)
.SetGrammar(registryOptions.GetScopeByLanguageId("json"));
textEditor.InstallTextMate(registryOptions).SetGrammar(registryOptions.GetScopeByLanguageId("json"));
var mainGrid = new StackPanel
{
@ -437,8 +421,7 @@ public static class DialogHelper
{
Dispatcher.UIThread.VerifyAccess();
var title =
exception is PromptSyntaxError ? "Prompt Syntax Error" : "Prompt Validation Error";
var title = exception is PromptSyntaxError ? "Prompt Syntax Error" : "Prompt Validation Error";
// Get the index of the error
var errorIndex = exception.TextOffset;

67
StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs

@ -20,15 +20,17 @@ public static class ComfyNodeBuilderExtensions
int? batchIndex = null
)
{
var emptyLatent = builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.EmptyLatentImage
{
Name = "EmptyLatentImage",
BatchSize = batchSize,
Height = height,
Width = width
}
);
var emptyLatent = builder
.Nodes
.AddTypedNode(
new ComfyNodeBuilder.EmptyLatentImage
{
Name = "EmptyLatentImage",
BatchSize = batchSize,
Height = height,
Width = width
}
);
builder.Connections.Primary = emptyLatent.Output;
builder.Connections.PrimarySize = new Size(width, height);
@ -36,7 +38,8 @@ public static class ComfyNodeBuilderExtensions
// If batch index is selected, add a LatentFromBatch
if (batchIndex is not null)
{
builder.Connections.Primary = builder.Nodes
builder.Connections.Primary = builder
.Nodes
.AddNamedNode(
ComfyNodeBuilder.LatentFromBatch(
"LatentFromBatch",
@ -64,9 +67,9 @@ public static class ComfyNodeBuilderExtensions
var sourceImageRelativePath = Path.Combine("Inference", image.GetHashGuidFileNameCached());
// Load source
var loadImage = builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.LoadImage { Name = "LoadImage", Image = sourceImageRelativePath }
);
var loadImage = builder
.Nodes
.AddTypedNode(new ComfyNodeBuilder.LoadImage { Name = "LoadImage", Image = sourceImageRelativePath });
builder.Connections.Primary = loadImage.Output1;
builder.Connections.PrimarySize = imageSize;
@ -74,7 +77,8 @@ public static class ComfyNodeBuilderExtensions
// If batch index is selected, add a LatentFromBatch
if (batchIndex is not null)
{
builder.Connections.Primary = builder.Nodes
builder.Connections.Primary = builder
.Nodes
.AddNamedNode(
ComfyNodeBuilder.LatentFromBatch(
"LatentFromBatch",
@ -93,24 +97,25 @@ public static class ComfyNodeBuilderExtensions
if (builder.Connections.Primary is null)
throw new ArgumentException("No Primary");
var image = builder.Connections.Primary.Match(
_ =>
builder.GetPrimaryAsImage(
builder.Connections.PrimaryVAE
?? builder.Connections.RefinerVAE
?? builder.Connections.BaseVAE
?? throw new ArgumentException("No Primary, Refiner, or Base VAE")
),
image => image
);
var image = builder
.Connections
.Primary
.Match(
_ =>
builder.GetPrimaryAsImage(
builder.Connections.PrimaryVAE
?? builder.Connections.Refiner.VAE
?? builder.Connections.Base.VAE
?? throw new ArgumentException("No Primary, Refiner, or Base VAE")
),
image => image
);
var previewImage = builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.PreviewImage
{
Name = builder.Nodes.GetUniqueName("SaveImage"),
Images = image
}
);
var previewImage = builder
.Nodes
.AddTypedNode(
new ComfyNodeBuilder.PreviewImage { Name = builder.Nodes.GetUniqueName("SaveImage"), Images = image }
);
builder.Connections.OutputNodes.Add(previewImage);

2
StabilityMatrix.Avalonia/Helpers/UriHandler.cs

@ -62,7 +62,7 @@ public class UriHandler
{
RegisterUriSchemeWin();
}
else
else if (Compat.IsLinux)
{
// Try to register on unix but ignore errors
// Library does not support some distros

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

@ -833,6 +833,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to CLIP Skip.
/// </summary>
public static string Label_CLIPSkip {
get {
return ResourceManager.GetString("Label_CLIPSkip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Close dialog when finished.
/// </summary>

3
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -897,4 +897,7 @@
<data name="Label_HolidayMode" xml:space="preserve">
<value>Holiday Mode</value>
</data>
<data name="Label_CLIPSkip" xml:space="preserve">
<value>CLIP Skip</value>
</data>
</root>

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

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Avalonia.Models.Inference;
@ -29,26 +29,19 @@ public class ModuleApplyStepEventArgs : EventArgs
/// <summary>
/// Generation overrides (like hires fix generate, current seed generate, etc.)
/// </summary>
public IReadOnlyDictionary<Type, bool> IsEnabledOverrides { get; init; } =
new Dictionary<Type, bool>();
public IReadOnlyDictionary<Type, bool> IsEnabledOverrides { get; init; } = new Dictionary<Type, bool>();
public class ModuleApplyStepTemporaryArgs
{
/// <summary>
/// Temporary conditioning apply step, used by samplers to apply control net.
/// </summary>
public (
ConditioningNodeConnection Positive,
ConditioningNodeConnection Negative
)? Conditioning { get; set; }
public ConditioningConnections? Conditioning { get; set; }
/// <summary>
/// Temporary refiner conditioning apply step, used by samplers to apply control net.
/// </summary>
public (
ConditioningNodeConnection Positive,
ConditioningNodeConnection Negative
)? RefinerConditioning { get; set; }
public ConditioningConnections? RefinerConditioning { get; set; }
/// <summary>
/// Temporary model apply step, used by samplers to apply control net.

11
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<CFBundleName>Stability Matrix</CFBundleName>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
@ -8,12 +9,19 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>./Assets/Icon.ico</ApplicationIcon>
<Version>2.7.0-pre.999</Version>
<Version>2.8.0-dev.999</Version>
<InformationalVersion>$(Version)</InformationalVersion>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<CFBundleURLTypes Include="dummy">
<CFBundleURLName>StabilityMatrix.URL</CFBundleURLName>
<CFBundleURLSchemes>stabilitymatrix;stabilitymatrix://</CFBundleURLSchemes>
</CFBundleURLTypes>
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="StabilityMatrix.Tests" />
<InternalsVisibleTo Include="StabilityMatrix.UITests" />
@ -40,6 +48,7 @@
<PackageReference Include="Dock.Avalonia" Version="11.0.0.3" />
<PackageReference Include="Dock.Model.Avalonia" Version="11.0.0.3" />
<PackageReference Include="Dock.Serializer" Version="11.0.0.3" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="DynamicData" Version="8.1.1" />
<PackageReference Include="Exceptionless.DateTimeExtensions" Version="3.4.3" />
<PackageReference Include="FluentAvalonia.BreadcrumbBar" Version="2.0.2" />

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

@ -0,0 +1,254 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:mocks="using: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">
<Design.PreviewWith>
<Panel Width="450" Height="600">
<StackPanel Margin="8" Spacing="4" Width="250">
<controls:BetterComboBox
HorizontalAlignment="Stretch"
ItemsSource="{x:Static mocks:DesignData.SampleHybridModels}"
SelectedIndex="0" />
<controls:BetterComboBox
HorizontalAlignment="Stretch"
ItemsSource="{x:Static mocks:DesignData.SampleHybridModels}"
SelectedIndex="0"
Theme="{DynamicResource BetterComboBoxHybridModelTheme}" />
</StackPanel>
</Panel>
</Design.PreviewWith>
<!-- ReSharper disable once Xaml.StaticResourceNotResolved -->
<ControlTheme
x:Key="BetterComboBoxItemHybridModelTheme"
BasedOn="{StaticResource {x:Type ComboBoxItem}}"
TargetType="ComboBoxItem">
<Setter Property="ToolTip.Placement" Value="RightEdgeAlignedTop" />
<Setter Property="ToolTip.Tip">
<Template>
<sg:SpacedGrid
x:DataType="models:HybridModelFile"
ColumnDefinitions="Auto,*"
ColumnSpacing="6"
RowSpacing="0">
<!-- Image -->
<controls:BetterAdvancedImage
Width="64"
Height="96"
CornerRadius="6"
IsVisible="{Binding Local.PreviewImageFullPathGlobal, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=''}"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue=''}"
Stretch="UniformToFill"
StretchDirection="Both" />
<StackPanel
Grid.Column="1"
MaxWidth="300"
VerticalAlignment="Stretch">
<!-- Title -->
<TextBlock
Margin="0,0,0,4"
HorizontalAlignment="Left"
FontSize="14"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.ModelName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
<!-- Version -->
<TextBlock
Margin="0,0,0,8"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.VersionName, FallbackValue=''}"
TextWrapping="WrapWithOverflow" />
<!-- Path -->
<TextBlock
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding FileName}"
TextWrapping="Wrap" />
</StackPanel>
</sg:SpacedGrid>
</Template>
</Setter>
</ControlTheme>
<!-- ReSharper disable once Xaml.StaticResourceNotResolved -->
<ControlTheme
x:Key="{x:Type controls:BetterComboBox}"
BasedOn="{StaticResource {x:Type ComboBox}}"
TargetType="controls:BetterComboBox" />
<ControlTheme
x:Key="BetterComboBoxHybridModelTheme"
BasedOn="{StaticResource {x:Type controls:BetterComboBox}}"
TargetType="controls:BetterComboBox">
<ControlTheme.Resources>
<controls:HybridModelTemplateSelector x:Key="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.Local}" DataType="models:HybridModelFile">
<sg:SpacedGrid
HorizontalAlignment="Stretch"
ColumnDefinitions="Auto,*"
ColumnSpacing="8"
TextBlock.TextTrimming="CharacterEllipsis"
TextBlock.TextWrapping="NoWrap">
<controls:BetterAdvancedImage
Grid.RowSpan="2"
Width="42"
Height="42"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal}"
Stretch="UniformToFill"
StretchDirection="Both">
<controls:BetterAdvancedImage.Clip>
<EllipseGeometry Rect="0,0,42,42" />
</controls:BetterAdvancedImage.Clip>
</controls:BetterAdvancedImage>
<!-- Text -->
<sg:SpacedGrid
Grid.Row="1"
Grid.Column="1"
RowDefinitions="Auto,Auto,Auto"
RowSpacing="1">
<TextBlock Text="{Binding Local.DisplayModelName}" TextTrimming="CharacterEllipsis" />
<TextBlock
Grid.Row="1"
FontSize="12"
FontWeight="Regular"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="{Binding Local.DisplayModelVersion}"
TextTrimming="CharacterEllipsis" />
<TextBlock
Grid.Row="2"
FontSize="11"
FontWeight="Normal"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding Local.DisplayModelFileName}" />
</sg:SpacedGrid>
</sg:SpacedGrid>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:HybridModelType.None}" DataType="models:HybridModelFile">
<StackPanel>
<TextBlock Text="{Binding ShortDisplayName}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</controls:HybridModelTemplateSelector>
<controls:HybridModelTemplateSelector x:Key="HybridModelSelectionBoxTemplateSelector">
<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.Local}" DataType="models:HybridModelFile">
<sg:SpacedGrid
HorizontalAlignment="Stretch"
ColumnDefinitions="Auto,*"
ColumnSpacing="8"
TextBlock.TextTrimming="CharacterEllipsis"
TextBlock.TextWrapping="NoWrap">
<controls:BetterAdvancedImage
Grid.RowSpan="2"
Width="36"
Height="36"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{Binding Local.PreviewImageFullPathGlobal}"
Stretch="UniformToFill"
StretchDirection="Both">
<controls:BetterAdvancedImage.Clip>
<EllipseGeometry Rect="0,0,36,36" />
</controls:BetterAdvancedImage.Clip>
</controls:BetterAdvancedImage>
<!-- Text -->
<sg:SpacedGrid
Grid.Row="1"
Grid.Column="1"
RowDefinitions="Auto,Auto"
RowSpacing="1">
<TextBlock Text="{Binding Local.DisplayModelName}" TextTrimming="CharacterEllipsis" />
<TextBlock
Grid.Row="1"
FontSize="12"
FontWeight="Regular"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="{Binding Local.DisplayModelVersion}"
TextTrimming="CharacterEllipsis" />
</sg:SpacedGrid>
</sg:SpacedGrid>
</DataTemplate>
<DataTemplate x:Key="{x:Static models:HybridModelType.None}" DataType="models:HybridModelFile">
<StackPanel>
<TextBlock Text="{Binding ShortDisplayName}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</controls:HybridModelTemplateSelector>
</ControlTheme.Resources>
<Setter Property="TextBlock.TextWrapping" Value="NoWrap" />
<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" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="Effect">
<DropShadowEffect
BlurRadius="32"
Opacity="0.6"
Color="#FF000000" />
</Setter>
</Style>
</ControlTheme>
</ResourceDictionary>

50
StabilityMatrix.Avalonia/Styles/FAComboBoxStyles.axaml

@ -2,33 +2,36 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:mocks="using: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">
<Design.PreviewWith>
<Panel Width="400" Height="600">
<Panel Width="350" Height="200" />
<Panel Width="250" Height="600">
<StackPanel Margin="8" Spacing="4">
<ui:FAComboBox
HorizontalAlignment="Stretch"
SelectedIndex="0"
Theme="{DynamicResource FAComboBoxHybridModelTheme}"
ItemsSource="{x:Static mocks:DesignData.SampleHybridModels}"/>
</StackPanel>
</Panel>
</Design.PreviewWith>
<!-- ReSharper disable once Xaml.StaticResourceNotResolved -->
<ControlTheme
x:Key="FAComboBoxItemHybridModelTheme"
BasedOn="{StaticResource {x:Type ui:FAComboBoxItem}}"
TargetType="ui:FAComboBoxItem">
<!--<Setter Property="ContentTemplate">
<DataTemplate DataType="models:HybridModelFile">
<StackPanel>
<TextBlock Text="{Binding ShortDisplayName}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</Setter>-->
<Setter Property="ToolTip.Placement" Value="RightEdgeAlignedTop" />
<Setter Property="ToolTip.Tip">
<Template>
<StackPanel
<sg:SpacedGrid
x:DataType="models:HybridModelFile"
Orientation="Horizontal"
Spacing="6">
ColumnDefinitions="Auto,*"
ColumnSpacing="6"
RowSpacing="0">
<!-- Image -->
<controls:BetterAdvancedImage
Width="64"
@ -39,10 +42,7 @@
Source="{Binding Local.PreviewImageFullPathGlobal, FallbackValue=''}"
Stretch="UniformToFill"
StretchDirection="Both" />
<Grid
MaxWidth="300"
VerticalAlignment="Stretch"
RowDefinitions="Auto,Auto,*">
<StackPanel Grid.Column="1" MaxWidth="300">
<!-- Title -->
<TextBlock
Margin="0,0,0,4"
@ -51,32 +51,30 @@
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.ModelName, FallbackValue=''}"
Text="{Binding Local.DisplayModelName}"
TextWrapping="WrapWithOverflow" />
<!-- Version -->
<TextBlock
Grid.Row="1"
Margin="0,0,0,8"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
IsVisible="{Binding Local.ConnectedModelInfo, Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
Text="{Binding Local.ConnectedModelInfo.VersionName, FallbackValue=''}"
Text="{Binding Local.DisplayModelVersion}"
TextWrapping="WrapWithOverflow" />
<!-- Path -->
<TextBlock
Grid.Row="2"
HorizontalAlignment="Left"
FontSize="13"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Text="{Binding FileName}"
TextWrapping="Wrap" />
</Grid>
</StackPanel>
</StackPanel>
</sg:SpacedGrid>
</Template>
</Setter>
</ControlTheme>
<!-- ReSharper disable once Xaml.StaticResourceNotResolved -->
<ControlTheme x:Key="FAComboBoxHybridModelTheme"
TargetType="ui:FAComboBox"
@ -88,7 +86,7 @@
</StackPanel>
</DataTemplate>
</Setter>
<Setter Property="ItemContainerTheme" Value="{StaticResource FAComboBoxItemHybridModelTheme}"/>
<Setter Property="ItemContainerTheme" Value="{StaticResource FAComboBoxItemHybridModelTheme}" />
</ControlTheme>
</ResourceDictionary>

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

@ -11,7 +11,6 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
@ -38,6 +37,14 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
[ObservableProperty]
private bool isVaeSelectionEnabled;
[ObservableProperty]
private bool isClipSkipEnabled;
[NotifyDataErrorInfo]
[ObservableProperty]
[Range(1, 24)]
private int clipSkip = 1;
public IInferenceClientManager ClientManager { get; } = clientManager;
/// <inheritdoc />
@ -47,50 +54,68 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
var baseLoader = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.CheckpointLoaderSimple
{
Name = "CheckpointLoader",
CkptName =
SelectedModel?.RelativePath
?? throw new ValidationException("Model not selected")
Name = "CheckpointLoader_Base",
CkptName = SelectedModel?.RelativePath ?? throw new ValidationException("Model not selected")
}
);
e.Builder.Connections.BaseModel = baseLoader.Output1;
e.Builder.Connections.BaseClip = baseLoader.Output2;
e.Builder.Connections.BaseVAE = baseLoader.Output3;
e.Builder.Connections.Base.Model = baseLoader.Output1;
e.Builder.Connections.Base.Clip = baseLoader.Output2;
e.Builder.Connections.Base.VAE = baseLoader.Output3;
// Load refiner
// Load refiner if enabled
if (IsRefinerSelectionEnabled && SelectedRefiner is { IsNone: false })
{
var refinerLoader = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.CheckpointLoaderSimple
{
Name = "Refiner_CheckpointLoader",
Name = "CheckpointLoader_Refiner",
CkptName =
SelectedRefiner?.RelativePath
?? throw new ValidationException("Refiner Model enabled but not selected")
}
);
e.Builder.Connections.RefinerModel = refinerLoader.Output1;
e.Builder.Connections.RefinerClip = refinerLoader.Output2;
e.Builder.Connections.RefinerVAE = refinerLoader.Output3;
e.Builder.Connections.Refiner.Model = refinerLoader.Output1;
e.Builder.Connections.Refiner.Clip = refinerLoader.Output2;
e.Builder.Connections.Refiner.VAE = refinerLoader.Output3;
}
// Load custom VAE
// Load VAE override if enabled
if (IsVaeSelectionEnabled && SelectedVae is { IsNone: false, IsDefault: false })
{
var vaeLoader = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.VAELoader
{
Name = "VAELoader",
VaeName =
SelectedVae?.RelativePath
?? throw new ValidationException("VAE enabled but not selected")
VaeName = SelectedVae?.RelativePath ?? throw new ValidationException("VAE enabled but not selected")
}
);
e.Builder.Connections.PrimaryVAE = vaeLoader.Output;
}
// Clip skip all models if enabled
if (IsClipSkipEnabled)
{
foreach (var (modelName, model) in e.Builder.Connections.Models)
{
if (model.Clip is not { } modelClip)
continue;
var clipSetLastLayer = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.CLIPSetLastLayer
{
Name = $"CLIP_Skip_{modelName}",
Clip = modelClip,
// Need to convert to negative indexing from (1 to 24) to (-1 to -24)
StopAtClipLayer = -ClipSkip
}
);
model.Clip = clipSetLastLayer.Output;
}
}
}
/// <inheritdoc />
@ -102,8 +127,10 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
SelectedModelName = SelectedModel?.RelativePath,
SelectedVaeName = SelectedVae?.RelativePath,
SelectedRefinerName = SelectedRefiner?.RelativePath,
ClipSkip = ClipSkip,
IsVaeSelectionEnabled = IsVaeSelectionEnabled,
IsRefinerSelectionEnabled = IsRefinerSelectionEnabled
IsRefinerSelectionEnabled = IsRefinerSelectionEnabled,
IsClipSkipEnabled = IsClipSkipEnabled
}
);
}
@ -125,8 +152,11 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
? HybridModelFile.None
: ClientManager.Models.FirstOrDefault(x => x.RelativePath == model.SelectedRefinerName);
ClipSkip = model.ClipSkip;
IsVaeSelectionEnabled = model.IsVaeSelectionEnabled;
IsRefinerSelectionEnabled = model.IsRefinerSelectionEnabled;
IsClipSkipEnabled = model.IsClipSkipEnabled;
}
/// <inheritdoc />
@ -145,19 +175,14 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
model = currentModels.FirstOrDefault(
m =>
m.Local?.ConnectedModelInfo?.Hashes.SHA256 is { } sha256
&& sha256.StartsWith(
parameters.ModelHash,
StringComparison.InvariantCultureIgnoreCase
)
&& sha256.StartsWith(parameters.ModelHash, StringComparison.InvariantCultureIgnoreCase)
);
}
else
{
// Name matches
model = currentModels.FirstOrDefault(m => m.RelativePath.EndsWith(paramsModelName));
model ??= currentModels.FirstOrDefault(
m => m.ShortDisplayName.StartsWith(paramsModelName)
);
model ??= currentModels.FirstOrDefault(m => m.ShortDisplayName.StartsWith(paramsModelName));
}
if (model is not null)
@ -181,8 +206,10 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
public string? SelectedModelName { get; init; }
public string? SelectedRefinerName { get; init; }
public string? SelectedVaeName { get; init; }
public int ClipSkip { get; init; } = 1;
public bool IsVaeSelectionEnabled { get; init; }
public bool IsRefinerSelectionEnabled { get; init; }
public bool IsClipSkipEnabled { get; init; }
}
}

26
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
@ -42,9 +40,8 @@ public class ControlNetModule : ModuleBase
{
Name = e.Nodes.GetUniqueName("ControlNet_LoadImage"),
Image =
card.SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached(
"Inference"
) ?? throw new ValidationException("No ImageSource")
card.SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference")
?? throw new ValidationException("No ImageSource")
}
);
@ -52,9 +49,7 @@ public class ControlNetModule : ModuleBase
new ComfyNodeBuilder.ControlNetLoader
{
Name = e.Nodes.GetUniqueName("ControlNetLoader"),
ControlNetName =
card.SelectedModel?.FileName
?? throw new ValidationException("No SelectedModel"),
ControlNetName = card.SelectedModel?.FileName ?? throw new ValidationException("No SelectedModel"),
}
);
@ -64,10 +59,8 @@ public class ControlNetModule : ModuleBase
Name = e.Nodes.GetUniqueName("ControlNetApply"),
Image = imageLoad.Output1,
ControlNet = controlNetLoader.Output,
Positive =
e.Temp.Conditioning?.Positive ?? throw new ArgumentException("No Conditioning"),
Negative =
e.Temp.Conditioning?.Negative ?? throw new ArgumentException("No Conditioning"),
Positive = e.Temp.Conditioning?.Positive ?? throw new ArgumentException("No Conditioning"),
Negative = e.Temp.Conditioning?.Negative ?? throw new ArgumentException("No Conditioning"),
Strength = card.Strength,
StartPercent = card.StartPercent,
EndPercent = card.EndPercent,
@ -85,18 +78,15 @@ public class ControlNetModule : ModuleBase
Name = e.Nodes.GetUniqueName("Refiner_ControlNetApply"),
Image = imageLoad.Output1,
ControlNet = controlNetLoader.Output,
Positive = e.Temp.RefinerConditioning.Value.Positive,
Negative = e.Temp.RefinerConditioning.Value.Negative,
Positive = e.Temp.RefinerConditioning.Positive,
Negative = e.Temp.RefinerConditioning.Negative,
Strength = card.Strength,
StartPercent = card.StartPercent,
EndPercent = card.EndPercent,
}
);
e.Temp.RefinerConditioning = (
controlNetRefinerApply.Output1,
controlNetRefinerApply.Output2
);
e.Temp.RefinerConditioning = (controlNetRefinerApply.Output1, controlNetRefinerApply.Output2);
}
}
}

31
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/FreeUModule.cs

@ -1,7 +1,9 @@
using StabilityMatrix.Avalonia.Models.Inference;
using System.Linq;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
@ -25,34 +27,17 @@ public class FreeUModule : ModuleBase
{
var card = GetCard<FreeUCardViewModel>();
// Currently applies to both base and refiner model
// Currently applies to all models
// TODO: Add option to apply to either base or refiner
if (e.Builder.Connections.BaseModel is not null)
foreach (var modelConnections in e.Builder.Connections.Models.Values.Where(m => m.Model is not null))
{
e.Builder.Connections.BaseModel = e.Nodes
modelConnections.Model = e.Nodes
.AddTypedNode(
new ComfyNodeBuilder.FreeU
{
Name = e.Nodes.GetUniqueName("FreeU"),
Model = e.Builder.Connections.BaseModel,
B1 = card.B1,
B2 = card.B2,
S1 = card.S1,
S2 = card.S2
}
)
.Output;
}
if (e.Builder.Connections.RefinerModel is not null)
{
e.Builder.Connections.RefinerModel = e.Nodes
.AddTypedNode(
new ComfyNodeBuilder.FreeU
{
Name = e.Nodes.GetUniqueName("Refiner_FreeU"),
Model = e.Builder.Connections.RefinerModel,
Name = e.Nodes.GetUniqueName($"FreeU_{modelConnections.Name}"),
Model = modelConnections.Model!,
B1 = card.B1,
B2 = card.B2,
S1 = card.S1,

9
StabilityMatrix.Avalonia/ViewModels/Inference/Modules/HiresFixModule.cs

@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
@ -73,7 +70,7 @@ public partial class HiresFixModule : ModuleBase
{
builder.Connections.Primary = builder.Group_Upscale(
builder.Nodes.GetUniqueName("HiresFix"),
builder.Connections.Primary ?? throw new ArgumentException("No Primary"),
builder.Connections.Primary.Unwrap(),
builder.Connections.GetDefaultVAE(),
selectedUpscaler,
hiresSize.Width,
@ -99,8 +96,8 @@ public partial class HiresFixModule : ModuleBase
samplerCard.SelectedScheduler?.Name
?? e.Builder.Connections.PrimaryScheduler?.Name
?? throw new ArgumentException("No PrimaryScheduler"),
Positive = builder.Connections.GetRefinerOrBaseConditioning(),
Negative = builder.Connections.GetRefinerOrBaseNegativeConditioning(),
Positive = builder.Connections.GetRefinerOrBaseConditioning().Positive,
Negative = builder.Connections.GetRefinerOrBaseConditioning().Negative,
LatentImage = builder.GetPrimaryAsLatent(),
Denoise = samplerCard.DenoiseStrength
}

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

@ -18,6 +18,7 @@ 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;
@ -26,10 +27,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(PromptCard))]
[ManagedService]
[Transient]
public partial class PromptCardViewModel
: LoadableViewModelBase,
IParametersLoadableState,
IComfyStep
public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoadableState, IComfyStep
{
private readonly IModelIndexService modelIndexService;
@ -74,13 +72,11 @@ public partial class PromptCardViewModel
/// Applies the prompt step.
/// Requires:
/// <list type="number">
/// <item><see cref="ComfyNodeBuilder.NodeBuilderConnections.BaseModel"/></item>
/// <item><see cref="ComfyNodeBuilder.NodeBuilderConnections.BaseClip"/></item>
/// <item><see cref="ComfyNodeBuilder.NodeBuilderConnections.Base"/> - Model, Clip</item>
/// </list>
/// Provides:
/// <list type="number">
/// <item><see cref="ComfyNodeBuilder.NodeBuilderConnections.BaseConditioning"/></item>
/// <item><see cref="ComfyNodeBuilder.NodeBuilderConnections.BaseNegativeConditioning"/></item>
/// <item><see cref="ComfyNodeBuilder.NodeBuilderConnections.Base"/> - Conditioning</item>
/// </list>
/// </summary>
public void ApplyStep(ModuleApplyStepEventArgs e)
@ -91,90 +87,44 @@ public partial class PromptCardViewModel
var negativePrompt = GetNegativePrompt();
negativePrompt.Process();
// If need to load loras, add a group
if (positivePrompt.ExtraNetworks.Count > 0)
foreach (var modelConnections in e.Builder.Connections.Models.Values)
{
var loras = positivePrompt.GetExtraNetworksAsLocalModels(modelIndexService).ToList();
// Add group to load loras onto model and clip in series
var lorasGroup = e.Builder.Group_LoraLoadMany(
"Loras",
e.Builder.Connections.BaseModel ?? throw new ArgumentException("BaseModel is null"),
e.Builder.Connections.BaseClip ?? throw new ArgumentException("BaseClip is null"),
loras
);
// Set last outputs as base model and clip
e.Builder.Connections.BaseModel = lorasGroup.Output1;
e.Builder.Connections.BaseClip = lorasGroup.Output2;
if (modelConnections.Model is not { } model || modelConnections.Clip is not { } clip)
continue;
// Refiner loras
if (e.Builder.Connections.RefinerModel is not null)
// If need to load loras, add a group
if (positivePrompt.ExtraNetworks.Count > 0)
{
// Add group to load loras onto refiner model and clip in series
var lorasGroupRefiner = e.Builder.Group_LoraLoadMany(
"Refiner_Loras",
e.Builder.Connections.RefinerModel
?? throw new ArgumentException("RefinerModel is null"),
e.Builder.Connections.RefinerClip
?? throw new ArgumentException("RefinerClip is null"),
loras
);
var loras = positivePrompt.GetExtraNetworksAsLocalModels(modelIndexService).ToList();
// Set last outputs as refiner model and clip
e.Builder.Connections.RefinerModel = lorasGroupRefiner.Output1;
e.Builder.Connections.RefinerClip = lorasGroupRefiner.Output2;
}
}
// Add group to load loras onto model and clip in series
var lorasGroup = e.Builder.Group_LoraLoadMany($"Loras_{modelConnections.Name}", model, clip, loras);
// Clips
var positiveClip = e.Builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.CLIPTextEncode
{
Name = "PositiveCLIP",
Clip = e.Builder.Connections.BaseClip!,
Text = positivePrompt.ProcessedText
}
);
var negativeClip = e.Builder.Nodes.AddTypedNode(
new ComfyNodeBuilder.CLIPTextEncode
{
Name = "NegativeCLIP",
Clip = e.Builder.Connections.BaseClip!,
Text = negativePrompt.ProcessedText
// Set last outputs as model and clip
modelConnections.Model = lorasGroup.Output1;
modelConnections.Clip = lorasGroup.Output2;
}
);
// Set base conditioning from Clips
e.Builder.Connections.BaseConditioning = positiveClip.Output;
e.Builder.Connections.BaseNegativeConditioning = negativeClip.Output;
// Refiner Clips
if (e.Builder.Connections.RefinerModel is not null)
{
var positiveClipRefiner = e.Builder.Nodes.AddTypedNode(
// Clips
var positiveClip = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.CLIPTextEncode
{
Name = "Refiner_PositiveCLIP",
Clip =
e.Builder.Connections.RefinerClip
?? throw new ArgumentException("RefinerClip is null"),
Name = $"PositiveCLIP_{modelConnections.Name}",
Clip = e.Builder.Connections.Base.Clip!,
Text = positivePrompt.ProcessedText
}
);
var negativeClipRefiner = e.Builder.Nodes.AddTypedNode(
var negativeClip = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.CLIPTextEncode
{
Name = "Refiner_NegativeCLIP",
Clip =
e.Builder.Connections.RefinerClip
?? throw new ArgumentException("RefinerClip is null"),
Name = $"NegativeCLIP_{modelConnections.Name}",
Clip = e.Builder.Connections.Base.Clip!,
Text = negativePrompt.ProcessedText
}
);
// Set refiner conditioning from Clips
e.Builder.Connections.RefinerConditioning = positiveClipRefiner.Output;
e.Builder.Connections.RefinerNegativeConditioning = negativeClipRefiner.Output;
// Set conditioning from Clips
modelConnections.Conditioning = (positiveClip.Output, negativeClip.Output);
}
}
@ -283,11 +233,7 @@ public partial class PromptCardViewModel
```
""";
var dialog = DialogHelper.CreateMarkdownDialog(
md,
"Prompt Syntax",
TextEditorPreset.Prompt
);
var dialog = DialogHelper.CreateMarkdownDialog(md, "Prompt Syntax", TextEditorPreset.Prompt);
dialog.MinDialogWidth = 800;
dialog.MaxDialogHeight = 1000;
dialog.MaxDialogWidth = 1000;
@ -305,9 +251,7 @@ public partial class PromptCardViewModel
}
catch (PromptError e)
{
await DialogHelper
.CreatePromptErrorDialog(e, prompt.RawText, modelIndexService)
.ShowAsync();
await DialogHelper.CreatePromptErrorDialog(e, prompt.RawText, modelIndexService).ShowAsync();
return;
}
@ -327,10 +271,7 @@ public partial class PromptCardViewModel
builder.AppendLine($"## Networks ({networks.Count}):");
builder.AppendLine("```csharp");
builder.AppendLine(
JsonSerializer.Serialize(
networks,
new JsonSerializerOptions() { WriteIndented = true, }
)
JsonSerializer.Serialize(networks, new JsonSerializerOptions() { WriteIndented = true, })
);
builder.AppendLine("```");
}
@ -378,11 +319,7 @@ public partial class PromptCardViewModel
public override JsonObject SaveStateToJsonObject()
{
return SerializeModel(
new PromptCardModel
{
Prompt = PromptDocument.Text,
NegativePrompt = NegativePromptDocument.Text
}
new PromptCardModel { Prompt = PromptDocument.Text, NegativePrompt = NegativePromptDocument.Text }
);
}
@ -405,10 +342,6 @@ public partial class PromptCardViewModel
/// <inheritdoc />
public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
{
return parameters with
{
PositivePrompt = PromptDocument.Text,
NegativePrompt = NegativePromptDocument.Text
};
return parameters with { PositivePrompt = PromptDocument.Text, NegativePrompt = NegativePromptDocument.Text };
}
}

64
StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs

@ -12,6 +12,7 @@ using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.Comfy;
@ -116,14 +117,8 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
}
// Provide temp values
e.Temp.Conditioning = (
e.Builder.Connections.BaseConditioning!,
e.Builder.Connections.BaseNegativeConditioning!
);
e.Temp.RefinerConditioning = (
e.Builder.Connections.RefinerConditioning!,
e.Builder.Connections.RefinerNegativeConditioning!
);
e.Temp.Conditioning = e.Builder.Connections.Base.Conditioning;
e.Temp.RefinerConditioning = e.Builder.Connections.Refiner.Conditioning;
// Apply steps from our addons
ApplyAddonSteps(e);
@ -147,15 +142,17 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
var primaryLatent = e.Builder.GetPrimaryAsLatent();
// Set primary sampler and scheduler
e.Builder.Connections.PrimarySampler = SelectedSampler ?? throw new ValidationException("Sampler not selected");
e.Builder.Connections.PrimaryScheduler =
SelectedScheduler ?? throw new ValidationException("Scheduler not selected");
var primarySampler = SelectedSampler ?? throw new ValidationException("Sampler not selected");
e.Builder.Connections.PrimarySampler = primarySampler;
var primaryScheduler = SelectedScheduler ?? throw new ValidationException("Scheduler not selected");
e.Builder.Connections.PrimaryScheduler = primaryScheduler;
// Use custom sampler if SDTurbo scheduler is selected
if (e.Builder.Connections.PrimaryScheduler == ComfyScheduler.SDTurbo)
{
// Error if using refiner
if (e.Builder.Connections.RefinerModel is not null)
if (e.Builder.Connections.Refiner.Model is not null)
{
throw new ValidationException("SDTurbo Scheduler cannot be used with Refiner Model");
}
@ -172,7 +169,7 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
new ComfyNodeBuilder.SDTurboScheduler
{
Name = "SDTurboScheduler",
Model = e.Builder.Connections.BaseModel ?? throw new ArgumentException("No BaseModel"),
Model = e.Builder.Connections.Base.Model.Unwrap(),
Steps = Steps
}
);
@ -181,7 +178,7 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
new ComfyNodeBuilder.SamplerCustom
{
Name = "Sampler",
Model = e.Builder.Connections.BaseModel ?? throw new ArgumentException("No BaseModel"),
Model = e.Builder.Connections.Base.Model,
AddNoise = true,
NoiseSeed = e.Builder.Connections.Seed,
Cfg = CfgScale,
@ -199,21 +196,23 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
}
// Use KSampler if no refiner, otherwise need KSamplerAdvanced
if (e.Builder.Connections.RefinerModel is null)
if (e.Builder.Connections.Refiner.Model is null)
{
var baseConditioning = e.Builder.Connections.Base.Conditioning.Unwrap();
// No refiner
var sampler = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.KSampler
{
Name = "Sampler",
Model = e.Builder.Connections.BaseModel ?? throw new ArgumentException("No BaseModel"),
Model = e.Builder.Connections.Base.Model.Unwrap(),
Seed = e.Builder.Connections.Seed,
SamplerName = e.Builder.Connections.PrimarySampler?.Name!,
Scheduler = e.Builder.Connections.PrimaryScheduler?.Name!,
SamplerName = primarySampler.Name,
Scheduler = primaryScheduler.Name,
Steps = Steps,
Cfg = CfgScale,
Positive = e.Temp.Conditioning?.Positive!,
Negative = e.Temp.Conditioning?.Negative!,
Positive = baseConditioning.Positive,
Negative = baseConditioning.Negative,
LatentImage = primaryLatent,
Denoise = DenoiseStrength,
}
@ -223,20 +222,23 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
}
else
{
var baseConditioning = e.Builder.Connections.Base.Conditioning.Unwrap();
var refinerConditioning = e.Builder.Connections.Refiner.Conditioning.Unwrap();
// Advanced base sampler for refiner
var sampler = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.KSamplerAdvanced
{
Name = "Sampler",
Model = e.Builder.Connections.BaseModel ?? throw new ArgumentException("No BaseModel"),
Model = e.Builder.Connections.Base.Model.Unwrap(),
AddNoise = true,
NoiseSeed = e.Builder.Connections.Seed,
Steps = TotalSteps,
Cfg = CfgScale,
SamplerName = e.Builder.Connections.PrimarySampler?.Name!,
Scheduler = e.Builder.Connections.PrimaryScheduler?.Name!,
Positive = e.Temp.Conditioning?.Positive!,
Negative = e.Temp.Conditioning?.Negative!,
SamplerName = primarySampler.Name,
Scheduler = primaryScheduler.Name,
Positive = baseConditioning.Positive,
Negative = baseConditioning.Negative,
LatentImage = primaryLatent,
StartAtStep = 0,
EndAtStep = Steps,
@ -248,16 +250,16 @@ public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLo
var refinerSampler = e.Nodes.AddTypedNode(
new ComfyNodeBuilder.KSamplerAdvanced
{
Name = "Refiner_Sampler",
Model = e.Builder.Connections.RefinerModel ?? throw new ArgumentException("No RefinerModel"),
Name = "Sampler_Refiner",
Model = e.Builder.Connections.Refiner.Model,
AddNoise = false,
NoiseSeed = e.Builder.Connections.Seed,
Steps = TotalSteps,
Cfg = CfgScale,
SamplerName = e.Builder.Connections.PrimarySampler?.Name!,
Scheduler = e.Builder.Connections.PrimaryScheduler?.Name!,
Positive = e.Temp.RefinerConditioning?.Positive!,
Negative = e.Temp.RefinerConditioning?.Negative!,
SamplerName = primarySampler.Name,
Scheduler = primaryScheduler.Name,
Positive = refinerConditioning.Positive,
Negative = refinerConditioning.Negative,
// Connect to previous sampler
LatentImage = sampler.Output,
StartAtStep = Steps,

47
StabilityMatrix.Core/Extensions/NullableExtensions.cs

@ -0,0 +1,47 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace StabilityMatrix.Core.Extensions;
public static class NullableExtensions
{
/// <summary>
/// Unwraps a nullable object, throwing an exception if it is null.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if (<typeparamref name="T"/>) <paramref name="obj"/> is null.
/// </exception>
[DebuggerStepThrough]
[EditorBrowsable(EditorBrowsableState.Never)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Unwrap<T>([NotNull] this T? obj, [CallerArgumentExpression("obj")] string? paramName = null)
where T : class
{
if (obj is null)
{
throw new ArgumentNullException(paramName, $"Unwrap of a null value ({typeof(T)}) {paramName}.");
}
return obj;
}
/// <summary>
/// Unwraps a nullable struct object, throwing an exception if it is null.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if (<typeparamref name="T"/>) <paramref name="obj"/> is null.
/// </exception>
[DebuggerStepThrough]
[EditorBrowsable(EditorBrowsableState.Never)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Unwrap<T>([NotNull] this T? obj, [CallerArgumentExpression("obj")] string? paramName = null)
where T : struct
{
if (obj is null)
{
throw new ArgumentNullException(paramName, $"Unwrap of a null value ({typeof(T)}) {paramName}.");
}
return obj.Value;
}
}

12
StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ConditioningConnections.cs

@ -0,0 +1,12 @@
namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
/// <summary>
/// Combination of the positive and negative conditioning connections.
/// </summary>
public record ConditioningConnections(ConditioningNodeConnection Positive, ConditioningNodeConnection Negative)
{
// Implicit from tuple
public static implicit operator ConditioningConnections(
(ConditioningNodeConnection Positive, ConditioningNodeConnection Negative) value
) => new(value.Positive, value.Negative);
}

15
StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ModelConnections.cs

@ -0,0 +1,15 @@
namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
/// <summary>
/// Connections from a loaded model
/// </summary>
public record ModelConnections(string Name)
{
public ModelNodeConnection? Model { get; set; }
public VAENodeConnection? VAE { get; set; }
public ClipNodeConnection? Clip { get; set; }
public ConditioningConnections? Conditioning { get; set; }
}

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

@ -119,6 +119,14 @@ public class ComfyNodeBuilder
public required int Width { get; init; }
}
public record CLIPSetLastLayer : ComfyTypedNodeBase<ClipNodeConnection>
{
public required ClipNodeConnection Clip { get; init; }
[Range(-24, -1)]
public int StopAtClipLayer { get; init; } = -1;
}
public static NamedComfyNode<LatentNodeConnection> LatentFromBatch(
string name,
LatentNodeConnection samples,
@ -725,18 +733,16 @@ public class ComfyNodeBuilder
public int BatchSize { get; set; } = 1;
public int? BatchIndex { get; set; }
public ModelNodeConnection? BaseModel { get; set; }
public VAENodeConnection? BaseVAE { get; set; }
public ClipNodeConnection? BaseClip { get; set; }
public Dictionary<string, ModelConnections> Models { get; } =
new() { ["Base"] = new ModelConnections("Base"), ["Refiner"] = new ModelConnections("Refiner") };
public ConditioningNodeConnection? BaseConditioning { get; set; }
public ConditioningNodeConnection? BaseNegativeConditioning { get; set; }
/// <summary>
/// ModelConnections from <see cref="Models"/> with <see cref="ModelConnections.Model"/> set
/// </summary>
public IEnumerable<ModelConnections> LoadedModels => Models.Values.Where(m => m.Model is not null);
public ModelNodeConnection? RefinerModel { get; set; }
public VAENodeConnection? RefinerVAE { get; set; }
public ClipNodeConnection? RefinerClip { get; set; }
public ConditioningNodeConnection? RefinerConditioning { get; set; }
public ConditioningNodeConnection? RefinerNegativeConditioning { get; set; }
public ModelConnections Base => Models["Base"];
public ModelConnections Refiner => Models["Refiner"];
public PrimaryNodeConnection? Primary { get; set; }
public VAENodeConnection? PrimaryVAE { get; set; }
@ -751,24 +757,19 @@ public class ComfyNodeBuilder
public ModelNodeConnection GetRefinerOrBaseModel()
{
return RefinerModel ?? BaseModel ?? throw new NullReferenceException("No Model");
}
public ConditioningNodeConnection GetRefinerOrBaseConditioning()
{
return RefinerConditioning ?? BaseConditioning ?? throw new NullReferenceException("No Conditioning");
return Refiner.Model ?? Base.Model ?? throw new NullReferenceException("No Refiner or Base Model");
}
public ConditioningNodeConnection GetRefinerOrBaseNegativeConditioning()
public ConditioningConnections GetRefinerOrBaseConditioning()
{
return RefinerNegativeConditioning
?? BaseNegativeConditioning
?? throw new NullReferenceException("No Negative Conditioning");
return Refiner.Conditioning
?? Base.Conditioning
?? throw new NullReferenceException("No Refiner or Base Conditioning");
}
public VAENodeConnection GetDefaultVAE()
{
return PrimaryVAE ?? RefinerVAE ?? BaseVAE ?? throw new NullReferenceException("No VAE");
return PrimaryVAE ?? Refiner.VAE ?? Base.VAE ?? throw new NullReferenceException("No VAE");
}
}

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

@ -29,6 +29,11 @@ public class LocalModelFile
/// </summary>
public string? PreviewImageRelativePath { get; set; }
/// <summary>
/// Optional preview image full path. Takes priority over <see cref="PreviewImageRelativePath"/>.
/// </summary>
public string? PreviewImageFullPath { get; set; }
/// <summary>
/// File name of the relative path.
/// </summary>
@ -54,6 +59,9 @@ public class LocalModelFile
public string? GetPreviewImageFullPath(string rootModelDirectory)
{
if (PreviewImageFullPath != null)
return PreviewImageFullPath;
return PreviewImageRelativePath == null ? null : Path.Combine(rootModelDirectory, PreviewImageRelativePath);
}
@ -61,7 +69,17 @@ public class LocalModelFile
public string FullPathGlobal => GetFullPath(GlobalConfig.LibraryDir.JoinDir("Models"));
[BsonIgnore]
public string? PreviewImageFullPathGlobal => GetPreviewImageFullPath(GlobalConfig.LibraryDir.JoinDir("Models"));
public string? PreviewImageFullPathGlobal =>
PreviewImageFullPath ?? GetPreviewImageFullPath(GlobalConfig.LibraryDir.JoinDir("Models"));
[BsonIgnore]
public string DisplayModelName => ConnectedModelInfo?.ModelName ?? FileNameWithoutExtension;
[BsonIgnore]
public string DisplayModelVersion => ConnectedModelInfo?.VersionName ?? string.Empty;
[BsonIgnore]
public string DisplayModelFileName => FileName;
protected bool Equals(LocalModelFile other)
{

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

@ -117,6 +117,11 @@ public abstract class BasePackage
return TorchVersion.DirectMl;
}
if (Compat.IsMacOS && Compat.IsArm && AvailableTorchVersions.Contains(TorchVersion.Mps))
{
return TorchVersion.Mps;
}
return TorchVersion.Cpu;
}

2
StabilityMatrix.Core/Processes/ProcessRunner.cs

@ -92,7 +92,7 @@ public static class ProcessRunner
else if (Compat.IsMacOS)
{
using var process = new Process();
process.StartInfo.FileName = "explorer";
process.StartInfo.FileName = "open";
process.StartInfo.Arguments = $"-R {Quote(filePath)}";
process.Start();
await process.WaitForExitAsync().ConfigureAwait(false);

1
StabilityMatrix.Core/StabilityMatrix.Core.csproj

@ -25,6 +25,7 @@
<PackageReference Include="DeviceId.Mac" Version="6.2.1" />
<PackageReference Include="DeviceId.Windows" Version="6.5.0" />
<PackageReference Include="DeviceId.Windows.Wmi" Version="6.5.1" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="DynamicData" Version="8.1.1" />
<PackageReference Include="Hardware.Info" Version="100.0.0.1" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />

1
StabilityMatrix.Tests/StabilityMatrix.Tests.csproj

@ -11,6 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="DotNext" Version="4.15.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.0" />

1
StabilityMatrix.UITests/StabilityMatrix.UITests.csproj

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Avalonia.Headless.XUnit" Version="11.0.5" />
<PackageReference Include="DotNet.Bundle" Version="0.9.13" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="Verify" Version="22.4.1" />

Loading…
Cancel
Save