Browse Source

Merge pull request #275 from ionite34/inference-metadata

pull/165/head
Ionite 1 year ago committed by GitHub
parent
commit
3a55b79d4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 45
      StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs
  2. 27
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  3. 9
      StabilityMatrix.Avalonia/Languages/Resources.resx
  4. 15
      StabilityMatrix.Avalonia/Models/IParametersLoadableState.cs
  5. 2
      StabilityMatrix.Avalonia/Models/Inference/LoadViewStateEventArgs.cs
  6. 5
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  7. 1
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
  8. 183
      StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs
  9. 28
      StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
  10. 49
      StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
  11. 20
      StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs
  12. 79
      StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs
  13. 16
      StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml
  14. 13
      StabilityMatrix.Avalonia/Views/InferencePage.axaml
  15. 70
      StabilityMatrix.Core/Helper/GenerationParametersConverter.cs
  16. 25
      StabilityMatrix.Core/Helper/ImageMetadata.cs
  17. 81
      StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs
  18. 25
      StabilityMatrix.Core/Models/Api/Comfy/ComfySamplerScheduler.cs
  19. 6
      StabilityMatrix.Core/Models/Api/Comfy/ComfyScheduler.cs
  20. 46
      StabilityMatrix.Core/Models/GenerationParameters.cs
  21. 3
      StabilityMatrix.sln.DotSettings

45
StabilityMatrix.Avalonia/Controls/Dock/DockUserControlBase.cs

@ -3,6 +3,7 @@ using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Threading;
@ -10,6 +11,7 @@ using Dock.Avalonia.Controls;
using Dock.Model;
using Dock.Model.Avalonia.Json;
using Dock.Model.Core;
using Dock.Serializer;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.ViewModels.Base;
@ -21,22 +23,23 @@ namespace StabilityMatrix.Avalonia.Controls.Dock;
/// </summary>
public abstract class DockUserControlBase : DropTargetUserControlBase
{
protected DockControl? BaseDock;
protected readonly AvaloniaDockSerializer DockSerializer = new();
protected readonly DockState DockState = new();
private DockControl? baseDock;
private readonly DockSerializer dockSerializer = new(typeof(AvaloniaList<>));
private readonly DockState dockState = new();
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
BaseDock =
baseDock =
this.FindControl<DockControl>("Dock")
?? throw new NullReferenceException("DockControl not found");
if (BaseDock.Layout is { } layout)
if (baseDock.Layout is { } layout)
{
Dispatcher.UIThread.Post(() => DockState.Save(layout), DispatcherPriority.Background);
dockState.Save(layout);
// Dispatcher.UIThread.Post(() => dockState.Save(layout), DispatcherPriority.Background);
}
}
@ -81,31 +84,45 @@ public abstract class DockUserControlBase : DropTargetUserControlBase
private void DataContext_OnLoadViewStateRequested(object? sender, LoadViewStateEventArgs args)
{
if (args.State.DockLayout is { } layout)
if (args.State?.DockLayout is { } layout)
{
// Provided
LoadDockLayout(layout);
}
else
{
// Restore default
RestoreDockLayout();
}
}
protected void LoadDockLayout(JsonObject data)
private void LoadDockLayout(JsonObject data)
{
LoadDockLayout(data.ToJsonString());
}
protected void LoadDockLayout(string data)
private void LoadDockLayout(string data)
{
if (BaseDock is null)
if (baseDock is null)
return;
if (DockSerializer.Deserialize<IDock?>(data) is { } layout)
if (dockSerializer.Deserialize<IDock?>(data) is { } layout)
{
baseDock.Layout = layout;
}
}
private void RestoreDockLayout()
{
// TODO: idk this doesn't work
if (baseDock?.Layout != null)
{
BaseDock.Layout = layout;
DockState.Restore(layout);
dockState.Restore(baseDock.Layout);
}
}
protected string? SaveDockLayout()
{
return BaseDock is null ? null : DockSerializer.Serialize(BaseDock.Layout);
return baseDock is null ? null : dockSerializer.Serialize(baseDock.Layout);
}
}

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

