Browse Source

Merge branch 'dev' into inference-modules

# Conflicts:
#	StabilityMatrix.Core/Converters/Json/DefaultUnknownEnumConverter.cs
pull/333/head
Ionite 12 months ago
parent
commit
b356460b79
No known key found for this signature in database
  1. 16
      CHANGELOG.md
  2. 2
      StabilityMatrix.Avalonia/App.axaml.cs
  3. 14
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  4. 19
      StabilityMatrix.Avalonia/Helpers/IOCommands.cs
  5. 9
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  6. 3
      StabilityMatrix.Avalonia/Languages/Resources.resx
  7. 20
      StabilityMatrix.Avalonia/ViewModels/Base/ViewModelBase.cs
  8. 8
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  9. 4
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  10. 14
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  11. 12
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  12. 47
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
  13. 7
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  14. 3
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
  15. 133
      StabilityMatrix.Core/Converters/Json/DefaultUnknownEnumConverter.cs
  16. 30
      StabilityMatrix.Core/Helper/ArchiveHelper.cs
  17. 9
      StabilityMatrix.Core/Models/Api/CivitCreator.cs
  18. 9
      StabilityMatrix.Core/Models/Api/CivitFileType.cs
  19. 6
      StabilityMatrix.Core/Models/Api/CivitModelFormat.cs
  20. 12
      StabilityMatrix.Core/Models/Api/CivitModelType.cs
  21. 2
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  22. 2
      StabilityMatrix.Core/Models/Packages/FocusControlNet.cs
  23. 72
      StabilityMatrix.Core/Models/Packages/RuinedFooocus.cs
  24. 1
      StabilityMatrix.Core/Models/Settings/TeachingTip.cs
  25. 3
      StabilityMatrix.Core/Models/SharedFolderType.cs
  26. 64
      StabilityMatrix.Core/Processes/ProcessRunner.cs
  27. 81
      StabilityMatrix.Tests/Core/DefaultUnknownEnumConverterTests.cs

16
CHANGELOG.md

@ -7,9 +7,11 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
## v2.7.0-dev.3 ## v2.7.0-dev.3
### Added ### Added
- New package: [RuinedFooocus](https://github.com/runew0lf/RuinedFooocus)
#### Model Browser #### Model Browser
- Right clicking anywhere on the model card will open the same menu as the three-dots button - Right clicking anywhere on the model card will open the same menu as the three-dots button
- New model downloads will save trigger words in metadata, if available - New model downloads will save trigger words in metadata, if available
- Model author username and avatar display, with clickable link to their profile
#### Checkpoints Page #### Checkpoints Page
- Added "Copy Trigger Words" option to the three-dots menu on the Checkpoints page (when data is available) - Added "Copy Trigger Words" option to the three-dots menu on the Checkpoints page (when data is available)
- Added trigger words on checkpoint card and tooltip - Added trigger words on checkpoint card and tooltip
@ -19,6 +21,8 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
- Animated zoom effect on hovering over model images - Animated zoom effect on hovering over model images
#### Checkpoints Page #### Checkpoints Page
- Rearranged top row layout to use CommandBar - Rearranged top row layout to use CommandBar
### Fixed
- Improved startup time and window load time after exiting dialogs
## v2.7.0-dev.2 ## v2.7.0-dev.2
### Added ### Added
@ -50,6 +54,18 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
## Changed ## Changed
- Model Browser page has been redesigned, featuring more information like rating and download counts - Model Browser page has been redesigned, featuring more information like rating and download counts
## v2.6.6
### Fixed
- Fixed error when receiving unknown model format values from the Model Browser
## v2.6.5
### Fixed
- Fixed process errors when installing or updating Pip packages using the Python packages dialog
## v2.6.4
### Fixed
- Fixed errors preventing Model Browser from finding results with certain search queries
## v2.6.3 ## v2.6.3
### Fixed ### Fixed
- Fixed InvalidOperationException during prerequisite installs on certain platforms where process name and duration reporting are not supported - Fixed InvalidOperationException during prerequisite installs on certain platforms where process name and duration reporting are not supported

2
StabilityMatrix.Avalonia/App.axaml.cs

@ -471,6 +471,8 @@ public sealed class App : Application
}; };
jsonSerializerOptions.Converters.Add(new ObjectToInferredTypesConverter()); jsonSerializerOptions.Converters.Add(new ObjectToInferredTypesConverter());
jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter<CivitFileType>()); jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter<CivitFileType>());
jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter<CivitModelType>());
jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter<CivitModelFormat>());
jsonSerializerOptions.Converters.Add( jsonSerializerOptions.Converters.Add(
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
); );

14
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -311,7 +311,12 @@ public static class DesignData
Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 }, Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 },
ModelVersions = [ ModelVersions = [
new() { Name = "v1.2.2-Inpainting" } new() { Name = "v1.2.2-Inpainting" }
] ],
Creator = new CivitCreator
{
Image = "https://gravatar.com/avatar/fe74084ae8a081dc2283f5bde4736756ad?f=y&d=retro",
Username = "creator-1"
}
}; };
}), }),
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm => dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
@ -335,7 +340,12 @@ public static class DesignData
} }
} }
} }
] ],
Creator = new CivitCreator
{
Image = "https://gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro",
Username = "creator-2"
}
}; };
}) })
}; };

19
StabilityMatrix.Avalonia/Helpers/IOCommands.cs

@ -0,0 +1,19 @@
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Avalonia.Helpers;
public static class IOCommands
{
public static RelayCommand<string?> OpenUrlCommand { get; } =
new(
url =>
{
if (string.IsNullOrWhiteSpace(url))
return;
ProcessRunner.OpenUrl(url);
},
url => !string.IsNullOrWhiteSpace(url)
);
}

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

@ -2075,6 +2075,15 @@ namespace StabilityMatrix.Avalonia.Languages {
} }
} }
/// <summary>
/// Looks up a localized string similar to Additional folders such as IPAdapters and TextualInversions (embeddings) can be enabled here.
/// </summary>
public static string TeachingTip_MoreCheckpointCategories {
get {
return ResourceManager.GetString("TeachingTip_MoreCheckpointCategories", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to The app will relaunch after updating. /// Looks up a localized string similar to The app will relaunch after updating.
/// </summary> /// </summary>

3
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -822,4 +822,7 @@
<data name="Label_TriggerWords" xml:space="preserve"> <data name="Label_TriggerWords" xml:space="preserve">
<value>Trigger words:</value> <value>Trigger words:</value>
</data> </data>
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>Additional folders such as IPAdapters and TextualInversions (embeddings) can be enabled here</value>
</data>
</root> </root>

20
StabilityMatrix.Avalonia/ViewModels/Base/ViewModelBase.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using JetBrains.Annotations; using JetBrains.Annotations;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@ -41,7 +42,10 @@ public partial class ViewModelBase : ObservableValidator, IRemovableListItem
if (!ViewModelState.HasFlag(ViewModelState.InitialLoaded)) if (!ViewModelState.HasFlag(ViewModelState.InitialLoaded))
{ {
ViewModelState |= ViewModelState.InitialLoaded; ViewModelState |= ViewModelState.InitialLoaded;
OnInitialLoaded(); OnInitialLoaded();
Dispatcher.UIThread.InvokeAsync(OnInitialLoadedAsync).SafeFireAndForget();
} }
} }
@ -56,10 +60,13 @@ public partial class ViewModelBase : ObservableValidator, IRemovableListItem
/// Runs on the UI thread via Dispatcher.UIThread.InvokeAsync. /// Runs on the UI thread via Dispatcher.UIThread.InvokeAsync.
/// The view loading will not wait for this to complete. /// The view loading will not wait for this to complete.
/// </summary> /// </summary>
public virtual Task OnLoadedAsync() public virtual Task OnLoadedAsync() => Task.CompletedTask;
{
return Task.CompletedTask; /// <summary>
} /// Called the first time the view's LoadedEvent is fired.
/// Sets the <see cref="ViewModelState.InitialLoaded"/> flag.
/// </summary>
protected virtual Task OnInitialLoadedAsync() => Task.CompletedTask;
/// <summary> /// <summary>
/// Called when the view's UnloadedEvent is fired. /// Called when the view's UnloadedEvent is fired.
@ -71,8 +78,5 @@ public partial class ViewModelBase : ObservableValidator, IRemovableListItem
/// Runs on the UI thread via Dispatcher.UIThread.InvokeAsync. /// Runs on the UI thread via Dispatcher.UIThread.InvokeAsync.
/// The view loading will not wait for this to complete. /// The view loading will not wait for this to complete.
/// </summary> /// </summary>
public virtual Task OnUnloadedAsync() public virtual Task OnUnloadedAsync() => Task.CompletedTask;
{
return Task.CompletedTask;
}
} }

8
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