@ -257,6 +257,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Open Project....
/// </summary>
public static string Action_OpenProjectEllipsis {
get {
return ResourceManager.GetString("Action_OpenProjectEllipsis", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open Web UI.
/// </summary>
@ -329,6 +338,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Restore Default Layout.
/// </summary>
public static string Action_RestoreDefaultLayout {
get {
return ResourceManager.GetString("Action_RestoreDefaultLayout", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Retry.
/// </summary>
@ -347,6 +365,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Save As....
/// </summary>
public static string Action_SaveAsEllipsis {
get {
return ResourceManager.GetString("Action_SaveAsEllipsis", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search.
/// </summary>

9
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -669,4 +669,13 @@
<data name="Label_ReleaseNotes" xml:space="preserve">
<value>Release Notes</value>
</data>
<data name="Action_OpenProjectEllipsis" xml:space="preserve">
<value>Open Project...</value>
</data>
<data name="Action_SaveAsEllipsis" xml:space="preserve">
<value>Save As...</value>
</data>
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Restore Default Layout</value>
</data>
</root>

15
StabilityMatrix.Avalonia/Models/IParametersLoadableState.cs

@ -0,0 +1,15 @@
using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.Models;
public interface IParametersLoadableState
{
void LoadStateFromParameters(GenerationParameters parameters);
GenerationParameters SaveStateToParameters(GenerationParameters parameters);
public GenerationParameters SaveStateToParameters()
{
return SaveStateToParameters(new GenerationParameters());
}
}

2
StabilityMatrix.Avalonia/Models/Inference/LoadViewStateEventArgs.cs

@ -7,5 +7,5 @@ namespace StabilityMatrix.Avalonia.Models.Inference;
/// </summary>
public class LoadViewStateEventArgs : EventArgs
{
public required ViewState State { get; init; }
public ViewState? State { get; init; }
}

5
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -8,7 +8,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>./Assets/Icon.ico</ApplicationIcon>
<Version>2.5.0-dev.5</Version>
<Version>2.5.0-pre.2</Version>
<InformationalVersion>$(Version)</InformationalVersion>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@ -33,8 +33,9 @@
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Dock.Avalonia" Version="11.0.0.1" />
<PackageReference Include="Dock.Avalonia" Version="11.0.0.2" />
<PackageReference Include="Dock.Model.Avalonia" Version="11.0.0.2" />
<PackageReference Include="Dock.Serializer" Version="11.0.0.2" />
<PackageReference Include="DynamicData" Version="7.14.2" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.4" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.217" />

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

@ -60,6 +60,7 @@ public abstract partial class InferenceGenerationViewModelBase
IInferenceClientManager inferenceClientManager,
INotificationService notificationService
)
: base(notificationService)
{
this.notificationService = notificationService;
this.vmFactory = vmFactory;

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

@ -1,18 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Input;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media.Animation;
using Microsoft.Extensions.DependencyInjection;
using NLog;
using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
@ -26,6 +37,10 @@ public abstract partial class InferenceTabViewModelBase
IPersistentViewProvider,
IDropTarget
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly INotificationService notificationService;
/// <summary>
/// The title of the tab
/// </summary>
@ -66,6 +81,8 @@ public abstract partial class InferenceTabViewModelBase
protected void LoadViewState(LoadViewStateEventArgs args) =>
loadViewStateRequestedEventManager?.RaiseEvent(this, args, nameof(LoadViewStateRequested));
protected void ResetViewState() => LoadViewState(new LoadViewStateEventArgs());
private WeakEventManager<SaveViewStateEventArgs>? saveViewStateRequestedEventManager;
public event EventHandler<SaveViewStateEventArgs> SaveViewStateRequested
@ -99,6 +116,23 @@ public abstract partial class InferenceTabViewModelBase
#endregion
protected InferenceTabViewModelBase(INotificationService notificationService)
{
this.notificationService = notificationService;
}
[RelayCommand]
private void RestoreDefaultViewState()
{
// ResetViewState();
// TODO: Dock reset not working, using this hack for now to get a new view
var navService = App.Services.GetRequiredService<INavigationService>();
navService.NavigateTo<LaunchPageViewModel>(new SuppressNavigationTransitionInfo());
((IPersistentViewProvider)this).AttachedPersistentView = null;
navService.NavigateTo<InferenceViewModel>(new BetterEntranceNavigationTransition());
}
[RelayCommand]
private async Task DebugSaveViewState()
{
@ -144,6 +178,79 @@ public abstract partial class InferenceTabViewModelBase
GC.SuppressFinalize(this);
}
/// <summary>
/// Loads image and metadata from a file path
/// </summary>
/// <remarks>This is safe to call from non-UI threads</remarks>
/// <param name="filePath">File path</param>
/// <exception cref="FileNotFoundException"></exception>
/// <exception cref="ApplicationException"></exception>
/// <exception cref="InvalidOperationException"></exception>
private void LoadImageMetadata(FilePath? filePath)
{
if (filePath is not { Exists: true })
{
throw new FileNotFoundException("File does not exist", filePath?.FullPath);
}
var metadata = ImageMetadata.GetAllFileMetadata(filePath);
// Has SMProject metadata
if (metadata.SMProject is not null)
{
var project = JsonSerializer.Deserialize<InferenceProjectDocument>(metadata.SMProject);
// Check project type matches
if (project?.ProjectType.ToViewModelType() == GetType() && project.State is not null)
{
Dispatcher.UIThread.Invoke(() => LoadStateFromJsonObject(project.State));
}
else
{
throw new ApplicationException("Unsupported project type");
}
// Load image
if (this is IImageGalleryComponent imageGalleryComponent)
{
imageGalleryComponent.LoadImagesToGallery(new ImageSource(filePath));
}
}
// Has generic metadata
else if (metadata.Parameters is { } parametersString)
{
if (!GenerationParameters.TryParse(parametersString, out var parameters))
{
throw new ApplicationException("Failed to parse parameters");
}
if (this is IParametersLoadableState paramsLoadableVm)
{
Dispatcher.UIThread.Invoke(
() => paramsLoadableVm.LoadStateFromParameters(parameters)
);
}
else
{
throw new InvalidOperationException(
"Load parameters target does not implement IParametersLoadableState"
);
}
// Load image
if (this is IImageGalleryComponent imageGalleryComponent)
{
Dispatcher.UIThread.Invoke(
() => imageGalleryComponent.LoadImagesToGallery(new ImageSource(filePath))
);
}
}
else
{
throw new ApplicationException("File does not contain any metadata");
}
}
/// <inheritdoc />
public void DragOver(object? sender, DragEventArgs e)
{
@ -162,10 +269,10 @@ public abstract partial class InferenceTabViewModelBase
if (e.Data.GetDataFormats().Contains(DataFormats.Files))
{
e.Handled = true;
e.DragEffects = DragDropEffects.None;
return;
}
// Other kinds - not supported
e.DragEffects = DragDropEffects.None;
}
@ -181,30 +288,42 @@ public abstract partial class InferenceTabViewModelBase
Dispatcher.UIThread.Post(() =>
{
var metadata = imageFile.ReadMetadata();
if (metadata.SMProject is not null)
try
{
var project = JsonSerializer.Deserialize<InferenceProjectDocument>(
metadata.SMProject
);
// Check project type matches
if (
project?.ProjectType.ToViewModelType() == GetType()
&& project.State is not null
)
var metadata = imageFile.ReadMetadata();
if (metadata.SMProject is not null)
{
LoadStateFromJsonObject(project.State);
}
// Load image
if (this is IImageGalleryComponent imageGalleryComponent)
{
imageGalleryComponent.LoadImagesToGallery(
new ImageSource(imageFile.GlobalFullPath)
var project = JsonSerializer.Deserialize<InferenceProjectDocument>(
metadata.SMProject
);
// Check project type matches
if (
project?.ProjectType.ToViewModelType() == GetType()
&& project.State is not null
)
{
LoadStateFromJsonObject(project.State);
}
// Load image
if (this is IImageGalleryComponent imageGalleryComponent)
{
imageGalleryComponent.LoadImagesToGallery(
new ImageSource(imageFile.GlobalFullPath)
);
}
}
}
catch (Exception ex)
{
Logger.Warn(ex, "Failed to load image from context drop");
notificationService.ShowPersistent(
$"Could not parse image metadata",
$"{imageFile.FileName} - {ex.Message}",
NotificationType.Warning
);
}
});
return;
@ -214,6 +333,30 @@ public abstract partial class InferenceTabViewModelBase
if (e.Data.GetDataFormats().Contains(DataFormats.Files))
{
e.Handled = true;
if (e.Data.Get(DataFormats.Files) is IEnumerable<IStorageItem> files)
{
if (files.Select(f => f.TryGetLocalPath()).FirstOrDefault() is { } path)
{
var file = new FilePath(path);
Dispatcher.UIThread.Post(() =>
{
try
{
LoadImageMetadata(file);
}
catch (Exception ex)
{
Logger.Warn(ex, "Failed to load image from OS file drop");
notificationService.ShowPersistent(
$"Could not parse image metadata",
$"{file.Name} - {ex.Message}",
NotificationType.Warning
);
}
});
}
}
}
}
}

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

@ -25,7 +25,9 @@ using InferenceTextToImageView = StabilityMatrix.Avalonia.Views.Inference.Infere
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(InferenceTextToImageView), persistent: true)]
public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase
public class InferenceTextToImageViewModel
: InferenceGenerationViewModelBase,
IParametersLoadableState
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@ -327,4 +329,28 @@ public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase
await RunGeneration(generationArgs, cancellationToken);
}
/// <inheritdoc />
public void LoadStateFromParameters(GenerationParameters parameters)
{
PromptCardViewModel.LoadStateFromParameters(parameters);
SamplerCardViewModel.LoadStateFromParameters(parameters);
SeedCardViewModel.Seed = Convert.ToInt64(parameters.Seed);
ModelCardViewModel.LoadStateFromParameters(parameters);
}
/// <inheritdoc />
public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
{
parameters = PromptCardViewModel.SaveStateToParameters(parameters);
parameters = SamplerCardViewModel.SaveStateToParameters(parameters);
parameters.Seed = (ulong)SeedCardViewModel.Seed;
parameters = ModelCardViewModel.SaveStateToParameters(parameters);
return parameters;
}
}

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

@ -1,7 +1,9 @@
using System.Linq;
using System;
using System.Linq;
using System.Text.Json.Nodes;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
@ -10,7 +12,7 @@ using StabilityMatrix.Core.Models;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(ModelCard))]
public partial class ModelCardViewModel : LoadableViewModelBase
public partial class ModelCardViewModel : LoadableViewModelBase, IParametersLoadableState
{
[ObservableProperty]
private HybridModelFile? selectedModel;
@ -71,4 +73,47 @@ public partial class ModelCardViewModel : LoadableViewModelBase
public string? SelectedVaeName { get; init; }
public bool IsVaeSelectionEnabled { get; init; }
}
/// <inheritdoc />
public void LoadStateFromParameters(GenerationParameters parameters)
{
if (parameters.ModelName is not { } paramsModelName)
return;
var currentModels = ClientManager.Models;
HybridModelFile? model;
// First try hash match
if (parameters.ModelHash is not null)
{
model = currentModels.FirstOrDefault(
m =>
m.Local?.ConnectedModelInfo?.Hashes.SHA256 is { } sha256
&& sha256.StartsWith(
parameters.ModelHash,
StringComparison.InvariantCultureIgnoreCase
)
);
}
else
{
// Name matches
model = currentModels.FirstOrDefault(m => m.FileName.EndsWith(paramsModelName));
model ??= currentModels.FirstOrDefault(
m => m.ShortDisplayName.StartsWith(paramsModelName)
);
}
if (model is not null)
{
SelectedModel = model;
}
}
/// <inheritdoc />
public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
{
return parameters with { ModelName = SelectedModel?.FileName };
}
}

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

@ -22,12 +22,13 @@ using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(PromptCard))]
public partial class PromptCardViewModel : LoadableViewModelBase
public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoadableState
{
private readonly IModelIndexService modelIndexService;
@ -284,4 +285,21 @@ public partial class PromptCardViewModel : LoadableViewModelBase
PromptDocument.Text = model.Prompt ?? "";
NegativePromptDocument.Text = model.NegativePrompt ?? "";
}
/// <inheritdoc />
public void LoadStateFromParameters(GenerationParameters parameters)
{
PromptDocument.Text = parameters.PositivePrompt ?? "";
NegativePromptDocument.Text = parameters.NegativePrompt ?? "";
}
/// <inheritdoc />
public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
{
return parameters with
{
PositivePrompt = PromptDocument.Text,
NegativePrompt = NegativePromptDocument.Text
};
}
}

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

@ -1,15 +1,19 @@
using System.Text.Json.Serialization;
using System.Linq;
using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api.Comfy;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(SamplerCard))]
public partial class SamplerCardViewModel : LoadableViewModelBase
public partial class SamplerCardViewModel : LoadableViewModelBase, IParametersLoadableState
{
[ObservableProperty]
private bool isRefinerStepsEnabled;
@ -61,42 +65,47 @@ public partial class SamplerCardViewModel : LoadableViewModelBase
ClientManager = clientManager;
}
/*/// <inheritdoc />
public override void LoadStateFromJsonObject(JsonObject state)
/// <inheritdoc />
public void LoadStateFromParameters(GenerationParameters parameters)
{
var model = DeserializeModel<SamplerCardModel>(state);
Steps = model.Steps;
IsDenoiseStrengthEnabled = model.IsDenoiseStrengthEnabled;
DenoiseStrength = model.DenoiseStrength;
IsCfgScaleEnabled = model.IsCfgScaleEnabled;
CfgScale = model.CfgScale;
IsDimensionsEnabled = model.IsDimensionsEnabled;
Width = model.Width;
Height = model.Height;
IsSamplerSelectionEnabled = model.IsSamplerSelectionEnabled;
SelectedSampler = model.SelectedSampler is null
? null
: new ComfySampler(model.SelectedSampler);
Width = parameters.Width;
Height = parameters.Height;
Steps = parameters.Steps;
CfgScale = parameters.CfgScale;
if (
!string.IsNullOrEmpty(parameters.Sampler)
&& GenerationParametersConverter.TryGetSamplerScheduler(
parameters.Sampler,
out var samplerScheduler
)
)
{
SelectedSampler = ClientManager.Samplers.FirstOrDefault(
s => s == samplerScheduler.Sampler
);
SelectedScheduler = ClientManager.Schedulers.FirstOrDefault(
s => s == samplerScheduler.Scheduler
);
}
}
/// <inheritdoc />
public override JsonObject SaveStateToJsonObject()
public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
{
return SerializeModel(
new SamplerCardModel
{
Steps = Steps,
IsDenoiseStrengthEnabled = IsDenoiseStrengthEnabled,
DenoiseStrength = DenoiseStrength,
IsCfgScaleEnabled = IsCfgScaleEnabled,
CfgScale = CfgScale,
IsDimensionsEnabled = IsDimensionsEnabled,
Width = Width,
Height = Height,
IsSamplerSelectionEnabled = IsSamplerSelectionEnabled,
SelectedSampler = SelectedSampler?.Name
}
);
}*/
var sampler = GenerationParametersConverter.TryGetParameters(
new ComfySamplerScheduler(SelectedSampler ?? default, SelectedScheduler ?? default),
out var res
)
? res
: null;
return parameters with
{
Width = Width,
Height = Height,
Steps = Steps,
CfgScale = CfgScale,
Sampler = sampler,
};
}
}