@ -470,12 +470,14 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
} }
// See if query is cached // See if query is cached
var cachedQuery = await liteDbContext.CivitModelQueryCache var cachedQueryResult = await notificationService.TryAsync(
liteDbContext.CivitModelQueryCache
.IncludeAll() .IncludeAll()
.FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest)); .FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest))
);
// If cached, update model cards // If cached, update model cards
if (cachedQuery is not null) if (cachedQueryResult.Result is { } cachedQuery)
{ {
var elapsed = timer.Elapsed; var elapsed = timer.Elapsed;
Logger.Debug( Logger.Debug(

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

@ -90,8 +90,8 @@ public partial class CheckpointFolder : ViewModelBase
public string TitleWithFilesCount => public string TitleWithFilesCount =>
CheckpointFiles.Any() || SubFolders.Any(f => f.CheckpointFiles.Any()) CheckpointFiles.Any() || SubFolders.Any(f => f.CheckpointFiles.Any())
? $"{Title} ({CheckpointFiles.Count + SubFolders.Sum(folder => folder.CheckpointFiles.Count)})" ? $"{FolderType.GetDescription() ?? FolderType.GetStringValue()} ({CheckpointFiles.Count + SubFolders.Sum(folder => folder.CheckpointFiles.Count)})"
: Title; : FolderType.GetDescription() ?? FolderType.GetStringValue();
public ProgressViewModel Progress { get; } = new(); public ProgressViewModel Progress { get; } = new();

14
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

@ -20,6 +20,7 @@ using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol; using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
using TeachingTip = StabilityMatrix.Core.Models.Settings.TeachingTip;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels;
@ -55,6 +56,9 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
[ObservableProperty] [ObservableProperty]
private string searchFilter = string.Empty; private string searchFilter = string.Empty;
[ObservableProperty]
private bool isCategoryTipOpen;
partial void OnIsImportAsConnectedChanged(bool value) partial void OnIsImportAsConnectedChanged(bool value)
{ {
if ( if (
@ -114,6 +118,16 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
if (Design.IsDesignMode) if (Design.IsDesignMode)
return; return;
if (
!settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.CheckpointCategoriesTip)
)
{
IsCategoryTipOpen = true;
settingsManager.Transaction(
s => s.SeenTeachingTips.Add(TeachingTip.CheckpointCategoriesTip)
);
}
IsLoading = CheckpointFolders.Count == 0; IsLoading = CheckpointFolders.Count == 0;
IsIndexing = CheckpointFolders.Count > 0; IsIndexing = CheckpointFolders.Count > 0;
// GetStuff(); // GetStuff();

12
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

@ -78,7 +78,7 @@ public partial class MainWindowViewModel : ViewModelBase
SelectedCategory ??= Pages.FirstOrDefault(); SelectedCategory ??= Pages.FirstOrDefault();
} }
public override async Task OnLoadedAsync() protected override async Task OnInitialLoadedAsync()
{ {
await base.OnLoadedAsync(); await base.OnLoadedAsync();
@ -102,16 +102,20 @@ public partial class MainWindowViewModel : ViewModelBase
// Index checkpoints if we dont have // Index checkpoints if we dont have
Task.Run(() => settingsManager.IndexCheckpoints()).SafeFireAndForget(); Task.Run(() => settingsManager.IndexCheckpoints()).SafeFireAndForget();
if (!App.IsHeadlessMode) // Disable preload for now, might be causing https://github.com/LykosAI/StabilityMatrix/issues/249
/*if (!App.IsHeadlessMode)
{ {
PreloadPages(); PreloadPages();
} }*/
Program.StartupTimer.Stop(); Program.StartupTimer.Stop();
var startupTime = CodeTimer.FormatTime(Program.StartupTimer.Elapsed); var startupTime = CodeTimer.FormatTime(Program.StartupTimer.Elapsed);
Logger.Info($"App started ({startupTime})"); Logger.Info($"App started ({startupTime})");
if (Program.Args.DebugOneClickInstall || !settingsManager.Settings.InstalledPackages.Any()) if (
Program.Args.DebugOneClickInstall
|| settingsManager.Settings.InstalledPackages.Count == 0
)
{ {
var viewModel = dialogFactory.Get<OneClickInstallViewModel>(); var viewModel = dialogFactory.Get<OneClickInstallViewModel>();
var dialog = new BetterContentDialog var dialog = new BetterContentDialog

47
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml

@ -14,6 +14,7 @@
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager" xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}" d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}"
d:DesignHeight="700" d:DesignHeight="700"
d:DesignWidth="800" d:DesignWidth="800"
@ -138,6 +139,48 @@
</Button> </Button>
</StackPanel> </StackPanel>
<!-- Username pill card -->
<Border
BoxShadow="inset 1.2 0 80 1.8 #66000000"
CornerRadius="16"
Margin="4"
Grid.Row="0"
HorizontalAlignment="Left"
VerticalAlignment="Bottom">
<Border.Resources>
<DropShadowEffect
x:Key="TextDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.9"/>
<DropShadowEffect
x:Key="ImageDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.2"/>
</Border.Resources>
<Button
Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding CivitModel.Creator.ProfileUrl}"
CornerRadius="16"
Classes="transparent"
Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<controls:BetterAdvancedImage
Width="22"
Height="22"
Effect="{StaticResource ImageDropShadowEffect}"
CornerRadius="11"
RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding CivitModel.Creator.Image, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Source="{Binding CivitModel.Creator.Image}"/>
<TextBlock
VerticalAlignment="Center"
Effect="{StaticResource TextDropShadowEffect}"
Text="{Binding CivitModel.Creator.Username}"/>
</StackPanel>
</Button>
</Border>
<StackPanel Grid.Row="0" Orientation="Horizontal"> <StackPanel Grid.Row="0" Orientation="Horizontal">
<controls:Card <controls:Card
@ -406,7 +449,9 @@
Grid.Row="1" Grid.Row="1"
Margin="8,0,8,0" Margin="8,0,8,0"
ScrollChanged="ScrollViewer_OnScrollChanged"> ScrollChanged="ScrollViewer_OnScrollChanged">
<ItemsRepeater ItemTemplate="{StaticResource CivitModelTemplate}" ItemsSource="{Binding ModelCards}"> <ItemsRepeater ItemTemplate="{StaticResource CivitModelTemplate}"
HorizontalAlignment="Center"
ItemsSource="{Binding ModelCards}">
<ItemsRepeater.Layout> <ItemsRepeater.Layout>
<UniformGridLayout Orientation="Horizontal" /> <UniformGridLayout Orientation="Horizontal" />
</ItemsRepeater.Layout> </ItemsRepeater.Layout>

7
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -433,6 +433,7 @@
</TextBox.InnerRightContent> </TextBox.InnerRightContent>
</TextBox> </TextBox>
<DropDownButton <DropDownButton
x:Name="CategoriesDropdown"
Content="{x:Static lang:Resources.Label_Categories}" Content="{x:Static lang:Resources.Label_Categories}"
Margin="8,0" Margin="8,0"
VerticalAlignment="Center" VerticalAlignment="Center"
@ -488,6 +489,12 @@
</ui:CommandBar.SecondaryCommands> </ui:CommandBar.SecondaryCommands>
</ui:CommandBar> </ui:CommandBar>
<ui:TeachingTip Grid.Row="0" Grid.Column="0" Name="TeachingTip1"
Target="{Binding #CategoriesDropdown}"
Title="{x:Static lang:Resources.TeachingTip_MoreCheckpointCategories}"
PreferredPlacement="Bottom"
IsOpen="{Binding IsCategoryTipOpen}" />
<StackPanel <StackPanel
IsVisible="False" IsVisible="False"
Grid.Column="1" Grid.Column="1"

3
StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs

@ -32,6 +32,7 @@ using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Update; using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
@ -183,7 +184,7 @@ public partial class MainWindow : AppWindowBase
var tip = this.FindControl<TeachingTip>("UpdateAvailableTeachingTip")!; var tip = this.FindControl<TeachingTip>("UpdateAvailableTeachingTip")!;
tip.Target = target; tip.Target = target;
tip.Subtitle = $"{Compat.AppVersion} -> {updateInfo.Version}"; tip.Subtitle = $"{Compat.AppVersion.ToDisplayString()} -> {updateInfo.Version}";
tip.IsOpen = true; tip.IsOpen = true;
} }
}); });

133
StabilityMatrix.Core/Converters/Json/DefaultUnknownEnumConverter.cs

@ -1,72 +1,110 @@
using System.Reflection; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using StabilityMatrix.Core.Extensions;
namespace StabilityMatrix.Core.Converters.Json; namespace StabilityMatrix.Core.Converters.Json;
public class DefaultUnknownEnumConverter<T> : JsonConverter<T> public class DefaultUnknownEnumConverter<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T
> : JsonConverter<T>
where T : Enum where T : Enum
{ {
// Get EnumMember attribute value /// <summary>
private Dictionary<string, T>? _enumMemberValues; /// Lazy initialization for <see cref="EnumMemberValues"/>.
/// </summary>
private IReadOnlyDictionary<string, T> EnumMemberValues => private readonly Lazy<Dictionary<string, T>> _enumMemberValuesLazy =
_enumMemberValues ??= typeof(T) new(
() =>
typeof(T)
.GetFields() .GetFields()
.Where(field => field.IsStatic) .Where(field => field.IsStatic)
.Select( .Select(
field => field =>
new new
{ {
Field = field, FieldName = field.Name,
Attribute = field FieldValue = (T)field.GetValue(null)!,
EnumMemberValue = field
.GetCustomAttributes<EnumMemberAttribute>(false) .GetCustomAttributes<EnumMemberAttribute>(false)
.FirstOrDefault() .FirstOrDefault()
?.Value?.ToString()
} }
) )
.Where(field => field.Attribute != null) .ToDictionary(x => x.EnumMemberValue ?? x.FieldName, x => x.FieldValue)
.ToDictionary( );
field => field.Attribute!.Value!.ToString(),
field => (T)field.Field.GetValue(null)! /// <summary>
/// Gets a dictionary of enum member values, keyed by the EnumMember attribute value, or the field name if no EnumMember attribute is present.
/// </summary>
private Dictionary<string, T> EnumMemberValues => _enumMemberValuesLazy.Value;
/// <summary>
/// Lazy initialization for <see cref="EnumMemberNames"/>.
/// </summary>
private readonly Lazy<Dictionary<T, string>> _enumMemberNamesLazy;
/// <summary>
/// Gets a dictionary of enum member names, keyed by the enum member value.
/// </summary>
private Dictionary<T, string> EnumMemberNames => _enumMemberNamesLazy.Value;
/// <summary>
/// Gets the value of the "Unknown" enum member, or the 0 value if no "Unknown" member is present.
/// </summary>
private T UnknownValue =>
EnumMemberValues.TryGetValue("Unknown", out var res) ? res : (T)Enum.ToObject(typeof(T), 0);
/// <inheritdoc />
public override bool HandleNull => true;
public DefaultUnknownEnumConverter()
{
_enumMemberNamesLazy = new Lazy<Dictionary<T, string>>(
() => EnumMemberValues.ToDictionary(x => x.Value, x => x.Key)
); );
}
/// <inheritdoc />
public override T Read( public override T Read(
ref Utf8JsonReader reader, ref Utf8JsonReader reader,
Type typeToConvert, Type typeToConvert,
JsonSerializerOptions options JsonSerializerOptions options
) )
{ {
if (reader.TokenType != JsonTokenType.String) if (reader.TokenType is not (JsonTokenType.String or JsonTokenType.PropertyName))
{ {
throw new JsonException(); throw new JsonException("Expected String or PropertyName token");
} }
var enumText = reader.GetString()?.Replace(" ", "_"); if (reader.GetString() is { } readerString)
if (Enum.TryParse(typeof(T), enumText, true, out var result)) {
// First try get exact match
if (EnumMemberValues.TryGetValue(readerString, out var enumMemberValue))
{ {
return (T)result!; return enumMemberValue;
} }
// Try using enum member values // Otherwise try get case-insensitive match
if (enumText != null) if (
{ EnumMemberValues.Keys.FirstOrDefault(
if (EnumMemberValues.TryGetValue(enumText, out var enumMemberResult)) key => key.Equals(readerString, StringComparison.OrdinalIgnoreCase)
) is
{ } enumMemberName
)
{ {
return enumMemberResult; return EnumMemberValues[enumMemberName];
}
} }
// Unknown value handling Debug.WriteLine($"Unknown enum member value for {typeToConvert}: {readerString}");
if (Enum.TryParse(typeof(T), "Unknown", true, out var unknownResult))
{
return (T)unknownResult!;
} }
throw new JsonException($"Unable to parse '{enumText}' to enum '{typeof(T)}'."); return UnknownValue;
} }
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
{ {
if (value == null) if (value == null)
@ -75,7 +113,7 @@ public class DefaultUnknownEnumConverter<T> : JsonConverter<T>
return; return;
} }
writer.WriteStringValue(value.GetStringValue().Replace("_", " ")); writer.WriteStringValue(EnumMemberNames[value]);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -83,41 +121,12 @@ public class DefaultUnknownEnumConverter<T> : JsonConverter<T>
ref Utf8JsonReader reader, ref Utf8JsonReader reader,
Type typeToConvert, Type typeToConvert,
JsonSerializerOptions options JsonSerializerOptions options
) ) => Read(ref reader, typeToConvert, options);
{
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
var enumText = reader.GetString()?.Replace(" ", "_");
if (Enum.TryParse(typeof(T), enumText, true, out var result))
{
return (T)result!;
}
// Unknown value handling
if (Enum.TryParse(typeof(T), "Unknown", true, out var unknownResult))
{
return (T)unknownResult!;
}
throw new JsonException($"Unable to parse '{enumText}' to enum '{typeof(T)}'.");
}
/// <inheritdoc /> /// <inheritdoc />
public override void WriteAsPropertyName( public override void WriteAsPropertyName(
Utf8JsonWriter writer, Utf8JsonWriter writer,
T? value, T? value,
JsonSerializerOptions options JsonSerializerOptions options
) ) => Write(writer, value, options);
{
if (value == null)
{
writer.WriteNullValue();
return;
}
writer.WritePropertyName(value.GetStringValue().Replace("_", " "));
}
} }