16
StabilityMatrix.Avalonia/Views/Inference/InferenceTextToImageView.axaml

@ -36,7 +36,9 @@
x:Name="MainLayout"
Id="MainLayout"
Orientation="Horizontal">
<ProportionalDockSplitter x:Name="Splitter1" Id="Splitter1" />
<!-- Left Pane -->
<ToolDock
x:Name="ConfigPane"
@ -55,12 +57,12 @@
</Tool>
</ToolDock>
<ProportionalDockSplitter x:Name="LeftSplitter" Id="LeftSplitter" />
<ProportionalDockSplitter x:Name="Splitter2" Id="Splitter2" />
<!-- Prompt Pane -->
<ToolDock
x:Name="PromptPane"
Alignment="Right"
Alignment="Left"
Id="PromptPane"
Proportion="0.25">
<Tool
@ -149,7 +151,7 @@
</Tool>
</ToolDock>
<ProportionalDockSplitter x:Name="MiddleRightSplitter" Id="MiddleRightSplitter" />
<ProportionalDockSplitter x:Name="Splitter3" Id="Splitter3" />
<!-- Middle Right Pane -->
<ToolDock
@ -191,7 +193,7 @@
</Tool>
</ToolDock>
<ProportionalDockSplitter x:Name="RightSplitter" Id="RightSplitter" />
<ProportionalDockSplitter x:Name="Splitter4" Id="Splitter4" />
<!-- Right Pane -->
<ToolDock
@ -208,12 +210,14 @@
<Grid
x:CompileBindings="False"
DataContext="{Binding ElementName=Dock, Path=DataContext}">
DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DockControl}, Path=DataContext}">
<controls:ImageFolderCard
DataContext="{Binding ImageFolderCardViewModel}" />
</Grid>
</Tool>
</ToolDock>
<ProportionalDockSplitter x:Name="Splitter5" Id="Splitter5" />
</ProportionalDock>
</RootDock>

13
StabilityMatrix.Avalonia/Views/InferencePage.axaml

@ -202,16 +202,25 @@
<ui:MenuFlyoutItem
Command="{Binding MenuOpenProjectCommand}"
InputGesture="Ctrl+O"
Text="Open Project..." />
IconSource="OpenFolder"
Text="{x:Static lang:Resources.Action_OpenProjectEllipsis}" />
<ui:MenuFlyoutSeparator />
<ui:MenuFlyoutItem
Command="{Binding MenuSaveCommand}"
InputGesture="Ctrl+S"
IconSource="Save"
Text="{x:Static lang:Resources.Action_Save}" />
<ui:MenuFlyoutItem
Command="{Binding MenuSaveAsCommand}"
InputGesture="Ctrl+Shift+S"
Text="Save As..." />
Text="{x:Static lang:Resources.Action_SaveAsEllipsis}" />
<ui:MenuFlyoutSeparator />
<ui:MenuFlyoutItem
Command="{Binding SelectedTab.RestoreDefaultViewStateCommand, FallbackValue={x:Null}}"
IconSource="CalendarReply"
Text="{x:Static lang:Resources.Action_RestoreDefaultLayout}" />
<!-- Debug mode menus -->
<ui:MenuFlyoutSeparator IsVisible="{Binding SharedState.IsDebugMode}" />

70
StabilityMatrix.Core/Helper/GenerationParametersConverter.cs