30
StabilityMatrix.Core/Helper/ArchiveHelper.cs

@ -86,22 +86,16 @@ public static partial class ArchiveHelper
public static async Task<ArchiveInfo> Extract7Z(string archivePath, string extractDirectory) public static async Task<ArchiveInfo> Extract7Z(string archivePath, string extractDirectory)
{ {
var args = var result = await ProcessRunner
$"x {ProcessRunner.Quote(archivePath)} -o{ProcessRunner.Quote(extractDirectory)} -y"; .GetProcessResultAsync(
SevenZipPath,
new[] { "x", archivePath, "-o" + ProcessRunner.Quote(extractDirectory), "-y" }
)
.ConfigureAwait(false);
Logger.Debug($"Starting process '{SevenZipPath}' with arguments '{args}'"); result.EnsureSuccessExitCode();
using var process = new Process(); var output = result.StandardOutput ?? "";
process.StartInfo = new ProcessStartInfo(SevenZipPath, args)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
process.Start();
await ProcessRunner.WaitForExitConditionAsync(process);
var output = await process.StandardOutput.ReadToEndAsync();
try try
{ {
@ -153,8 +147,12 @@ public static partial class ArchiveHelper
$"x {ProcessRunner.Quote(archivePath)} -o{ProcessRunner.Quote(extractDirectory)} -y -bsp1"; $"x {ProcessRunner.Quote(archivePath)} -o{ProcessRunner.Quote(extractDirectory)} -y -bsp1";
Logger.Debug($"Starting process '{SevenZipPath}' with arguments '{args}'"); Logger.Debug($"Starting process '{SevenZipPath}' with arguments '{args}'");
var process = ProcessRunner.StartProcess(SevenZipPath, args, outputDataReceived: onOutput); using var process = ProcessRunner.StartProcess(
await ProcessRunner.WaitForExitConditionAsync(process); SevenZipPath,
args,
outputDataReceived: onOutput
);
await ProcessRunner.WaitForExitConditionAsync(process).ConfigureAwait(false);
progress.Report(new ProgressReport(1f, "Finished extracting", type: ProgressType.Extract)); progress.Report(new ProgressReport(1f, "Finished extracting", type: ProgressType.Extract));

9
StabilityMatrix.Core/Models/Api/CivitCreator.cs

@ -2,11 +2,14 @@
namespace StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Models.Api;
public class CivitCreator public record CivitCreator
{ {
[JsonPropertyName("username")] [JsonPropertyName("username")]
public string Username { get; set; } public string? Username { get; init; }
[JsonPropertyName("image")] [JsonPropertyName("image")]
public string? Image { get; set; } public string? Image { get; init; }
[JsonIgnore]
public string? ProfileUrl => Username is null ? null : $"https://civitai.com/user/{Username}";
} }

9
StabilityMatrix.Core/Models/Api/CivitFileType.cs

@ -1,4 +1,5 @@
using System.Text.Json.Serialization; using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Converters.Json;
namespace StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Models.Api;
@ -6,8 +7,10 @@ namespace StabilityMatrix.Core.Models.Api;
[JsonConverter(typeof(DefaultUnknownEnumConverter<CivitFileType>))] [JsonConverter(typeof(DefaultUnknownEnumConverter<CivitFileType>))]
public enum CivitFileType public enum CivitFileType
{ {
Unknown,
Model, Model,
VAE, VAE,
Training_Data,
Unknown, [EnumMember(Value = "Training Data")]
TrainingData
} }

6
StabilityMatrix.Core/Models/Api/CivitModelFormat.cs

@ -1,12 +1,14 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using StabilityMatrix.Core.Converters.Json;
namespace StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Models.Api;
[JsonConverter(typeof(DefaultUnknownEnumConverter<CivitModelFormat>))]
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CivitModelFormat public enum CivitModelFormat
{ {
Unknown,
SafeTensor, SafeTensor,
PickleTensor, PickleTensor,
Diffusers,
Other Other
} }

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

@ -9,29 +9,37 @@ namespace StabilityMatrix.Core.Models.Api;
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
public enum CivitModelType public enum CivitModelType
{ {
Unknown,
[ConvertTo<SharedFolderType>(SharedFolderType.StableDiffusion)] [ConvertTo<SharedFolderType>(SharedFolderType.StableDiffusion)]
Checkpoint, Checkpoint,
[ConvertTo<SharedFolderType>(SharedFolderType.TextualInversion)] [ConvertTo<SharedFolderType>(SharedFolderType.TextualInversion)]
TextualInversion, TextualInversion,
[ConvertTo<SharedFolderType>(SharedFolderType.Hypernetwork)] [ConvertTo<SharedFolderType>(SharedFolderType.Hypernetwork)]
Hypernetwork, Hypernetwork,
[ConvertTo<SharedFolderType>(SharedFolderType.Lora)] [ConvertTo<SharedFolderType>(SharedFolderType.Lora)]
LORA, LORA,
[ConvertTo<SharedFolderType>(SharedFolderType.ControlNet)] [ConvertTo<SharedFolderType>(SharedFolderType.ControlNet)]
Controlnet, Controlnet,
[ConvertTo<SharedFolderType>(SharedFolderType.LyCORIS)] [ConvertTo<SharedFolderType>(SharedFolderType.LyCORIS)]
LoCon, LoCon,
[ConvertTo<SharedFolderType>(SharedFolderType.VAE)] [ConvertTo<SharedFolderType>(SharedFolderType.VAE)]
VAE, VAE,
// Unused/obsolete/unknown/meta options // Unused/obsolete/unknown/meta options
AestheticGradient, AestheticGradient,
Model, Model,
MotionModule,
Poses, Poses,
Upscaler, Upscaler,
Wildcards, Wildcards,
Workflows, Workflows,
Other, Other,
All, All
Unknown
} }

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

@ -149,7 +149,7 @@ public class A3WebUI : BaseGitPackage
Name = "No Half", Name = "No Half",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
Description = "Do not switch the model to 16-bit floats", Description = "Do not switch the model to 16-bit floats",
InitialValue = HardwareHelper.HasAmdGpu(), InitialValue = HardwareHelper.PreferRocm() || HardwareHelper.PreferDirectML(),
Options = new() { "--no-half" } Options = new() { "--no-half" }
}, },
new() new()

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

@ -27,10 +27,8 @@ public class FocusControlNet : Fooocus
public override string Author => "fenneishi"; public override string Author => "fenneishi";
public override string Blurb => public override string Blurb =>
"Fooocus-ControlNet adds more control to the original Fooocus software."; "Fooocus-ControlNet adds more control to the original Fooocus software.";
public override string LicenseType => "GPL-3.0";
public override string LicenseUrl => public override string LicenseUrl =>
"https://github.com/fenneishi/Fooocus-ControlNet-SDXL/blob/main/LICENSE"; "https://github.com/fenneishi/Fooocus-ControlNet-SDXL/blob/main/LICENSE";
public override string LaunchCommand => "launch.py";
public override Uri PreviewImageUri => public override Uri PreviewImageUri =>
new("https://github.com/fenneishi/Fooocus-ControlNet-SDXL/raw/main/asset/canny/snip.png"); new("https://github.com/fenneishi/Fooocus-ControlNet-SDXL/raw/main/asset/canny/snip.png");
public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Expert; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Expert;

72
StabilityMatrix.Core/Models/Packages/RuinedFooocus.cs

@ -0,0 +1,72 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.Packages;
[Singleton(typeof(BasePackage))]
public class RuinedFooocus : Fooocus
{
public RuinedFooocus(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
public override string Name => "RuinedFooocus";
public override string DisplayName { get; set; } = "RuinedFooocus";
public override string Author => "runew0lf";
public override string Blurb =>
"RuinedFooocus combines the best aspects of Stable Diffusion and Midjourney into one seamless, cutting-edge experience";
public override string LicenseUrl =>
"https://github.com/runew0lf/RuinedFooocus/blob/main/LICENSE";
public override Uri PreviewImageUri =>
new("https://raw.githubusercontent.com/runew0lf/pmmconfigs/main/RuinedFooocus_ss.png");
public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Expert;
public override async Task InstallPackage(
string installLocation,
TorchVersion torchVersion,
SharedFolderMethod selectedSharedFolderMethod,
DownloadPackageVersionOptions versionOptions,
IProgress<ProgressReport>? progress = null,
Action<ProcessOutput>? onConsoleOutput = null
)
{
if (torchVersion == TorchVersion.Cuda)
{
var venvRunner = await SetupVenv(installLocation, forceRecreate: true)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true));
await InstallCudaTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false);
var requirements = new FilePath(installLocation, "requirements_versions.txt");
await venvRunner
.PipInstallFromRequirements(requirements, onConsoleOutput, excludes: "torch")
.ConfigureAwait(false);
}
else
{
await base.InstallPackage(
installLocation,
torchVersion,
selectedSharedFolderMethod,
versionOptions,
progress,
onConsoleOutput
)
.ConfigureAwait(false);
}
}
}

1
StabilityMatrix.Core/Models/Settings/TeachingTip.cs

@ -11,6 +11,7 @@ public record TeachingTip(string Value) : StringValue(Value)
{ {
public static TeachingTip AccountsCredentialsStorageNotice => public static TeachingTip AccountsCredentialsStorageNotice =>
new("AccountsCredentialsStorageNotice"); new("AccountsCredentialsStorageNotice");
public static TeachingTip CheckpointCategoriesTip => new("CheckpointCategoriesTip");
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()

3
StabilityMatrix.Core/Models/SharedFolderType.cs

@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using StabilityMatrix.Core.Extensions;
namespace StabilityMatrix.Core.Models; namespace StabilityMatrix.Core.Models;
@ -21,6 +22,8 @@ public enum SharedFolderType
ApproxVAE = 1 << 11, ApproxVAE = 1 << 11,
Karlo = 1 << 12, Karlo = 1 << 12,
DeepDanbooru = 1 << 13, DeepDanbooru = 1 << 13,
[Description("TextualInversion (Embeddings)")]
TextualInversion = 1 << 14, TextualInversion = 1 << 14,
Hypernetwork = 1 << 15, Hypernetwork = 1 << 15,
ControlNet = 1 << 16, ControlNet = 1 << 16,

64
StabilityMatrix.Core/Processes/ProcessRunner.cs

@ -402,66 +402,40 @@ public static class ProcessRunner
} }
/// <summary> /// <summary>
/// Check if the process exited with the expected exit code. /// Waits for process to exit, then validates exit code.
/// </summary> /// </summary>
/// <param name="process">Process to check.</param> /// <param name="process">Process to check.</param>
/// <param name="expectedExitCode">Expected exit code.</param> /// <param name="expectedExitCode">Expected exit code.</param>
/// <param name="stdout">Process stdout.</param> /// <param name="cancelToken">Cancellation token.</param>
/// <param name="stderr">Process stderr.</param>
/// <exception cref="ProcessException">Thrown if exit code does not match expected value.</exception> /// <exception cref="ProcessException">Thrown if exit code does not match expected value.</exception>
// ReSharper disable once MemberCanBePrivate.Global public static async Task WaitForExitConditionAsync(
public static Task ValidateExitConditionAsync(
Process process, Process process,
int expectedExitCode = 0, int expectedExitCode = 0,
string? stdout = null, CancellationToken cancelToken = default
string? stderr = null
) )
{ {
var exitCode = process.ExitCode; if (!process.HasExited)
if (exitCode == expectedExitCode)
{ {
return Task.CompletedTask; await process.WaitForExitAsync(cancelToken).ConfigureAwait(false);
} }
var pName = process.StartInfo.FileName; if (process.ExitCode == expectedExitCode)
var msg = {
$"Process {pName} failed with exit-code {exitCode}. stdout: '{stdout}', stderr: '{stderr}'"; return;
Logger.Error(msg);
throw new ProcessException(msg);
} }
/// <summary> // Accessing ProcessName may error on some platforms
/// Waits for process to exit, then validates exit code. string? processName = null;
/// </summary> try
/// <param name="process">Process to check.</param>
/// <param name="expectedExitCode">Expected exit code.</param>
/// <param name="cancelToken">Cancellation token.</param>
/// <exception cref="ProcessException">Thrown if exit code does not match expected value.</exception>
public static async Task WaitForExitConditionAsync(
Process process,
int expectedExitCode = 0,
CancellationToken cancelToken = default
)
{
if (process is AnsiProcess)
{ {
throw new ArgumentException( processName = process.ProcessName;
$"{nameof(WaitForExitConditionAsync)} does not support AnsiProcess, which uses custom async data reading",
nameof(process)
);
} }
catch (SystemException) { }
var stdout = new StringBuilder(); throw new ProcessException(
var stderr = new StringBuilder(); "Process "
process.OutputDataReceived += (_, args) => stdout.Append(args.Data); + (processName == null ? "" : processName + " ")
process.ErrorDataReceived += (_, args) => stderr.Append(args.Data); + $"failed with exit-code {process.ExitCode}."
await process.WaitForExitAsync(cancelToken).ConfigureAwait(false); );
await ValidateExitConditionAsync(
process,
expectedExitCode,
stdout.ToString(),
stderr.ToString()
)
.ConfigureAwait(false);
} }
} }