@ -0,0 +1,70 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using StabilityMatrix.Core.Models.Api.Comfy;
namespace StabilityMatrix.Core.Helper;
public static class GenerationParametersConverter
{
private static readonly ImmutableDictionary<
string,
ComfySamplerScheduler
> ParamsToSamplerSchedulers = new Dictionary<string, ComfySamplerScheduler>
{
["DPM++ 2M Karras"] = (ComfySampler.Dpmpp2M, ComfyScheduler.Karras),
["DPM++ SDE Karras"] = (ComfySampler.DpmppSde, ComfyScheduler.Karras),
["DPM++ 2M SDE Exponential"] = (ComfySampler.Dpmpp2MSde, ComfyScheduler.Exponential),
["DPM++ 2M SDE Karras"] = (ComfySampler.Dpmpp2MSde, ComfyScheduler.Karras),
["Euler a"] = (ComfySampler.EulerAncestral, ComfyScheduler.Normal),
["Euler"] = (ComfySampler.Euler, ComfyScheduler.Normal),
["LMS"] = (ComfySampler.LMS, ComfyScheduler.Normal),
["Heun"] = (ComfySampler.Heun, ComfyScheduler.Normal),
["DPM2"] = (ComfySampler.Dpm2, ComfyScheduler.Normal),
["DPM2 Karras"] = (ComfySampler.Dpm2, ComfyScheduler.Karras),
["DPM2 a"] = (ComfySampler.Dpm2Ancestral, ComfyScheduler.Normal),
["DPM2 a Karras"] = (ComfySampler.Dpm2Ancestral, ComfyScheduler.Karras),
["DPM++ 2S a"] = (ComfySampler.Dpmpp2SAncestral, ComfyScheduler.Normal),
["DPM++ 2S a Karras"] = (ComfySampler.Dpmpp2SAncestral, ComfyScheduler.Karras),
["DPM++ 2M"] = (ComfySampler.Dpmpp2M, ComfyScheduler.Normal),
["DPM++ SDE"] = (ComfySampler.DpmppSde, ComfyScheduler.Normal),
["DPM++ 2M SDE"] = (ComfySampler.Dpmpp2MSde, ComfyScheduler.Normal),
["DPM++ 3M SDE"] = (ComfySampler.Dpmpp3MSde, ComfyScheduler.Normal),
["DPM++ 3M SDE Karras"] = (ComfySampler.Dpmpp3MSde, ComfyScheduler.Karras),
["DPM++ 3M SDE Exponential"] = (ComfySampler.Dpmpp3MSde, ComfyScheduler.Exponential),
["DPM fast"] = (ComfySampler.DpmFast, ComfyScheduler.Normal),
["DPM adaptive"] = (ComfySampler.DpmAdaptive, ComfyScheduler.Normal),
["LMS Karras"] = (ComfySampler.LMS, ComfyScheduler.Karras),
["DDIM"] = (ComfySampler.DDIM, ComfyScheduler.Normal),
["UniPC"] = (ComfySampler.UniPC, ComfyScheduler.Normal),
}.ToImmutableDictionary();
private static readonly ImmutableDictionary<
ComfySamplerScheduler,
string
> SamplerSchedulersToParams = ParamsToSamplerSchedulers.ToImmutableDictionary(
x => x.Value,
x => x.Key
);
/// <summary>
/// Converts a parameters-type string to a <see cref="ComfySamplerScheduler"/>.
/// </summary>
public static bool TryGetSamplerScheduler(
string parameters,
out ComfySamplerScheduler samplerScheduler
)
{
return ParamsToSamplerSchedulers.TryGetValue(parameters, out samplerScheduler);
}
/// <summary>
/// Converts a <see cref="ComfySamplerScheduler"/> to a parameters-type string.
/// </summary>
public static bool TryGetParameters(
ComfySamplerScheduler samplerScheduler,
[NotNullWhen(true)] out string? parameters
)
{
return SamplerSchedulersToParams.TryGetValue(samplerScheduler, out parameters);
}
}

25
StabilityMatrix.Core/Helper/ImageMetadata.cs

@ -65,6 +65,29 @@ public class ImageMetadata
return new System.Drawing.Size(imageWidth, imageHeight);
}
public static (
string? Parameters,
string? ParametersJson,
string? SMProject,
string? ComfyNodes
) GetAllFileMetadata(FilePath filePath)
{
using var stream = filePath.Info.OpenRead();
using var reader = new BinaryReader(stream);
var parameters = ReadTextChunk(reader, "parameters");
var parametersJson = ReadTextChunk(reader, "parameters-json");
var smProject = ReadTextChunk(reader, "smproj");
var comfyNodes = ReadTextChunk(reader, "prompt");
return (
string.IsNullOrEmpty(parameters) ? null : parameters,
string.IsNullOrEmpty(parametersJson) ? null : parametersJson,
string.IsNullOrEmpty(smProject) ? null : smProject,
string.IsNullOrEmpty(comfyNodes) ? null : comfyNodes
);
}
public IEnumerable<Tag>? GetTextualData()
{
// Get the PNG-tEXt directory
@ -118,7 +141,7 @@ public class ImageMetadata
{
// skip to end of png header stuff
byteStream.BaseStream.Position = 0x21;
while (byteStream.BaseStream.Position < byteStream.BaseStream.Length)
while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4)
{
var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).Reverse().ToArray());
var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4));

81
StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs

@ -1,39 +1,74 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
namespace StabilityMatrix.Core.Models.Api.Comfy;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public readonly record struct ComfySampler(string Name)
{
private static Dictionary<string, string> ConvertDict { get; } =
public static ComfySampler Euler { get; } = new("euler");
public static ComfySampler EulerAncestral { get; } = new("euler_ancestral");
public static ComfySampler Heun { get; } = new("heun");
public static ComfySampler Dpm2 { get; } = new("dpm_2");
public static ComfySampler Dpm2Ancestral { get; } = new("dpm_2_ancestral");
public static ComfySampler LMS { get; } = new("lms");
public static ComfySampler DpmFast { get; } = new("dpm_fast");
public static ComfySampler DpmAdaptive { get; } = new("dpm_adaptive");
public static ComfySampler Dpmpp2SAncestral { get; } = new("dpmpp_2s_ancestral");
public static ComfySampler DpmppSde { get; } = new("dpmpp_sde");
public static ComfySampler DpmppSdeGpu { get; } = new("dpmpp_sde_gpu");
public static ComfySampler Dpmpp2M { get; } = new("dpmpp_2m");
public static ComfySampler Dpmpp2MSde { get; } = new("dpmpp_2m_sde");
public static ComfySampler Dpmpp2MSdeGpu { get; } = new("dpmpp_2m_sde_gpu");
public static ComfySampler Dpmpp3M { get; } = new("dpmpp_3m");
public static ComfySampler Dpmpp3MSde { get; } = new("dpmpp_3m_sde");
public static ComfySampler Dpmpp3MSdeGpu { get; } = new("dpmpp_3m_sde_gpu");
public static ComfySampler DDIM { get; } = new("ddim");
public static ComfySampler UniPC { get; } = new("uni_pc");
public static ComfySampler UniPCBh2 { get; } = new("uni_pc_bh2");
private static Dictionary<ComfySampler, string> ConvertDict { get; } =
new()
{
["euler"] = "Euler",
["euler_ancestral"] = "Euler Ancestral",
["heun"] = "Heun",
["dpm_2"] = "DPM 2",
["dpm_2_ancestral"] = "DPM 2 Ancestral",
["lms"] = "LMS",
["dpm_fast"] = "DPM Fast",
["dpm_adaptive"] = "DPM Adaptive",
["dpmpp_2s_ancestral"] = "DPM++ 2S Ancestral",
["dpmpp_sde"] = "DPM++ SDE",
["dpmpp_sde_gpu"] = "DPM++ SDE GPU",
["dpmpp_2m"] = "DPM++ 2M",
["dpmpp_2m_sde"] = "DPM++ 2M SDE",
["dpmpp_2m_sde_gpu"] = "DPM++ 2M SDE GPU",
["dpmpp_3m"] = "DPM++ 3M",
["dpmpp_3m_sde"] = "DPM++ 3M SDE",
["dpmpp_3m_sde_gpu"] = "DPM++ 3M SDE GPU",
["ddim"] = "DDIM",
["uni_pc"] = "UniPC",
["uni_pc_bh2"] = "UniPC BH2"
[Euler] = "Euler",
[EulerAncestral] = "Euler Ancestral",
[Heun] = "Heun",
[Dpm2] = "DPM 2",
[Dpm2Ancestral] = "DPM 2 Ancestral",
[LMS] = "LMS",
[DpmFast] = "DPM Fast",
[DpmAdaptive] = "DPM Adaptive",
[Dpmpp2SAncestral] = "DPM++ 2S Ancestral",
[DpmppSde] = "DPM++ SDE",
[DpmppSdeGpu] = "DPM++ SDE GPU",
[Dpmpp2M] = "DPM++ 2M",
[Dpmpp2MSde] = "DPM++ 2M SDE",
[Dpmpp2MSdeGpu] = "DPM++ 2M SDE GPU",
[Dpmpp3M] = "DPM++ 3M",
[Dpmpp3MSde] = "DPM++ 3M SDE",
[Dpmpp3MSdeGpu] = "DPM++ 3M SDE GPU",
[DDIM] = "DDIM",
[UniPC] = "UniPC",
[UniPCBh2] = "UniPC BH2"
};
public static IReadOnlyList<ComfySampler> Defaults { get; } =
ConvertDict.Keys.Select(k => new ComfySampler(k)).ToImmutableArray();
ConvertDict.Keys.ToImmutableArray();
public string DisplayName =>
ConvertDict.TryGetValue(Name, out var displayName) ? displayName : Name;
ConvertDict.TryGetValue(this, out var displayName) ? displayName : Name;
/// <inheritdoc />
public bool Equals(ComfySampler other)
{
return Name == other.Name;
}
/// <inheritdoc />
public override int GetHashCode()
{
return Name.GetHashCode();
}
private sealed class NameEqualityComparer : IEqualityComparer<ComfySampler>
{

25
StabilityMatrix.Core/Models/Api/Comfy/ComfySamplerScheduler.cs

@ -0,0 +1,25 @@
namespace StabilityMatrix.Core.Models.Api.Comfy;
/// <summary>
/// Pair of <see cref="ComfySampler"/> and <see cref="ComfyScheduler"/>
/// </summary>
public readonly record struct ComfySamplerScheduler(ComfySampler Sampler, ComfyScheduler Scheduler)
{
/// <inheritdoc />
public bool Equals(ComfySamplerScheduler other)
{
return Sampler.Equals(other.Sampler) && Scheduler.Equals(other.Scheduler);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(Sampler, Scheduler);
}
// Implicit conversion from (ComfySampler, ComfyScheduler)
public static implicit operator ComfySamplerScheduler((ComfySampler, ComfyScheduler) tuple)
{
return new ComfySamplerScheduler(tuple.Item1, tuple.Item2);
}
}

6
StabilityMatrix.Core/Models/Api/Comfy/ComfyScheduler.cs

@ -4,10 +4,14 @@ namespace StabilityMatrix.Core.Models.Api.Comfy;
public readonly record struct ComfyScheduler(string Name)
{
public static ComfyScheduler Normal { get; } = new("normal");
public static ComfyScheduler Karras { get; } = new("karras");
public static ComfyScheduler Exponential { get; } = new("exponential");
private static Dictionary<string, string> ConvertDict { get; } =
new()
{
["normal"] = "Normal",
[Normal.Name] = "Normal",
["karras"] = "Karras",
["exponential"] = "Exponential",
["sgm_uniform"] = "SGM Uniform",

46
StabilityMatrix.Core/Models/GenerationParameters.cs

@ -1,11 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using StabilityMatrix.Core.Models.Api.Comfy;
namespace StabilityMatrix.Core.Models;
[JsonSerializable(typeof(GenerationParameters))]
public partial class GenerationParameters
public partial record GenerationParameters
{
public string? PositivePrompt { get; set; }
public string? NegativePrompt { get; set; }
@ -78,6 +79,8 @@ public partial class GenerationParameters
Sampler = match.Groups["Sampler"].Value,
CfgScale = double.Parse(match.Groups["CfgScale"].Value),
Seed = ulong.Parse(match.Groups["Seed"].Value),
Height = int.Parse(match.Groups["Height"].Value),
Width = int.Parse(match.Groups["Width"].Value),
ModelHash = match.Groups["ModelHash"].Value,
ModelName = match.Groups["ModelName"].Value,
};
@ -85,8 +88,47 @@ public partial class GenerationParameters
return true;
}
/// <summary>
/// Converts current <see cref="Sampler"/> string to <see cref="ComfySampler"/> and <see cref="ComfyScheduler"/>.
/// </summary>
/// <returns></returns>
public (ComfySampler sampler, ComfyScheduler scheduler)? GetComfySamplers()
{
if (Sampler is not { } source)
return null;
var scheduler = source switch
{
_ when source.Contains("Karras") => ComfyScheduler.Karras,
_ when source.Contains("Exponential") => ComfyScheduler.Exponential,
_ => ComfyScheduler.Normal,
};
var sampler = source switch
{
"LMS" => ComfySampler.LMS,
"DDIM" => ComfySampler.DDIM,
"UniPC" => ComfySampler.UniPC,
"DPM fast" => ComfySampler.DpmFast,
"DPM adaptive" => ComfySampler.DpmAdaptive,
"Heun" => ComfySampler.Heun,
_ when source.StartsWith("DPM2 a") => ComfySampler.Dpm2Ancestral,
_ when source.StartsWith("DPM2") => ComfySampler.Dpm2,
_ when source.StartsWith("DPM++ 2M SDE") => ComfySampler.Dpmpp2MSde,
_ when source.StartsWith("DPM++ 2M") => ComfySampler.Dpmpp2M,
_ when source.StartsWith("DPM++ 3M SDE") => ComfySampler.Dpmpp3MSde,
_ when source.StartsWith("DPM++ 3M") => ComfySampler.Dpmpp3M,
_ when source.StartsWith("DPM++ SDE") => ComfySampler.DpmppSde,
_ when source.StartsWith("DPM++ 2S a") => ComfySampler.Dpmpp2SAncestral,
_ => default
};
return (sampler, scheduler);
}
// Example: Steps: 30, Sampler: DPM++ 2M Karras, CFG scale: 7, Seed: 2216407431, Size: 640x896, Model hash: eb2h052f91, Model: anime_v1
[GeneratedRegex(
"""^Steps: (?<Steps>\d+), Sampler: (?<Sampler>.+?), CFG scale: (?<CfgScale>\d+(\.\d+)?), Seed: (?<Seed>\d+), Size: \d+x\d+, Model hash: (?<ModelHash>.+?), Model: (?<ModelName>.+)$"""
"""^Steps: (?<Steps>\d+), Sampler: (?<Sampler>.+?), CFG scale: (?<CfgScale>\d+(\.\d+)?), Seed: (?<Seed>\d+), Size: (?<Width>\d+)x(?<Height>\d+), Model hash: (?<ModelHash>.+?), Model: (?<ModelName>.+)$"""
)]
private static partial Regex ParseLastLineRegex();
}

3
StabilityMatrix.sln.DotSettings

@ -1,8 +1,11 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DDIM/@EntryIndexedValue">DDIM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EOF/@EntryIndexedValue">EOF</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ESRGAN/@EntryIndexedValue">ESRGAN</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LMS/@EntryIndexedValue">LMS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LRU/@EntryIndexedValue">LRU</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PC/@EntryIndexedValue">PC</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"&gt;&lt;ExtraRule Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=F0CA621CDF5AB24282D8CDC11C520997/@KeyIndexDefined">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/QuickList/=F0CA621CDF5AB24282D8CDC11C520997/Entry/=1A64B45C2359DD448CF8A25D4A7469F9/@KeyIndexDefined">True</s:Boolean>

Loading…
Cancel
Save