81
StabilityMatrix.Tests/Core/DefaultUnknownEnumConverterTests.cs

@ -0,0 +1,81 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using StabilityMatrix.Core.Converters.Json;
namespace StabilityMatrix.Tests.Core;
[TestClass]
public class DefaultUnknownEnumConverterTests
{
[TestMethod]
[ExpectedException(typeof(JsonException))]
public void TestDeserialize_NormalEnum_ShouldError()
{
const string json = "\"SomeUnknownValue\"";
JsonSerializer.Deserialize<NormalEnum>(json);
}
[TestMethod]
public void TestDeserialize_UnknownEnum_ShouldConvert()
{
const string json = "\"SomeUnknownValue\"";
var result = JsonSerializer.Deserialize<UnknownEnum>(json);
Assert.AreEqual(UnknownEnum.Unknown, result);
}
[TestMethod]
public void TestDeserialize_DefaultEnum_ShouldConvert()
{
const string json = "\"SomeUnknownValue\"";
var result = JsonSerializer.Deserialize<DefaultEnum>(json);
Assert.AreEqual(DefaultEnum.CustomDefault, result);
}
[TestMethod]
public void TestSerialize_UnknownEnum_ShouldConvert()
{
const string expected = "\"Unknown\"";
var result = JsonSerializer.Serialize(UnknownEnum.Unknown);
Assert.AreEqual(expected, result);
}
[TestMethod]
public void TestSerialize_DefaultEnum_ShouldConvert()
{
const string expected = "\"CustomDefault\"";
var result = JsonSerializer.Serialize(DefaultEnum.CustomDefault);
Assert.AreEqual(expected, result);
}
private enum NormalEnum
{
Unknown,
Value1,
Value2
}
[JsonConverter(typeof(DefaultUnknownEnumConverter<UnknownEnum>))]
private enum UnknownEnum
{
Unknown,
Value1,
Value2
}
[JsonConverter(typeof(DefaultUnknownEnumConverter<DefaultEnum>))]
private enum DefaultEnum
{
CustomDefault,
Value1,
Value2
}
}
Loading…
Cancel
Save