From b4fd64e7fc9592b60ba9dd2c6fa3887a1986080e Mon Sep 17 00:00:00 2001
From: jt
Date: Sat, 18 Nov 2023 00:14:22 -0800
Subject: [PATCH 001/116] macos fixes & remove mac label for volta
---
.../StabilityMatrix.Avalonia.Diagnostics.csproj | 1 +
StabilityMatrix.Avalonia/App.axaml | 1 +
StabilityMatrix.Avalonia/Helpers/UriHandler.cs | 2 +-
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj | 9 +++++++++
StabilityMatrix.Core/Models/Packages/BasePackage.cs | 5 +++++
StabilityMatrix.Core/Models/Packages/VoltaML.cs | 2 +-
StabilityMatrix.Core/Processes/ProcessRunner.cs | 2 +-
StabilityMatrix.Core/StabilityMatrix.Core.csproj | 1 +
StabilityMatrix.Tests/StabilityMatrix.Tests.csproj | 1 +
StabilityMatrix.UITests/StabilityMatrix.UITests.csproj | 1 +
10 files changed, 22 insertions(+), 3 deletions(-)
diff --git a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
index eb84e3f3..84e8c3f9 100644
--- a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
+++ b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
@@ -21,6 +21,7 @@
+
diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml
index c2821641..2e08109c 100644
--- a/StabilityMatrix.Avalonia/App.axaml
+++ b/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">
diff --git a/StabilityMatrix.Avalonia/Helpers/UriHandler.cs b/StabilityMatrix.Avalonia/Helpers/UriHandler.cs
index e500a1e4..a6f04b3d 100644
--- a/StabilityMatrix.Avalonia/Helpers/UriHandler.cs
+++ b/StabilityMatrix.Avalonia/Helpers/UriHandler.cs
@@ -64,7 +64,7 @@ public class UriHandler
{
RegisterUriSchemeWin();
}
- else
+ else if (Compat.IsLinux)
{
RegisterUriSchemeUnix();
}
diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
index fb16d35f..d3111ecd 100644
--- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
+++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
@@ -1,5 +1,6 @@
+ Stability Matrix
WinExe
net8.0
win-x64;linux-x64;osx-x64;osx-arm64
@@ -14,6 +15,13 @@
true
+
+
+ StabilityMatrix.URL
+ stabilitymatrix;stabilitymatrix://
+
+
+
@@ -37,6 +45,7 @@
+
diff --git a/StabilityMatrix.Core/Models/Packages/BasePackage.cs b/StabilityMatrix.Core/Models/Packages/BasePackage.cs
index 29ff06b9..f704b0f0 100644
--- a/StabilityMatrix.Core/Models/Packages/BasePackage.cs
+++ b/StabilityMatrix.Core/Models/Packages/BasePackage.cs
@@ -133,6 +133,11 @@ public abstract class BasePackage
return TorchVersion.DirectMl;
}
+ if (Compat.IsMacOS && Compat.IsArm && AvailableTorchVersions.Contains(TorchVersion.Mps))
+ {
+ return TorchVersion.Mps;
+ }
+
return TorchVersion.Cpu;
}
diff --git a/StabilityMatrix.Core/Models/Packages/VoltaML.cs b/StabilityMatrix.Core/Models/Packages/VoltaML.cs
index eeef1ce3..933e4fb3 100644
--- a/StabilityMatrix.Core/Models/Packages/VoltaML.cs
+++ b/StabilityMatrix.Core/Models/Packages/VoltaML.cs
@@ -62,7 +62,7 @@ public class VoltaML : BaseGitPackage
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink;
public override IEnumerable AvailableTorchVersions =>
- new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Mps };
+ new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl };
public override IEnumerable AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };
diff --git a/StabilityMatrix.Core/Processes/ProcessRunner.cs b/StabilityMatrix.Core/Processes/ProcessRunner.cs
index 3a20cc60..8adde5eb 100644
--- a/StabilityMatrix.Core/Processes/ProcessRunner.cs
+++ b/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);
diff --git a/StabilityMatrix.Core/StabilityMatrix.Core.csproj b/StabilityMatrix.Core/StabilityMatrix.Core.csproj
index 254f8efe..f62ba22a 100644
--- a/StabilityMatrix.Core/StabilityMatrix.Core.csproj
+++ b/StabilityMatrix.Core/StabilityMatrix.Core.csproj
@@ -23,6 +23,7 @@
+
diff --git a/StabilityMatrix.Tests/StabilityMatrix.Tests.csproj b/StabilityMatrix.Tests/StabilityMatrix.Tests.csproj
index 87423475..a0acdc33 100644
--- a/StabilityMatrix.Tests/StabilityMatrix.Tests.csproj
+++ b/StabilityMatrix.Tests/StabilityMatrix.Tests.csproj
@@ -11,6 +11,7 @@
+
diff --git a/StabilityMatrix.UITests/StabilityMatrix.UITests.csproj b/StabilityMatrix.UITests/StabilityMatrix.UITests.csproj
index 8062d3bd..961598f0 100644
--- a/StabilityMatrix.UITests/StabilityMatrix.UITests.csproj
+++ b/StabilityMatrix.UITests/StabilityMatrix.UITests.csproj
@@ -11,6 +11,7 @@
+
From e389085ec11104f51ac5b10c4e7edab3dc3f9f42 Mon Sep 17 00:00:00 2001
From: JT
Date: Tue, 28 Nov 2023 23:03:51 -0800
Subject: [PATCH 002/116] Added image to video in inference (mostly)
---
StabilityMatrix.Avalonia/App.axaml | 3 +-
.../Controls/ModelCard.axaml | 2 +
.../VideoGenerationSettingsCard.axaml | 122 +++++++++
.../VideoGenerationSettingsCard.axaml.cs | 7 +
.../Controls/VideoOutputSettingsCard.axaml | 86 +++++++
.../Controls/VideoOutputSettingsCard.axaml.cs | 7 +
.../DesignData/DesignData.cs | 14 +
.../Models/Inference/VideoOutputMethod.cs | 8 +
.../Models/InferenceProjectDocument.cs | 1 +
.../Models/InferenceProjectType.cs | 4 +-
.../StabilityMatrix.Avalonia.csproj | 12 +
.../Base/InferenceGenerationViewModelBase.cs | 37 ++-
.../InferenceImageToVideoViewModel.cs | 243 ++++++++++++++++++
.../Inference/ModelCardViewModel.cs | 5 +-
.../Video/ImgToVidModelCardViewModel.cs | 37 +++
.../Video/SvdImgToVidConditioningViewModel.cs | 90 +++++++
.../Video/VideoOutputSettingsCardViewModel.cs | 80 ++++++
.../Inference/InferenceImageToVideoView.axaml | 221 ++++++++++++++++
.../InferenceImageToVideoView.axaml.cs | 13 +
.../Views/InferencePage.axaml | 10 +
.../Views/InferencePage.axaml.cs | 7 +
.../Api/Comfy/NodeTypes/NodeConnections.cs | 2 +
.../Api/Comfy/Nodes/ComfyNodeBuilder.cs | 41 +++
23 files changed, 1038 insertions(+), 14 deletions(-)
create mode 100644 StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml
create mode 100644 StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml.cs
create mode 100644 StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml
create mode 100644 StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml.cs
create mode 100644 StabilityMatrix.Avalonia/Models/Inference/VideoOutputMethod.cs
create mode 100644 StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
create mode 100644 StabilityMatrix.Avalonia/ViewModels/Inference/Video/ImgToVidModelCardViewModel.cs
create mode 100644 StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs
create mode 100644 StabilityMatrix.Avalonia/ViewModels/Inference/Video/VideoOutputSettingsCardViewModel.cs
create mode 100644 StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml
create mode 100644 StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml.cs
diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml
index 93486dab..4e02cb07 100644
--- a/StabilityMatrix.Avalonia/App.axaml
+++ b/StabilityMatrix.Avalonia/App.axaml
@@ -74,7 +74,8 @@
-
+
+
diff --git a/StabilityMatrix.Avalonia/Controls/ModelCard.axaml b/StabilityMatrix.Avalonia/Controls/ModelCard.axaml
index e0fc6a1c..ce990ac9 100644
--- a/StabilityMatrix.Avalonia/Controls/ModelCard.axaml
+++ b/StabilityMatrix.Avalonia/Controls/ModelCard.axaml
@@ -12,6 +12,7 @@
+
@@ -135,6 +136,7 @@
diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
index d27b1500..3515089c 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
@@ -418,19 +418,34 @@ public abstract partial class InferenceGenerationViewModelBase
);
}
- var bytesWithMetadata = PngDataHelper.AddMetadata(imageArray, parameters, project);
-
- // Write using generated name
- var filePath = await WriteOutputImageAsync(
- new MemoryStream(bytesWithMetadata),
- args,
- i + 1,
- images.Count
- );
+ if (comfyImage.FileName.EndsWith(".png"))
+ {
+ var bytesWithMetadata = PngDataHelper.AddMetadata(imageArray, parameters, project);
+
+ // Write using generated name
+ var filePath = await WriteOutputImageAsync(
+ new MemoryStream(bytesWithMetadata),
+ args,
+ i + 1,
+ images.Count
+ );
- outputImages.Add(new ImageSource(filePath));
+ outputImages.Add(new ImageSource(filePath));
+ EventManager.Instance.OnImageFileAdded(filePath);
+ }
+ else
+ {
+ // Write using generated name
+ var filePath = await WriteOutputImageAsync(
+ new MemoryStream(imageArray),
+ args,
+ i + 1,
+ images.Count
+ );
- EventManager.Instance.OnImageFileAdded(filePath);
+ outputImages.Add(new ImageSource(filePath));
+ EventManager.Instance.OnImageFileAdded(filePath);
+ }
}
// Download all images to make grid, if multiple
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
new file mode 100644
index 00000000..3a6c5f5d
--- /dev/null
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
@@ -0,0 +1,243 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Drawing;
+using System.Linq;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using NLog;
+using StabilityMatrix.Avalonia.Extensions;
+using StabilityMatrix.Avalonia.Models;
+using StabilityMatrix.Avalonia.Models.Inference;
+using StabilityMatrix.Avalonia.Services;
+using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
+using StabilityMatrix.Avalonia.ViewModels.Inference.Video;
+using StabilityMatrix.Avalonia.Views.Inference;
+using StabilityMatrix.Core.Attributes;
+using StabilityMatrix.Core.Models;
+using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
+using StabilityMatrix.Core.Services;
+
+#pragma warning disable CS0657 // Not a valid attribute location for this declaration
+
+namespace StabilityMatrix.Avalonia.ViewModels.Inference;
+
+[View(typeof(InferenceImageToVideoView), persistent: true)]
+[ManagedService]
+[Transient]
+public class InferenceImageToVideoViewModel
+ : InferenceGenerationViewModelBase,
+ IParametersLoadableState
+{
+ private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+
+ private readonly INotificationService notificationService;
+ private readonly IModelIndexService modelIndexService;
+
+ [JsonIgnore]
+ public StackCardViewModel StackCardViewModel { get; }
+
+ [JsonPropertyName("Model")]
+ public ImgToVidModelCardViewModel ModelCardViewModel { get; }
+
+ [JsonPropertyName("Sampler")]
+ public SamplerCardViewModel SamplerCardViewModel { get; }
+
+ [JsonPropertyName("BatchSize")]
+ public BatchSizeCardViewModel BatchSizeCardViewModel { get; }
+
+ [JsonPropertyName("Seed")]
+ public SeedCardViewModel SeedCardViewModel { get; }
+
+ [JsonPropertyName("ImageLoader")]
+ public SelectImageCardViewModel SelectImageCardViewModel { get; }
+
+ [JsonPropertyName("Conditioning")]
+ public SvdImgToVidConditioningViewModel SvdImgToVidConditioningViewModel { get; }
+
+ [JsonPropertyName("VideoOutput")]
+ public VideoOutputSettingsCardViewModel VideoOutputSettingsCardViewModel { get; }
+
+ public InferenceImageToVideoViewModel(
+ INotificationService notificationService,
+ IInferenceClientManager inferenceClientManager,
+ ISettingsManager settingsManager,
+ ServiceManager vmFactory,
+ IModelIndexService modelIndexService
+ )
+ : base(vmFactory, inferenceClientManager, notificationService, settingsManager)
+ {
+ this.notificationService = notificationService;
+ this.modelIndexService = modelIndexService;
+
+ // Get sub view models from service manager
+
+ SeedCardViewModel = vmFactory.Get();
+ SeedCardViewModel.GenerateNewSeed();
+
+ ModelCardViewModel = vmFactory.Get();
+
+ SamplerCardViewModel = vmFactory.Get(samplerCard =>
+ {
+ samplerCard.IsDimensionsEnabled = true;
+ samplerCard.IsCfgScaleEnabled = true;
+ samplerCard.IsSamplerSelectionEnabled = true;
+ samplerCard.IsSchedulerSelectionEnabled = true;
+ });
+
+ BatchSizeCardViewModel = vmFactory.Get();
+
+ SelectImageCardViewModel = vmFactory.Get();
+ SvdImgToVidConditioningViewModel = vmFactory.Get();
+ VideoOutputSettingsCardViewModel = vmFactory.Get();
+
+ StackCardViewModel = vmFactory.Get();
+ StackCardViewModel.AddCards(
+ ModelCardViewModel,
+ SvdImgToVidConditioningViewModel,
+ SamplerCardViewModel,
+ SeedCardViewModel,
+ VideoOutputSettingsCardViewModel,
+ BatchSizeCardViewModel
+ );
+ }
+
+ ///
+ protected override void BuildPrompt(BuildPromptEventArgs args)
+ {
+ base.BuildPrompt(args);
+
+ var builder = args.Builder;
+
+ builder.Connections.Seed = args.SeedOverride switch
+ {
+ { } seed => Convert.ToUInt64(seed),
+ _ => Convert.ToUInt64(SeedCardViewModel.Seed)
+ };
+
+ // Load models
+ ModelCardViewModel.ApplyStep(args);
+
+ // Setup latent from image
+ var imageLoad = builder.Nodes.AddTypedNode(
+ new ComfyNodeBuilder.LoadImage
+ {
+ Name = builder.Nodes.GetUniqueName("ControlNet_LoadImage"),
+ Image =
+ SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference")
+ ?? throw new ValidationException()
+ }
+ );
+ builder.Connections.Primary = imageLoad.Output1;
+ builder.Connections.PrimarySize =
+ SelectImageCardViewModel.CurrentBitmapSize
+ ?? new Size(SamplerCardViewModel.Width, SamplerCardViewModel.Height);
+
+ // Setup img2vid stuff
+ // Set width & height from SamplerCard
+ SvdImgToVidConditioningViewModel.Width = SamplerCardViewModel.Width;
+ SvdImgToVidConditioningViewModel.Height = SamplerCardViewModel.Height;
+ SvdImgToVidConditioningViewModel.ApplyStep(args);
+
+ // Setup Sampler and Refiner if enabled
+ SamplerCardViewModel.ApplyStep(args);
+
+ // Animated webp output
+ VideoOutputSettingsCardViewModel.ApplyStep(args);
+ }
+
+ ///
+ protected override IEnumerable GetInputImages()
+ {
+ if (SelectImageCardViewModel.ImageSource is { } image)
+ {
+ yield return image;
+ }
+ }
+
+ ///
+ protected override async Task GenerateImageImpl(
+ GenerateOverrides overrides,
+ CancellationToken cancellationToken
+ )
+ {
+ if (!await CheckClientConnectedWithPrompt() || !ClientManager.IsConnected)
+ {
+ return;
+ }
+
+ // If enabled, randomize the seed
+ var seedCard = StackCardViewModel.GetCard();
+ if (overrides is not { UseCurrentSeed: true } && seedCard.IsRandomizeEnabled)
+ {
+ seedCard.GenerateNewSeed();
+ }
+
+ var batches = BatchSizeCardViewModel.BatchCount;
+
+ var batchArgs = new List();
+
+ for (var i = 0; i < batches; i++)
+ {
+ var seed = seedCard.Seed + i;
+
+ var buildPromptArgs = new BuildPromptEventArgs
+ {
+ Overrides = overrides,
+ SeedOverride = seed
+ };
+ BuildPrompt(buildPromptArgs);
+
+ var generationArgs = new ImageGenerationEventArgs
+ {
+ Client = ClientManager.Client,
+ Nodes = buildPromptArgs.Builder.ToNodeDictionary(),
+ OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(),
+ Parameters = SaveStateToParameters(new GenerationParameters()),
+ Project = InferenceProjectDocument.FromLoadable(this),
+ // Only clear output images on the first batch
+ ClearOutputImages = i == 0
+ };
+
+ batchArgs.Add(generationArgs);
+ }
+
+ // Run batches
+ foreach (var args in batchArgs)
+ {
+ await RunGeneration(args, cancellationToken);
+ }
+ }
+
+ ///
+ public void LoadStateFromParameters(GenerationParameters parameters)
+ {
+ SamplerCardViewModel.LoadStateFromParameters(parameters);
+ ModelCardViewModel.LoadStateFromParameters(parameters);
+
+ SeedCardViewModel.Seed = Convert.ToInt64(parameters.Seed);
+ }
+
+ ///
+ public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
+ {
+ parameters = SamplerCardViewModel.SaveStateToParameters(parameters);
+ parameters = ModelCardViewModel.SaveStateToParameters(parameters);
+
+ parameters.Seed = (ulong)SeedCardViewModel.Seed;
+
+ return parameters;
+ }
+
+ // Migration for v2 deserialization
+ public override void LoadStateFromJsonObject(JsonObject state, int version)
+ {
+ if (version > 2)
+ {
+ LoadStateFromJsonObject(state);
+ }
+ }
+}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
index aa748d05..87dc4b5e 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
@@ -38,6 +38,9 @@ public partial class ModelCardViewModel
[ObservableProperty]
private bool isVaeSelectionEnabled;
+ [ObservableProperty]
+ private bool disableSettings;
+
public string? SelectedModelName => SelectedModel?.RelativePath;
public string? SelectedVaeName => SelectedVae?.RelativePath;
@@ -50,7 +53,7 @@ public partial class ModelCardViewModel
}
///
- public void ApplyStep(ModuleApplyStepEventArgs e)
+ public virtual void ApplyStep(ModuleApplyStepEventArgs e)
{
// Load base checkpoint
var baseLoader = e.Nodes.AddTypedNode(
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/ImgToVidModelCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/ImgToVidModelCardViewModel.cs
new file mode 100644
index 00000000..c5513e6e
--- /dev/null
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/ImgToVidModelCardViewModel.cs
@@ -0,0 +1,37 @@
+using System.ComponentModel.DataAnnotations;
+using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Avalonia.Models.Inference;
+using StabilityMatrix.Avalonia.Services;
+using StabilityMatrix.Core.Attributes;
+using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
+
+namespace StabilityMatrix.Avalonia.ViewModels.Inference.Video;
+
+[View(typeof(ModelCard))]
+[ManagedService]
+[Transient]
+public class ImgToVidModelCardViewModel : ModelCardViewModel
+{
+ public ImgToVidModelCardViewModel(IInferenceClientManager clientManager)
+ : base(clientManager)
+ {
+ DisableSettings = true;
+ }
+
+ public override void ApplyStep(ModuleApplyStepEventArgs e)
+ {
+ var imgToVidLoader = e.Nodes.AddTypedNode(
+ new ComfyNodeBuilder.ImageOnlyCheckpointLoader
+ {
+ Name = "ImageOnlyCheckpointLoader",
+ CkptName =
+ SelectedModel?.RelativePath
+ ?? throw new ValidationException("Model not selected")
+ }
+ );
+
+ e.Builder.Connections.BaseModel = imgToVidLoader.Output1;
+ e.Builder.Connections.BaseClipVision = imgToVidLoader.Output2;
+ e.Builder.Connections.BaseVAE = imgToVidLoader.Output3;
+ }
+}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs
new file mode 100644
index 00000000..4932ad55
--- /dev/null
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs
@@ -0,0 +1,90 @@
+using System.ComponentModel.DataAnnotations;
+using CommunityToolkit.Mvvm.ComponentModel;
+using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Avalonia.Models;
+using StabilityMatrix.Avalonia.Models.Inference;
+using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Core.Attributes;
+using StabilityMatrix.Core.Models;
+using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
+
+namespace StabilityMatrix.Avalonia.ViewModels.Inference.Video;
+
+[View(typeof(VideoGenerationSettingsCard))]
+[ManagedService]
+[Transient]
+public partial class SvdImgToVidConditioningViewModel
+ : LoadableViewModelBase,
+ IParametersLoadableState,
+ IComfyStep
+{
+ [ObservableProperty]
+ private int width = 1024;
+
+ [ObservableProperty]
+ private int height = 576;
+
+ [ObservableProperty]
+ private int numFrames = 14;
+
+ [ObservableProperty]
+ private int motionBucketId = 127;
+
+ [ObservableProperty]
+ private int fps = 6;
+
+ [ObservableProperty]
+ private double augmentationLevel;
+
+ [ObservableProperty]
+ private double minCfg = 1.0d;
+
+ public void LoadStateFromParameters(GenerationParameters parameters)
+ {
+ // TODO
+ }
+
+ public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
+ {
+ // TODO
+ return parameters;
+ }
+
+ public void ApplyStep(ModuleApplyStepEventArgs e)
+ {
+ // do VideoLinearCFGGuidance stuff first
+ var cfgGuidanceNode = e.Nodes.AddTypedNode(
+ new ComfyNodeBuilder.VideoLinearCFGGuidance
+ {
+ Name = e.Nodes.GetUniqueName("LinearCfgGuidance"),
+ Model =
+ e.Builder.Connections.BaseModel
+ ?? throw new ValidationException("Model not selected"),
+ MinCfg = MinCfg
+ }
+ );
+
+ e.Builder.Connections.BaseModel = cfgGuidanceNode.Output;
+
+ // then do the SVD stuff
+ var svdImgToVidConditioningNode = e.Nodes.AddTypedNode(
+ new ComfyNodeBuilder.SVD_img2vid_Conditioning
+ {
+ ClipVision = e.Builder.Connections.BaseClipVision!,
+ InitImage = e.Builder.GetPrimaryAsImage(),
+ Vae = e.Builder.Connections.BaseVAE!,
+ Name = e.Nodes.GetUniqueName("SvdImgToVidConditioning"),
+ Width = Width,
+ Height = Height,
+ VideoFrames = NumFrames,
+ MotionBucketId = MotionBucketId,
+ Fps = Fps,
+ AugmentationLevel = AugmentationLevel
+ }
+ );
+
+ e.Builder.Connections.BaseConditioning = svdImgToVidConditioningNode.Output1;
+ e.Builder.Connections.BaseNegativeConditioning = svdImgToVidConditioningNode.Output2;
+ e.Builder.Connections.Primary = svdImgToVidConditioningNode.Output3;
+ }
+}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/VideoOutputSettingsCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/VideoOutputSettingsCardViewModel.cs
new file mode 100644
index 00000000..3fd77079
--- /dev/null
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/VideoOutputSettingsCardViewModel.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using CommunityToolkit.Mvvm.ComponentModel;
+using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Avalonia.Models;
+using StabilityMatrix.Avalonia.Models.Inference;
+using StabilityMatrix.Avalonia.ViewModels.Base;
+using StabilityMatrix.Core.Attributes;
+using StabilityMatrix.Core.Models;
+using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
+
+namespace StabilityMatrix.Avalonia.ViewModels.Inference.Video;
+
+[View(typeof(VideoOutputSettingsCard))]
+[ManagedService]
+[Transient]
+public partial class VideoOutputSettingsCardViewModel
+ : LoadableViewModelBase,
+ IParametersLoadableState,
+ IComfyStep
+{
+ [ObservableProperty]
+ private double fps = 6;
+
+ [ObservableProperty]
+ private bool lossless = true;
+
+ [ObservableProperty]
+ private int quality = 85;
+
+ [ObservableProperty]
+ private VideoOutputMethod selectedMethod = VideoOutputMethod.Default;
+
+ [ObservableProperty]
+ private List availableMethods = Enum.GetValues().ToList();
+
+ public void LoadStateFromParameters(GenerationParameters parameters)
+ {
+ // TODO
+ }
+
+ public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
+ {
+ // TODO
+ return parameters;
+ }
+
+ public void ApplyStep(ModuleApplyStepEventArgs e)
+ {
+ if (e.Builder.Connections.Primary is null)
+ throw new ArgumentException("No Primary");
+
+ var image = e.Builder.Connections.Primary.Match(
+ _ =>
+ e.Builder.GetPrimaryAsImage(
+ e.Builder.Connections.PrimaryVAE
+ ?? e.Builder.Connections.RefinerVAE
+ ?? e.Builder.Connections.BaseVAE
+ ?? throw new ArgumentException("No Primary, Refiner, or Base VAE")
+ ),
+ image => image
+ );
+
+ var outputStep = e.Nodes.AddTypedNode(
+ new ComfyNodeBuilder.SaveAnimatedWEBP
+ {
+ Name = e.Nodes.GetUniqueName("SaveAnimatedWEBP"),
+ Images = image,
+ FilenamePrefix = "InferenceVideo",
+ Fps = Fps,
+ Lossless = Lossless,
+ Quality = Quality,
+ Method = SelectedMethod.ToString().ToLowerInvariant()
+ }
+ );
+
+ e.Builder.Connections.OutputNodes.Add(outputStep);
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml
new file mode 100644
index 00000000..02a2fd50
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml.cs b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml.cs
new file mode 100644
index 00000000..585ae911
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml.cs
@@ -0,0 +1,13 @@
+using StabilityMatrix.Avalonia.Controls.Dock;
+using StabilityMatrix.Core.Attributes;
+
+namespace StabilityMatrix.Avalonia.Views.Inference;
+
+[Transient]
+public partial class InferenceImageToVideoView : DockUserControlBase
+{
+ public InferenceImageToVideoView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml b/StabilityMatrix.Avalonia/Views/InferencePage.axaml
index 74692705..e11be41d 100644
--- a/StabilityMatrix.Avalonia/Views/InferencePage.axaml
+++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml
@@ -68,6 +68,16 @@
Symbol="ResizeImage"/>
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs b/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs
index 05e4eb76..d3188e30 100644
--- a/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/InferencePage.axaml.cs
@@ -60,4 +60,11 @@ public partial class InferencePage : UserControlBase
{
(DataContext as InferenceViewModel)!.AddTabCommand.Execute(InferenceProjectType.Upscale);
}
+
+ private void AddTabMenu_ImgToVideo_OnClick(object? sender, RoutedEventArgs e)
+ {
+ (DataContext as InferenceViewModel)!.AddTabCommand.Execute(
+ InferenceProjectType.ImageToVideo
+ );
+ }
}
diff --git a/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs
index ce6521fe..259c99c4 100644
--- a/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs
+++ b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs
@@ -17,3 +17,5 @@ public class ConditioningNodeConnection : NodeConnectionBase { }
public class ClipNodeConnection : NodeConnectionBase { }
public class ControlNetNodeConnection : NodeConnectionBase { }
+
+public class ClipVisionNodeConnection : NodeConnectionBase { }
diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs
index dce175e3..eb6bce79 100644
--- a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs
+++ b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs
@@ -290,6 +290,12 @@ public class ComfyNodeBuilder
public required string CkptName { get; init; }
}
+ public record ImageOnlyCheckpointLoader
+ : ComfyTypedNodeBase
+ {
+ public required string CkptName { get; init; }
+ }
+
public record FreeU : ComfyTypedNodeBase
{
public required ModelNodeConnection Model { get; init; }
@@ -357,6 +363,40 @@ public class ComfyNodeBuilder
public required double EndPercent { get; init; }
}
+ public record SVD_img2vid_Conditioning
+ : ComfyTypedNodeBase<
+ ConditioningNodeConnection,
+ ConditioningNodeConnection,
+ LatentNodeConnection
+ >
+ {
+ public required ClipVisionNodeConnection ClipVision { get; init; }
+ public required ImageNodeConnection InitImage { get; init; }
+ public required VAENodeConnection Vae { get; init; }
+ public required int Width { get; init; }
+ public required int Height { get; init; }
+ public required int VideoFrames { get; init; }
+ public required int MotionBucketId { get; init; }
+ public required int Fps { get; set; }
+ public required double AugmentationLevel { get; init; }
+ }
+
+ public record VideoLinearCFGGuidance : ComfyTypedNodeBase
+ {
+ public required ModelNodeConnection Model { get; init; }
+ public required double MinCfg { get; init; }
+ }
+
+ public record SaveAnimatedWEBP : ComfyTypedNodeBase
+ {
+ public required ImageNodeConnection Images { get; init; }
+ public required string FilenamePrefix { get; init; }
+ public required double Fps { get; init; }
+ public required bool Lossless { get; init; }
+ public required int Quality { get; init; }
+ public required string Method { get; init; }
+ }
+
public ImageNodeConnection Lambda_LatentToImage(
LatentNodeConnection latent,
VAENodeConnection vae
@@ -823,6 +863,7 @@ public class ComfyNodeBuilder
public ModelNodeConnection? BaseModel { get; set; }
public VAENodeConnection? BaseVAE { get; set; }
public ClipNodeConnection? BaseClip { get; set; }
+ public ClipVisionNodeConnection? BaseClipVision { get; set; }
public ConditioningNodeConnection? BaseConditioning { get; set; }
public ConditioningNodeConnection? BaseNegativeConditioning { get; set; }
From 6154d835eed8240d1a053028d615e3517b380f7b Mon Sep 17 00:00:00 2001
From: JT
Date: Tue, 28 Nov 2023 23:15:57 -0800
Subject: [PATCH 003/116] add header to VideoOutputSettingsCard to indicate
it's for output settings
---
.../Controls/VideoOutputSettingsCard.axaml | 106 ++++++++++--------
1 file changed, 59 insertions(+), 47 deletions(-)
diff --git a/StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml b/StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml
index b9e9ac0b..90962683 100644
--- a/StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml
+++ b/StabilityMatrix.Avalonia/Controls/VideoOutputSettingsCard.axaml
@@ -8,7 +8,8 @@
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages">
-
+
@@ -19,14 +20,23 @@
-
+
-
-
-
-
-
-
-
-
-
+ SpinButtonPlacementMode="Inline" />
+
+
+
+
+
+
+
+
+
From c5bda38264cd5fc9e0eb3e80c2fc39d840ab33b2 Mon Sep 17 00:00:00 2001
From: Ionite
Date: Sat, 16 Dec 2023 12:18:21 -0500
Subject: [PATCH 004/116] Add CLIPSetLastLayer Node
---
.../Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs
index 25d3f9cf..f308e993 100644
--- a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs
+++ b/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
+ {
+ public required ClipNodeConnection Clip { get; init; }
+
+ [Range(-24, -1)]
+ public int StopAtClipLayer { get; init; } = -1;
+ }
+
public static NamedComfyNode LatentFromBatch(
string name,
LatentNodeConnection samples,
From 097845a70984eb7ec5eb9f15da8ffa006461a603 Mon Sep 17 00:00:00 2001
From: Ionite
Date: Sat, 16 Dec 2023 16:14:12 -0500
Subject: [PATCH 005/116] Add NullableExtensions.Unwrap
---
.../Extensions/NullableExtensions.cs | 28 +++++++++++++++++++
1 file changed, 28 insertions(+)
create mode 100644 StabilityMatrix.Core/Extensions/NullableExtensions.cs
diff --git a/StabilityMatrix.Core/Extensions/NullableExtensions.cs b/StabilityMatrix.Core/Extensions/NullableExtensions.cs
new file mode 100644
index 00000000..aa644fb9
--- /dev/null
+++ b/StabilityMatrix.Core/Extensions/NullableExtensions.cs
@@ -0,0 +1,28 @@
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace StabilityMatrix.Core.Extensions;
+
+public static class NullableExtensions
+{
+ ///
+ /// Unwraps a nullable object, throwing an exception if it is null.
+ ///
+ ///
+ /// Thrown if () is null.
+ ///
+ [DebuggerStepThrough]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static T Unwrap([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;
+ }
+}
From 74aac6272b9cdb10ea4759262c29527c81f9725f Mon Sep 17 00:00:00 2001
From: Ionite
Date: Sat, 16 Dec 2023 16:53:14 -0500
Subject: [PATCH 006/116] Add NullableExtensions.Unwrap struct overload
---
.../Extensions/NullableExtensions.cs | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/StabilityMatrix.Core/Extensions/NullableExtensions.cs b/StabilityMatrix.Core/Extensions/NullableExtensions.cs
index aa644fb9..64264ef6 100644
--- a/StabilityMatrix.Core/Extensions/NullableExtensions.cs
+++ b/StabilityMatrix.Core/Extensions/NullableExtensions.cs
@@ -25,4 +25,23 @@ public static class NullableExtensions
}
return obj;
}
+
+ ///
+ /// Unwraps a nullable struct object, throwing an exception if it is null.
+ ///
+ ///
+ /// Thrown if () is null.
+ ///
+ [DebuggerStepThrough]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static T Unwrap([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;
+ }
}
From 5871241d6feedc05466857498d1d463737a3dbc6 Mon Sep 17 00:00:00 2001
From: Ionite
Date: Sat, 16 Dec 2023 16:54:39 -0500
Subject: [PATCH 007/116] Add Clip skip and model connection refactors
---
.../Extensions/ComfyNodeBuilderExtensions.cs | 67 +++++-----
.../Inference/ModuleApplyStepEventArgs.cs | 15 +--
.../Inference/ModelCardViewModel.cs | 79 +++++++----
.../Inference/Modules/FreeUModule.cs | 31 ++---
.../Inference/Modules/HiresFixModule.cs | 9 +-
.../Inference/PromptCardViewModel.cs | 125 ++++--------------
.../Inference/SamplerCardViewModel.cs | 64 ++++-----
.../NodeTypes/ConditioningConnections.cs | 12 ++
.../Api/Comfy/NodeTypes/ModelConnections.cs | 15 +++
.../Api/Comfy/Nodes/ComfyNodeBuilder.cs | 35 ++---
10 files changed, 208 insertions(+), 244 deletions(-)
create mode 100644 StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ConditioningConnections.cs
create mode 100644 StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ModelConnections.cs
diff --git a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs
index 38c28fb3..a4a88705 100644
--- a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs
+++ b/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);
diff --git a/StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs b/StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs
index f90baa4b..8119abc1 100644
--- a/StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs
+++ b/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
///
/// Generation overrides (like hires fix generate, current seed generate, etc.)
///
- public IReadOnlyDictionary IsEnabledOverrides { get; init; } =
- new Dictionary();
+ public IReadOnlyDictionary IsEnabledOverrides { get; init; } = new Dictionary();
public class ModuleApplyStepTemporaryArgs
{
///
/// Temporary conditioning apply step, used by samplers to apply control net.
///
- public (
- ConditioningNodeConnection Positive,
- ConditioningNodeConnection Negative
- )? Conditioning { get; set; }
+ public ConditioningConnections? Conditioning { get; set; }
///
/// Temporary refiner conditioning apply step, used by samplers to apply control net.
///
- public (
- ConditioningNodeConnection Positive,
- ConditioningNodeConnection Negative
- )? RefinerConditioning { get; set; }
+ public ConditioningConnections? RefinerConditioning { get; set; }
///
/// Temporary model apply step, used by samplers to apply control net.
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
index 5336a3e1..38e13bb2 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
@@ -9,9 +9,10 @@ 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;
-using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
+using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
@@ -38,6 +39,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;
///
@@ -47,50 +56,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;
+ }
+ }
}
///
@@ -102,8 +129,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 +154,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;
}
///
@@ -145,19 +177,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 +208,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; }
public bool IsVaeSelectionEnabled { get; init; }
public bool IsRefinerSelectionEnabled { get; init; }
+ public bool IsClipSkipEnabled { get; init; }
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Modules/FreeUModule.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Modules/FreeUModule.cs
index 4ac393b6..12093310 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/Modules/FreeUModule.cs
+++ b/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();
- // 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,
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Modules/HiresFixModule.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Modules/HiresFixModule.cs
index 4fd98fea..588cd263 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/Modules/HiresFixModule.cs
+++ b/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
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs
index bad20dba..b6973534 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs
+++ b/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:
///
- ///
- ///
+ /// - - Model, Clip
///
/// Provides:
///
- ///
- ///
+ /// - - Conditioning
///
///
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
///
public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
{
- return parameters with
- {
- PositivePrompt = PromptDocument.Text,
- NegativePrompt = NegativePromptDocument.Text
- };
+ return parameters with { PositivePrompt = PromptDocument.Text, NegativePrompt = NegativePromptDocument.Text };
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs
index 32640d8e..a129696f 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/SamplerCardViewModel.cs
+++ b/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,
diff --git a/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ConditioningConnections.cs b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ConditioningConnections.cs
new file mode 100644
index 00000000..02a3c2af
--- /dev/null
+++ b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ConditioningConnections.cs
@@ -0,0 +1,12 @@
+namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
+
+///
+/// Combination of the positive and negative conditioning connections.
+///
+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);
+}
diff --git a/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ModelConnections.cs b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ModelConnections.cs
new file mode 100644
index 00000000..abb66596
--- /dev/null
+++ b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/ModelConnections.cs
@@ -0,0 +1,15 @@
+namespace StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
+
+///
+/// Connections from a loaded model
+///
+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; }
+}
diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs
index f308e993..73e94197 100644
--- a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs
+++ b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs
@@ -733,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 Models { get; } =
+ new() { ["Base"] = new ModelConnections("Base"), ["Refiner"] = new ModelConnections("Refiner") };
- public ConditioningNodeConnection? BaseConditioning { get; set; }
- public ConditioningNodeConnection? BaseNegativeConditioning { get; set; }
+ ///
+ /// ModelConnections from with set
+ ///
+ public IEnumerable 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; }
@@ -759,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");
}
}
From 5eaaedf4dd23fac184feca37a171db728255531f Mon Sep 17 00:00:00 2001
From: Ionite
Date: Sat, 16 Dec 2023 16:55:06 -0500
Subject: [PATCH 008/116] Add clip skip selection to ModelCard control
---
.../Controls/Inference/ModelCard.axaml | 37 ++++++++++++++++---
1 file changed, 32 insertions(+), 5 deletions(-)
diff --git a/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
index e0fc6a1c..7bf995a8 100644
--- a/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
+++ b/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
@@ -7,6 +7,7 @@
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"
x:DataType="inference:ModelCardViewModel">
@@ -21,7 +22,7 @@
-
+
-
-
+ -->
+
-
+
@@ -190,6 +192,31 @@
IsVisible="{Binding IsVaeSelectionEnabled}"
ItemsSource="{Binding ClientManager.VaeModels}"
SelectedItem="{Binding SelectedVae}" />
+
+
+
+
+
From d82f3d4843b3ec4200fa95893c946f71ea8f1d17 Mon Sep 17 00:00:00 2001
From: Ionite
Date: Sat, 16 Dec 2023 16:57:23 -0500
Subject: [PATCH 009/116] Fix Value access
---
.../Inference/Modules/ControlNetModule.cs | 26 ++++++-------------
1 file changed, 8 insertions(+), 18 deletions(-)
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs
index 4f4e7191..26bf7897 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs
+++ b/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);
}
}
}
From 1c37e9b4e400e0639be005dc30f5d89647ccd04c Mon Sep 17 00:00:00 2001
From: Ionite
Date: Sat, 16 Dec 2023 16:58:28 -0500
Subject: [PATCH 010/116] Version bump
---
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
index 446a2476..2113c78c 100644
--- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
+++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
@@ -9,7 +9,7 @@
app.manifest
true
./Assets/Icon.ico
- 2.7.0-pre.999
+ 2.8.0-dev.999
$(Version)
true
true
From ed4f78a57ece8bcbc9c548f118b3b1e4bdf4ada4 Mon Sep 17 00:00:00 2001
From: Ionite
Date: Sat, 16 Dec 2023 17:02:21 -0500
Subject: [PATCH 011/116] Add clip skip default value
---
.../ViewModels/Inference/ModelCardViewModel.cs | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
index 38e13bb2..b8a1d94b 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
@@ -9,9 +9,7 @@ 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;
-using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
@@ -208,7 +206,7 @@ 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; }
+ public int ClipSkip { get; init; } = 1;
public bool IsVaeSelectionEnabled { get; init; }
public bool IsRefinerSelectionEnabled { get; init; }
From a6c87a2be3f28c9143617266ace9b51841846006 Mon Sep 17 00:00:00 2001
From: Ionite
Date: Sat, 16 Dec 2023 21:11:56 -0500
Subject: [PATCH 012/116] Add BetterComboBox
---
StabilityMatrix.Avalonia/App.axaml | 1 +
.../Controls/BetterComboBox.cs | 41 ++++
.../ControlThemes/BetterComboBoxStyles.axaml | 202 ++++++++++++++++++
3 files changed, 244 insertions(+)
create mode 100644 StabilityMatrix.Avalonia/Controls/BetterComboBox.cs
create mode 100644 StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml
diff --git a/StabilityMatrix.Avalonia/App.axaml b/StabilityMatrix.Avalonia/App.axaml
index f54dc933..afeea869 100644
--- a/StabilityMatrix.Avalonia/App.axaml
+++ b/StabilityMatrix.Avalonia/App.axaml
@@ -24,6 +24,7 @@
+
diff --git a/StabilityMatrix.Avalonia/Controls/BetterComboBox.cs b/StabilityMatrix.Avalonia/Controls/BetterComboBox.cs
new file mode 100644
index 00000000..7209879c
--- /dev/null
+++ b/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 SelectionBoxItemTemplateProperty =
+ AvaloniaProperty.RegisterDirect(
+ 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);
+ }
+
+ ///
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+
+ if (e.NameScope.Find("ContentPresenter") is { } contentPresenter)
+ {
+ if (SelectionBoxItemTemplate is { } template)
+ {
+ contentPresenter.ContentTemplate = template;
+ }
+ }
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml b/StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml
new file mode 100644
index 00000000..887299ca
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml
@@ -0,0 +1,202 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 1afe3e6347a48a5d7a22dd4507ccfadfaf99db13 Mon Sep 17 00:00:00 2001
From: Ionite
Date: Sat, 16 Dec 2023 21:12:59 -0500
Subject: [PATCH 013/116] Add improved model card combo box styles
---
.../Controls/Inference/ModelCard.axaml | 117 ++----------------
.../DesignData/DesignData.cs | 38 ++++++
.../Models/Database/LocalModelFile.cs | 17 ++-
3 files changed, 67 insertions(+), 105 deletions(-)
diff --git a/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
index 7bf995a8..4e5a3ed2 100644
--- a/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
+++ b/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
@@ -30,112 +30,21 @@
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_Model}"
TextAlignment="Left" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ SelectedItem="{Binding SelectedModel}"/>
+
diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
index 3515089c..e49fe303 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
@@ -23,6 +23,7 @@ using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
+using StabilityMatrix.Core.Animation;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
@@ -41,9 +42,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.Base;
/// This includes a progress reporter, image output view model, and generation virtual methods.
///
[SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")]
-public abstract partial class InferenceGenerationViewModelBase
- : InferenceTabViewModelBase,
- IImageGalleryComponent
+public abstract partial class InferenceGenerationViewModelBase : InferenceTabViewModelBase, IImageGalleryComponent
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@@ -92,20 +91,14 @@ public abstract partial class InferenceGenerationViewModelBase
ImageGenerationEventArgs args,
int batchNum = 0,
int batchTotal = 0,
- bool isGrid = false
+ bool isGrid = false,
+ string fileExtension = "png"
)
{
var defaultOutputDir = settingsManager.ImagesInferenceDirectory;
defaultOutputDir.Create();
- return WriteOutputImageAsync(
- imageStream,
- defaultOutputDir,
- args,
- batchNum,
- batchTotal,
- isGrid
- );
+ return WriteOutputImageAsync(imageStream, defaultOutputDir, args, batchNum, batchTotal, isGrid, fileExtension);
}
///
@@ -117,7 +110,8 @@ public abstract partial class InferenceGenerationViewModelBase
ImageGenerationEventArgs args,
int batchNum = 0,
int batchTotal = 0,
- bool isGrid = false
+ bool isGrid = false,
+ string fileExtension = "png"
)
{
var formatTemplateStr = settingsManager.Settings.InferenceOutputImageFileNameFormat;
@@ -136,10 +130,7 @@ public abstract partial class InferenceGenerationViewModelBase
)
{
// Fallback to default
- Logger.Warn(
- "Failed to parse format template: {FormatTemplate}, using default",
- formatTemplateStr
- );
+ Logger.Warn("Failed to parse format template: {FormatTemplate}, using default", formatTemplateStr);
format = FileNameFormat.Parse(FileNameFormat.DefaultTemplate, formatProvider);
}
@@ -155,7 +146,7 @@ public abstract partial class InferenceGenerationViewModelBase
}
var fileName = format.GetFileName();
- var file = outputDir.JoinFile($"{fileName}.png");
+ var file = outputDir.JoinFile($"{fileName}.{fileExtension}");
// Until the file is free, keep adding _{i} to the end
for (var i = 0; i < 100; i++)
@@ -163,14 +154,14 @@ public abstract partial class InferenceGenerationViewModelBase
if (!file.Exists)
break;
- file = outputDir.JoinFile($"{fileName}_{i + 1}.png");
+ file = outputDir.JoinFile($"{fileName}_{i + 1}.{fileExtension}");
}
// If that fails, append an 7-char uuid
if (file.Exists)
{
var uuid = Guid.NewGuid().ToString("N")[..7];
- file = outputDir.JoinFile($"{fileName}_{uuid}.png");
+ file = outputDir.JoinFile($"{fileName}_{uuid}.{fileExtension}");
}
await using var fileStream = file.Info.OpenWrite();
@@ -200,11 +191,7 @@ public abstract partial class InferenceGenerationViewModelBase
{
var uploadName = await image.GetHashGuidFileNameAsync();
- Logger.Debug(
- "Uploading image {FileName} as {UploadName}",
- localFile.Name,
- uploadName
- );
+ Logger.Debug("Uploading image {FileName} as {UploadName}", localFile.Name, uploadName);
// For pngs, strip metadata since Pillow can't handle some valid files?
if (localFile.Info.Extension.Equals(".png", StringComparison.OrdinalIgnoreCase))
@@ -228,10 +215,7 @@ public abstract partial class InferenceGenerationViewModelBase
/// Runs a generation task
///
/// Thrown if args.Parameters or args.Project are null
- protected async Task RunGeneration(
- ImageGenerationEventArgs args,
- CancellationToken cancellationToken
- )
+ protected async Task RunGeneration(ImageGenerationEventArgs args, CancellationToken cancellationToken)
{
var client = args.Client;
var nodes = args.Nodes;
@@ -311,32 +295,18 @@ public abstract partial class InferenceGenerationViewModelBase
{
Logger.Warn(e, "Comfy node exception while queuing prompt");
await DialogHelper
- .CreateJsonDialog(
- e.JsonData,
- "Comfy Error",
- "Node execution encountered an error"
- )
+ .CreateJsonDialog(e.JsonData, "Comfy Error", "Node execution encountered an error")
.ShowAsync();
return;
}
// Get output images
- var imageOutputs = await client.GetImagesForExecutedPromptAsync(
- promptTask.Id,
- cancellationToken
- );
-
- if (
- !imageOutputs.TryGetValue(args.OutputNodeNames[0], out var images)
- || images is not { Count: > 0 }
- )
+ var imageOutputs = await client.GetImagesForExecutedPromptAsync(promptTask.Id, cancellationToken);
+
+ if (!imageOutputs.TryGetValue(args.OutputNodeNames[0], out var images) || images is not { Count: > 0 })
{
// No images match
- notificationService.Show(
- "No output",
- "Did not receive any output images",
- NotificationType.Warning
- );
+ notificationService.Show("No output", "Did not receive any output images", NotificationType.Warning);
return;
}
@@ -369,10 +339,7 @@ public abstract partial class InferenceGenerationViewModelBase
///
/// Handles image output metadata for generation runs
///
- private async Task ProcessOutputImages(
- IReadOnlyCollection images,
- ImageGenerationEventArgs args
- )
+ private async Task ProcessOutputImages(IReadOnlyCollection images, ImageGenerationEventArgs args)
{
var client = args.Client;
@@ -395,10 +362,7 @@ public abstract partial class InferenceGenerationViewModelBase
var project = args.Project!;
// Lock seed
- project.TryUpdateModel(
- "Seed",
- model => model with { IsRandomizeEnabled = false }
- );
+ project.TryUpdateModel("Seed", model => model with { IsRandomizeEnabled = false });
// Seed and batch override for batches
if (images.Count > 1 && project.ProjectType is InferenceProjectType.TextToImage)
@@ -433,6 +397,29 @@ public abstract partial class InferenceGenerationViewModelBase
outputImages.Add(new ImageSource(filePath));
EventManager.Instance.OnImageFileAdded(filePath);
}
+ else if (comfyImage.FileName.EndsWith(".webp"))
+ {
+ // Write using generated name
+ var webpFilePath = await WriteOutputImageAsync(
+ new MemoryStream(imageArray),
+ args,
+ i + 1,
+ images.Count,
+ fileExtension: Path.GetExtension(comfyImage.FileName).Replace(".", "")
+ );
+
+ // convert to gif
+ await GifConverter.ConvertWebpToGif(webpFilePath);
+ var gifFilePath = webpFilePath.ToString().Replace(".webp", ".gif");
+ if (File.Exists(gifFilePath))
+ {
+ // delete webp
+ File.Delete(webpFilePath);
+ }
+
+ outputImages.Add(new ImageSource(gifFilePath));
+ EventManager.Instance.OnImageFileAdded(gifFilePath);
+ }
else
{
// Write using generated name
@@ -440,7 +427,8 @@ public abstract partial class InferenceGenerationViewModelBase
new MemoryStream(imageArray),
args,
i + 1,
- images.Count
+ images.Count,
+ fileExtension: Path.GetExtension(comfyImage.FileName).Replace(".", "")
);
outputImages.Add(new ImageSource(filePath));
@@ -456,25 +444,14 @@ public abstract partial class InferenceGenerationViewModelBase
var project = args.Project!;
// Lock seed
- project.TryUpdateModel(
- "Seed",
- model => model with { IsRandomizeEnabled = false }
- );
+ project.TryUpdateModel("Seed", model => model with { IsRandomizeEnabled = false });
var grid = ImageProcessor.CreateImageGrid(loadedImages);
var gridBytes = grid.Encode().ToArray();
- var gridBytesWithMetadata = PngDataHelper.AddMetadata(
- gridBytes,
- args.Parameters!,
- args.Project!
- );
+ var gridBytesWithMetadata = PngDataHelper.AddMetadata(gridBytes, args.Parameters!, args.Project!);
// Save to disk
- var gridPath = await WriteOutputImageAsync(
- new MemoryStream(gridBytesWithMetadata),
- args,
- isGrid: true
- );
+ var gridPath = await WriteOutputImageAsync(new MemoryStream(gridBytesWithMetadata), args, isGrid: true);
// Insert to start of images
var gridImage = new ImageSource(gridPath);
@@ -497,10 +474,7 @@ public abstract partial class InferenceGenerationViewModelBase
///
/// Implementation for Generate Image
///
- protected virtual Task GenerateImageImpl(
- GenerateOverrides overrides,
- CancellationToken cancellationToken
- )
+ protected virtual Task GenerateImageImpl(GenerateOverrides overrides, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
@@ -511,10 +485,7 @@ public abstract partial class InferenceGenerationViewModelBase
/// Optional overrides (side buttons)
/// Cancellation token
[RelayCommand(IncludeCancelCommand = true, FlowExceptionsToTaskScheduler = true)]
- private async Task GenerateImage(
- GenerateFlags options = default,
- CancellationToken cancellationToken = default
- )
+ private async Task GenerateImage(GenerateFlags options = default, CancellationToken cancellationToken = default)
{
var overrides = GenerateOverrides.FromFlags(options);
@@ -555,21 +526,19 @@ public abstract partial class InferenceGenerationViewModelBase
/// Handles the progress update received event from the websocket.
/// Updates the progress view model.
///
- protected virtual void OnProgressUpdateReceived(
- object? sender,
- ComfyProgressUpdateEventArgs args
- )
+ protected virtual void OnProgressUpdateReceived(object? sender, ComfyProgressUpdateEventArgs args)
{
- Dispatcher.UIThread.Post(() =>
- {
- OutputProgress.Value = args.Value;
- OutputProgress.Maximum = args.Maximum;
- OutputProgress.IsIndeterminate = false;
+ Dispatcher
+ .UIThread
+ .Post(() =>
+ {
+ OutputProgress.Value = args.Value;
+ OutputProgress.Maximum = args.Maximum;
+ OutputProgress.IsIndeterminate = false;
- OutputProgress.Text =
- $"({args.Value} / {args.Maximum})"
- + (args.RunningNode != null ? $" {args.RunningNode}" : "");
- });
+ OutputProgress.Text =
+ $"({args.Value} / {args.Maximum})" + (args.RunningNode != null ? $" {args.RunningNode}" : "");
+ });
}
private void AttachRunningNodeChangedHandler(ComfyTask comfyTask)
@@ -594,13 +563,15 @@ public abstract partial class InferenceGenerationViewModelBase
return;
}
- Dispatcher.UIThread.Post(() =>
- {
- OutputProgress.IsIndeterminate = true;
- OutputProgress.Value = 100;
- OutputProgress.Maximum = 100;
- OutputProgress.Text = nodeName;
- });
+ Dispatcher
+ .UIThread
+ .Post(() =>
+ {
+ OutputProgress.IsIndeterminate = true;
+ OutputProgress.Value = 100;
+ OutputProgress.Maximum = 100;
+ OutputProgress.Text = nodeName;
+ });
}
public class ImageGenerationEventArgs : EventArgs
@@ -628,11 +599,7 @@ public abstract partial class InferenceGenerationViewModelBase
overrides[typeof(HiresFixModule)] = args.Overrides.IsHiresFixEnabled.Value;
}
- return new ModuleApplyStepEventArgs
- {
- Builder = args.Builder,
- IsEnabledOverrides = overrides
- };
+ return new ModuleApplyStepEventArgs { Builder = args.Builder, IsEnabledOverrides = overrides };
}
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs
index 965b787f..fdd9a520 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs
@@ -28,24 +28,16 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
- private static readonly Type[] SerializerIgnoredTypes =
- {
- typeof(ICommand),
- typeof(IRelayCommand)
- };
+ private static readonly Type[] SerializerIgnoredTypes = { typeof(ICommand), typeof(IRelayCommand) };
private static readonly string[] SerializerIgnoredNames = { nameof(HasErrors) };
- private static readonly JsonSerializerOptions SerializerOptions =
- new() { IgnoreReadOnlyProperties = true };
+ private static readonly JsonSerializerOptions SerializerOptions = new() { IgnoreReadOnlyProperties = true };
private static bool ShouldIgnoreProperty(PropertyInfo property)
{
// Skip if read-only and not IJsonLoadableState
- if (
- property.SetMethod is null
- && !typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)
- )
+ if (property.SetMethod is null && !typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType))
{
Logger.ConditionalTrace("Skipping {Property} - read-only", property.Name);
return true;
@@ -107,11 +99,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
{
// Get all of our properties using reflection
var properties = GetType().GetProperties();
- Logger.ConditionalTrace(
- "Serializing {Type} with {Count} properties",
- GetType(),
- properties.Length
- );
+ Logger.ConditionalTrace("Serializing {Type} with {Count} properties", GetType(), properties.Length);
foreach (var property in properties)
{
@@ -119,9 +107,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
// If JsonPropertyName provided, use that as the key
if (
- property
- .GetCustomAttributes(typeof(JsonPropertyNameAttribute), true)
- .FirstOrDefault()
+ property.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).FirstOrDefault()
is JsonPropertyNameAttribute jsonPropertyName
)
{
@@ -168,10 +154,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
if (property.GetValue(this) is not IJsonLoadableState propertyValue)
{
// If null, it must have a default constructor
- if (
- property.PropertyType.GetConstructor(Type.EmptyTypes)
- is not { } constructorInfo
- )
+ if (property.PropertyType.GetConstructor(Type.EmptyTypes) is not { } constructorInfo)
{
throw new InvalidOperationException(
$"Property {property.Name} is IJsonLoadableState but current object is null and has no default constructor"
@@ -188,11 +171,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
}
else
{
- Logger.ConditionalTrace(
- "Loading {Property} ({Type})",
- property.Name,
- property.PropertyType
- );
+ Logger.ConditionalTrace("Loading {Property} ({Type})", property.Name, property.PropertyType);
var propertyValue = value.Deserialize(property.PropertyType, SerializerOptions);
property.SetValue(this, propertyValue);
@@ -216,11 +195,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
{
// Get all of our properties using reflection.
var properties = GetType().GetProperties();
- Logger.ConditionalTrace(
- "Serializing {Type} with {Count} properties",
- GetType(),
- properties.Length
- );
+ Logger.ConditionalTrace("Serializing {Type} with {Count} properties", GetType(), properties.Length);
// Create a JSON object to store the state.
var state = new JsonObject();
@@ -237,9 +212,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
// If JsonPropertyName provided, use that as the key.
if (
- property
- .GetCustomAttributes(typeof(JsonPropertyNameAttribute), true)
- .FirstOrDefault()
+ property.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).FirstOrDefault()
is JsonPropertyNameAttribute jsonPropertyName
)
{
@@ -270,11 +243,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
}
else
{
- Logger.ConditionalTrace(
- "Serializing {Property} ({Type})",
- property.Name,
- property.PropertyType
- );
+ Logger.ConditionalTrace("Serializing {Property} ({Type})", property.Name, property.PropertyType);
var value = property.GetValue(this);
if (value is not null)
{
@@ -297,8 +266,7 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
protected static JsonObject SerializeModel(T model)
{
var node = JsonSerializer.SerializeToNode(model);
- return node?.AsObject()
- ?? throw new NullReferenceException("Failed to serialize state to JSON object.");
+ return node?.AsObject() ?? throw new NullReferenceException("Failed to serialize state to JSON object.");
}
///
diff --git a/StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs
index 34253d7b..f6893400 100644
--- a/StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/ConsoleViewModel.cs
@@ -7,9 +7,9 @@ using System.Threading.Tasks.Dataflow;
using Avalonia.Threading;
using AvaloniaEdit.Document;
using CommunityToolkit.Mvvm.ComponentModel;
+using NLog;
using Nito.AsyncEx;
using Nito.AsyncEx.Synchronous;
-using NLog;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Processes;
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
index 5cab55c2..8a5dae66 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
@@ -126,10 +126,7 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
ShowProgressBar = true;
IsProgressIndeterminate = true;
- UpdateText = string.Format(
- Resources.TextTemplate_UpdatingPackage,
- Resources.Label_StabilityMatrix
- );
+ UpdateText = string.Format(Resources.TextTemplate_UpdatingPackage, Resources.Label_StabilityMatrix);
try
{
@@ -173,10 +170,7 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
UpdateText = "Getting a few things ready...";
await using (new MinimumDelay(500, 1000))
{
- Process.Start(
- UpdateHelper.ExecutablePath,
- $"--wait-for-exit-pid {Environment.ProcessId}"
- );
+ Process.Start(UpdateHelper.ExecutablePath, $"--wait-for-exit-pid {Environment.ProcessId}");
}
UpdateText = "Update complete. Restarting Stability Matrix in 3 seconds...";
@@ -189,7 +183,7 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
App.Shutdown();
}
-
+
internal async Task GetReleaseNotes(string changelogUrl)
{
using var client = httpClientFactory.CreateClient();
@@ -262,15 +256,10 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
// Join all blocks until and excluding the current version
// If we're on a pre-release, include the current release
- var currentVersionBlock = results.FindIndex(
- x => x.Version == currentVersion.WithoutMetadata()
- );
+ var currentVersionBlock = results.FindIndex(x => x.Version == currentVersion.WithoutMetadata());
// For mismatching build metadata, add one
- if (
- currentVersionBlock != -1
- && results[currentVersionBlock].Version?.Metadata != currentVersion.Metadata
- )
+ if (currentVersionBlock != -1 && results[currentVersionBlock].Version?.Metadata != currentVersion.Metadata)
{
currentVersionBlock++;
}
@@ -278,9 +267,7 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
// Support for previous pre-release without changelogs
if (currentVersionBlock == -1)
{
- currentVersionBlock = results.FindIndex(
- x => x.Version == currentVersion.WithoutPrereleaseOrMetadata()
- );
+ currentVersionBlock = results.FindIndex(x => x.Version == currentVersion.WithoutPrereleaseOrMetadata());
// Add 1 if found to include the current release
if (currentVersionBlock != -1)
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
index 3a6c5f5d..13183691 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
@@ -7,18 +7,20 @@ using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
using NLog;
-using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.Inference;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
-using StabilityMatrix.Avalonia.ViewModels.Inference.Modules;
using StabilityMatrix.Avalonia.ViewModels.Inference.Video;
using StabilityMatrix.Avalonia.Views.Inference;
using StabilityMatrix.Core.Attributes;
+using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
+using StabilityMatrix.Core.Models.Api.Comfy;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
+using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
#pragma warning disable CS0657 // Not a valid attribute location for this declaration
@@ -28,9 +30,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(InferenceImageToVideoView), persistent: true)]
[ManagedService]
[Transient]
-public class InferenceImageToVideoViewModel
- : InferenceGenerationViewModelBase,
- IParametersLoadableState
+public partial class InferenceImageToVideoViewModel : InferenceGenerationViewModelBase, IParametersLoadableState
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@@ -61,6 +61,10 @@ public class InferenceImageToVideoViewModel
[JsonPropertyName("VideoOutput")]
public VideoOutputSettingsCardViewModel VideoOutputSettingsCardViewModel { get; }
+ [ObservableProperty]
+ [JsonIgnore]
+ private string outputUri;
+
public InferenceImageToVideoViewModel(
INotificationService notificationService,
IInferenceClientManager inferenceClientManager,
@@ -86,6 +90,10 @@ public class InferenceImageToVideoViewModel
samplerCard.IsCfgScaleEnabled = true;
samplerCard.IsSamplerSelectionEnabled = true;
samplerCard.IsSchedulerSelectionEnabled = true;
+ samplerCard.CfgScale = 2.5d;
+ samplerCard.SelectedSampler = ComfySampler.Euler;
+ samplerCard.SelectedScheduler = ComfyScheduler.Karras;
+ samplerCard.IsDenoiseStrengthEnabled = true;
});
BatchSizeCardViewModel = vmFactory.Get();
@@ -105,6 +113,19 @@ public class InferenceImageToVideoViewModel
);
}
+ public override void OnLoaded()
+ {
+ EventManager.Instance.ImageFileAdded += OnImageFileAdded;
+ }
+
+ private void OnImageFileAdded(object? sender, FilePath e)
+ {
+ if (!e.Extension.Contains("gif"))
+ return;
+
+ OutputUri = e;
+ }
+
///
protected override void BuildPrompt(BuildPromptEventArgs args)
{
@@ -122,19 +143,19 @@ public class InferenceImageToVideoViewModel
ModelCardViewModel.ApplyStep(args);
// Setup latent from image
- var imageLoad = builder.Nodes.AddTypedNode(
- new ComfyNodeBuilder.LoadImage
- {
- Name = builder.Nodes.GetUniqueName("ControlNet_LoadImage"),
- Image =
- SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference")
- ?? throw new ValidationException()
- }
- );
+ var imageLoad = builder
+ .Nodes
+ .AddTypedNode(
+ new ComfyNodeBuilder.LoadImage
+ {
+ Name = builder.Nodes.GetUniqueName("ControlNet_LoadImage"),
+ Image =
+ SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference")
+ ?? throw new ValidationException()
+ }
+ );
builder.Connections.Primary = imageLoad.Output1;
- builder.Connections.PrimarySize =
- SelectImageCardViewModel.CurrentBitmapSize
- ?? new Size(SamplerCardViewModel.Width, SamplerCardViewModel.Height);
+ builder.Connections.PrimarySize = SelectImageCardViewModel.CurrentBitmapSize;
// Setup img2vid stuff
// Set width & height from SamplerCard
@@ -159,10 +180,7 @@ public class InferenceImageToVideoViewModel
}
///
- protected override async Task GenerateImageImpl(
- GenerateOverrides overrides,
- CancellationToken cancellationToken
- )
+ protected override async Task GenerateImageImpl(GenerateOverrides overrides, CancellationToken cancellationToken)
{
if (!await CheckClientConnectedWithPrompt() || !ClientManager.IsConnected)
{
@@ -184,11 +202,7 @@ public class InferenceImageToVideoViewModel
{
var seed = seedCard.Seed + i;
- var buildPromptArgs = new BuildPromptEventArgs
- {
- Overrides = overrides,
- SeedOverride = seed
- };
+ var buildPromptArgs = new BuildPromptEventArgs { Overrides = overrides, SeedOverride = seed };
BuildPrompt(buildPromptArgs);
var generationArgs = new ImageGenerationEventArgs
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs
index 4531d1b0..2ca1dbd8 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageUpscaleViewModel.cs
@@ -107,9 +107,7 @@ public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase
// If upscale is enabled, add another upscale group
if (IsUpscaleEnabled)
{
- var upscaleSize = builder.Connections.PrimarySize.WithScale(
- UpscalerCardViewModel.Scale
- );
+ var upscaleSize = builder.Connections.PrimarySize.WithScale(UpscalerCardViewModel.Scale);
// Build group
builder.Connections.Primary = builder
@@ -144,10 +142,7 @@ public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase
}
///
- protected override async Task GenerateImageImpl(
- GenerateOverrides overrides,
- CancellationToken cancellationToken
- )
+ protected override async Task GenerateImageImpl(GenerateOverrides overrides, CancellationToken cancellationToken)
{
if (!ClientManager.IsConnected)
{
@@ -174,10 +169,7 @@ public class InferenceImageUpscaleViewModel : InferenceGenerationViewModelBase
Client = ClientManager.Client,
Nodes = buildPromptArgs.Builder.ToNodeDictionary(),
OutputNodeNames = buildPromptArgs.Builder.Connections.OutputNodeNames.ToArray(),
- Parameters = new GenerationParameters
- {
- ModelName = UpscalerCardViewModel.SelectedUpscaler?.Name,
- },
+ Parameters = new GenerationParameters { ModelName = UpscalerCardViewModel.SelectedUpscaler?.Name, },
Project = InferenceProjectDocument.FromLoadable(this)
};
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
index 31ce9843..764f7a07 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs
@@ -26,9 +26,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(InferenceTextToImageView), IsPersistent = true)]
[ManagedService]
[Transient]
-public class InferenceTextToImageViewModel
- : InferenceGenerationViewModelBase,
- IParametersLoadableState
+public class InferenceTextToImageViewModel : InferenceGenerationViewModelBase, IParametersLoadableState
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@@ -162,22 +160,19 @@ public class InferenceTextToImageViewModel
///
protected override IEnumerable GetInputImages()
{
- var samplerImages = SamplerCardViewModel.ModulesCardViewModel.Cards
+ var samplerImages = SamplerCardViewModel
+ .ModulesCardViewModel
+ .Cards
.OfType()
.SelectMany(m => m.GetInputImages());
- var moduleImages = ModulesCardViewModel.Cards
- .OfType()
- .SelectMany(m => m.GetInputImages());
+ var moduleImages = ModulesCardViewModel.Cards.OfType().SelectMany(m => m.GetInputImages());
return samplerImages.Concat(moduleImages);
}
///
- protected override async Task GenerateImageImpl(
- GenerateOverrides overrides,
- CancellationToken cancellationToken
- )
+ protected override async Task GenerateImageImpl(GenerateOverrides overrides, CancellationToken cancellationToken)
{
// Validate the prompts
if (!await PromptCardViewModel.ValidatePrompts())
@@ -205,11 +200,7 @@ public class InferenceTextToImageViewModel
{
var seed = seedCard.Seed + i;
- var buildPromptArgs = new BuildPromptEventArgs
- {
- Overrides = overrides,
- SeedOverride = seed
- };
+ var buildPromptArgs = new BuildPromptEventArgs { Overrides = overrides, SeedOverride = seed };
BuildPrompt(buildPromptArgs);
var generationArgs = new ImageGenerationEventArgs
@@ -270,16 +261,12 @@ public class InferenceTextToImageViewModel
if (state.TryGetPropertyValue("HiresSampler", out var hiresSamplerState))
{
- module
- .GetCard()
- .LoadStateFromJsonObject(hiresSamplerState!.AsObject());
+ module.GetCard().LoadStateFromJsonObject(hiresSamplerState!.AsObject());
}
if (state.TryGetPropertyValue("HiresUpscaler", out var hiresUpscalerState))
{
- module
- .GetCard()
- .LoadStateFromJsonObject(hiresUpscalerState!.AsObject());
+ module.GetCard().LoadStateFromJsonObject(hiresUpscalerState!.AsObject());
}
});
@@ -289,17 +276,17 @@ public class InferenceTextToImageViewModel
if (state.TryGetPropertyValue("Upscaler", out var upscalerState))
{
- module
- .GetCard()
- .LoadStateFromJsonObject(upscalerState!.AsObject());
+ module.GetCard().LoadStateFromJsonObject(upscalerState!.AsObject());
}
});
// Add FreeU to sampler
- SamplerCardViewModel.ModulesCardViewModel.AddModule(module =>
- {
- module.IsEnabled = state.GetPropertyValueOrDefault("IsFreeUEnabled");
- });
+ SamplerCardViewModel
+ .ModulesCardViewModel
+ .AddModule(module =>
+ {
+ module.IsEnabled = state.GetPropertyValueOrDefault("IsFreeUEnabled");
+ });
}
base.LoadStateFromJsonObject(state);
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
index 51a99498..2a4f652f 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
@@ -10,8 +10,8 @@ using StabilityMatrix.Avalonia.Services;
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;
+using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
@@ -42,6 +42,7 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
private bool disableSettings;
public IInferenceClientManager ClientManager { get; } = clientManager;
+
///
public virtual void ApplyStep(ModuleApplyStepEventArgs e)
{
@@ -50,9 +51,7 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
new ComfyNodeBuilder.CheckpointLoaderSimple
{
Name = "CheckpointLoader",
- CkptName =
- SelectedModel?.RelativePath
- ?? throw new ValidationException("Model not selected")
+ CkptName = SelectedModel?.RelativePath ?? throw new ValidationException("Model not selected")
}
);
@@ -85,9 +84,7 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
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")
}
);
@@ -147,19 +144,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)
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs
index 4f4e7191..2433debc 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/Modules/ControlNetModule.cs
@@ -42,9 +42,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 +51,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 +61,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,
@@ -93,10 +88,7 @@ public class ControlNetModule : ModuleBase
}
);
- e.Temp.RefinerConditioning = (
- controlNetRefinerApply.Output1,
- controlNetRefinerApply.Output2
- );
+ e.Temp.RefinerConditioning = (controlNetRefinerApply.Output1, controlNetRefinerApply.Output2);
}
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
index c1c66a72..912a34f1 100644
--- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
@@ -54,11 +54,9 @@ public partial class OutputsPageViewModel : PageViewModelBase
private readonly ILogger logger;
public override string Title => Resources.Label_OutputsPageTitle;
- public override IconSource IconSource =>
- new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true };
+ public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Grid, IsFilled = true };
- public SourceCache OutputsCache { get; } =
- new(file => file.AbsolutePath);
+ public SourceCache OutputsCache { get; } = new(file => file.AbsolutePath);
public IObservableCollection Outputs { get; set; } =
new ObservableCollectionExtended();
@@ -88,8 +86,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
[ObservableProperty]
private bool isConsolidating;
- public bool CanShowOutputTypes =>
- SelectedCategory?.Name?.Equals("Shared Output Folder") ?? false;
+ public bool CanShowOutputTypes => SelectedCategory?.Name?.Equals("Shared Output Folder") ?? false;
public string NumImagesSelected =>
NumItemsSelected == 1
@@ -163,10 +160,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
GetOutputs(path);
}
- partial void OnSelectedCategoryChanged(
- PackageOutputCategory? oldValue,
- PackageOutputCategory? newValue
- )
+ partial void OnSelectedCategoryChanged(PackageOutputCategory? oldValue, PackageOutputCategory? newValue)
{
if (oldValue == newValue || newValue == null)
return;
@@ -217,13 +211,11 @@ public partial class OutputsPageViewModel : PageViewModelBase
var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item.ImageFile };
using var onNext = Observable
- .FromEventPattern(
- vm,
- nameof(ImageViewerViewModel.NavigationRequested)
- )
+ .FromEventPattern(vm, nameof(ImageViewerViewModel.NavigationRequested))
.Subscribe(ctx =>
{
- Dispatcher.UIThread
+ Dispatcher
+ .UIThread
.InvokeAsync(async () =>
{
var sender = (ImageViewerViewModel)ctx.Sender!;
@@ -232,9 +224,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
if (newIndex >= 0 && newIndex < Outputs.Count)
{
var newImage = Outputs[newIndex];
- var newImageSource = new ImageSource(
- new FilePath(newImage.ImageFile.AbsolutePath)
- );
+ var newImageSource = new ImageSource(new FilePath(newImage.ImageFile.AbsolutePath));
// Preload
await newImageSource.GetBitmapAsync();
@@ -386,14 +376,16 @@ public partial class OutputsPageViewModel : PageViewModelBase
public async Task ConsolidateImages()
{
var stackPanel = new StackPanel();
- stackPanel.Children.Add(
- new TextBlock
- {
- Text = Resources.Label_ConsolidateExplanation,
- TextWrapping = TextWrapping.Wrap,
- Margin = new Thickness(0, 8, 0, 16)
- }
- );
+ stackPanel
+ .Children
+ .Add(
+ new TextBlock
+ {
+ Text = Resources.Label_ConsolidateExplanation,
+ TextWrapping = TextWrapping.Wrap,
+ Margin = new Thickness(0, 8, 0, 16)
+ }
+ );
foreach (var category in Categories)
{
if (category.Name == "Shared Output Folder")
@@ -401,15 +393,17 @@ public partial class OutputsPageViewModel : PageViewModelBase
continue;
}
- stackPanel.Children.Add(
- new CheckBox
- {
- Content = $"{category.Name} ({category.Path})",
- IsChecked = true,
- Margin = new Thickness(0, 8, 0, 0),
- Tag = category.Path
- }
- );
+ stackPanel
+ .Children
+ .Add(
+ new CheckBox
+ {
+ Content = $"{category.Name} ({category.Path})",
+ IsChecked = true,
+ Margin = new Thickness(0, 8, 0, 0),
+ Tag = category.Path
+ }
+ );
}
var confirmationDialog = new BetterContentDialog
@@ -430,25 +424,14 @@ public partial class OutputsPageViewModel : PageViewModelBase
Directory.CreateDirectory(settingsManager.ConsolidatedImagesDirectory);
- foreach (
- var category in stackPanel.Children.OfType().Where(c => c.IsChecked == true)
- )
+ foreach (var category in stackPanel.Children.OfType().Where(c => c.IsChecked == true))
{
- if (
- string.IsNullOrWhiteSpace(category.Tag?.ToString())
- || !Directory.Exists(category.Tag?.ToString())
- )
+ if (string.IsNullOrWhiteSpace(category.Tag?.ToString()) || !Directory.Exists(category.Tag?.ToString()))
continue;
var directory = category.Tag.ToString();
- foreach (
- var path in Directory.EnumerateFiles(
- directory,
- "*.png",
- SearchOption.AllDirectories
- )
- )
+ foreach (var path in Directory.EnumerateFiles(directory, "*.png", SearchOption.AllDirectories))
{
try
{
@@ -499,10 +482,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
if (
!Directory.Exists(directory)
- && (
- SelectedCategory.Path != settingsManager.ImagesDirectory
- || SelectedOutputType != SharedOutputType.All
- )
+ && (SelectedCategory.Path != settingsManager.ImagesDirectory || SelectedOutputType != SharedOutputType.All)
)
{
Directory.CreateDirectory(directory);
@@ -534,23 +514,18 @@ public partial class OutputsPageViewModel : PageViewModelBase
var previouslySelectedCategory = SelectedCategory;
- var packageCategories = settingsManager.Settings.InstalledPackages
+ var packageCategories = settingsManager
+ .Settings
+ .InstalledPackages
.Where(x => !x.UseSharedOutputFolder)
.Select(packageFactory.GetPackagePair)
.WhereNotNull()
- .Where(
- p =>
- p.BasePackage.SharedOutputFolders != null
- && p.BasePackage.SharedOutputFolders.Any()
- )
+ .Where(p => p.BasePackage.SharedOutputFolders != null && p.BasePackage.SharedOutputFolders.Any())
.Select(
pair =>
new PackageOutputCategory
{
- Path = Path.Combine(
- pair.InstalledPackage.FullPath!,
- pair.BasePackage.OutputFolderName
- ),
+ Path = Path.Combine(pair.InstalledPackage.FullPath!, pair.BasePackage.OutputFolderName),
Name = pair.InstalledPackage.DisplayName ?? ""
}
)
@@ -558,25 +533,16 @@ public partial class OutputsPageViewModel : PageViewModelBase
packageCategories.Insert(
0,
- new PackageOutputCategory
- {
- Path = settingsManager.ImagesDirectory,
- Name = "Shared Output Folder"
- }
+ new PackageOutputCategory { Path = settingsManager.ImagesDirectory, Name = "Shared Output Folder" }
);
packageCategories.Insert(
1,
- new PackageOutputCategory
- {
- Path = settingsManager.ImagesInferenceDirectory,
- Name = "Inference"
- }
+ new PackageOutputCategory { Path = settingsManager.ImagesInferenceDirectory, Name = "Inference" }
);
Categories = new ObservableCollection(packageCategories);
SelectedCategory =
- Categories.FirstOrDefault(x => x.Name == previouslySelectedCategory?.Name)
- ?? Categories.First();
+ Categories.FirstOrDefault(x => x.Name == previouslySelectedCategory?.Name) ?? Categories.First();
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs
index f88d17f7..6f3c3c4c 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Settings/InferenceSettingsViewModel.cs
@@ -169,7 +169,7 @@ public partial class InferenceSettingsViewModel : PageViewModelBase
var files = await storage.OpenFilePickerAsync(
new FilePickerOpenOptions
{
- FileTypeFilter = new List { new("CSV") { Patterns = ["*.csv"] } }
+ FileTypeFilter = new List { new("CSV") { Patterns = ["*.csv"] } }
}
);
diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml
index 02a2fd50..92a5866c 100644
--- a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml
+++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml
@@ -10,6 +10,7 @@
xmlns:vmInference="using:StabilityMatrix.Avalonia.ViewModels.Inference"
xmlns:dock="clr-namespace:StabilityMatrix.Avalonia.Controls.Dock"
xmlns:modelsInference="clr-namespace:StabilityMatrix.Avalonia.Models.Inference"
+ xmlns:gif="clr-namespace:Avalonia.Gif;assembly=Avalonia.Gif"
d:DataContext="{x:Static mocks:DesignData.InferenceImageToVideoViewModel}"
d:DesignHeight="800"
d:DesignWidth="1000"
@@ -93,9 +94,9 @@
-
+ SourceUri="{Binding OutputUri}" />
+
diff --git a/StabilityMatrix.sln b/StabilityMatrix.sln
index 77cf2c6d..ff5b52a4 100644
--- a/StabilityMatrix.sln
+++ b/StabilityMatrix.sln
@@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.Avalonia.Di
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.UITests", "StabilityMatrix.UITests\StabilityMatrix.UITests.csproj", "{8C7EDDD1-7FC1-4A15-B379-910A8DA7BCA6}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Gif", "Avalonia.Gif\Avalonia.Gif.csproj", "{72A73F1E-024B-4A25-AD34-626198D9527F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -43,6 +45,10 @@ Global
{8C7EDDD1-7FC1-4A15-B379-910A8DA7BCA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C7EDDD1-7FC1-4A15-B379-910A8DA7BCA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C7EDDD1-7FC1-4A15-B379-910A8DA7BCA6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {72A73F1E-024B-4A25-AD34-626198D9527F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {72A73F1E-024B-4A25-AD34-626198D9527F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {72A73F1E-024B-4A25-AD34-626198D9527F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {72A73F1E-024B-4A25-AD34-626198D9527F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
From a2d8040310eb27678d318678360ed74b2eb15c6c Mon Sep 17 00:00:00 2001
From: JT
Date: Sun, 17 Dec 2023 01:09:02 -0800
Subject: [PATCH 016/116] faster gif conversion
---
.../Base/InferenceGenerationViewModelBase.cs | 19 +++++---
.../InferenceImageToVideoViewModel.cs | 14 ++++++
.../Inference/InferenceImageToVideoView.axaml | 13 +++--
.../Animation/GifConverter.cs | 48 ++++++++++++++++---
.../Models/Database/LocalImageFile.cs | 20 ++------
.../StabilityMatrix.Core.csproj | 3 ++
6 files changed, 86 insertions(+), 31 deletions(-)
diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
index e49fe303..51ea578b 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
@@ -409,13 +409,20 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
);
// convert to gif
- await GifConverter.ConvertWebpToGif(webpFilePath);
+ var inputStream = File.OpenRead(webpFilePath);
var gifFilePath = webpFilePath.ToString().Replace(".webp", ".gif");
- if (File.Exists(gifFilePath))
- {
- // delete webp
- File.Delete(webpFilePath);
- }
+ var outputStream = File.OpenWrite(gifFilePath);
+
+ await GifConverter.ConvertAnimatedWebpToGifAsync(inputStream, outputStream);
+ await inputStream.DisposeAsync();
+ await outputStream.FlushAsync();
+ await outputStream.DisposeAsync();
+
+ // if (File.Exists(gifFilePath))
+ // {
+ // // delete webp
+ // File.Delete(webpFilePath);
+ // }
outputImages.Add(new ImageSource(gifFilePath));
EventManager.Instance.OnImageFileAdded(gifFilePath);
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
index 13183691..b1e6d838 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
@@ -65,6 +65,10 @@ public partial class InferenceImageToVideoViewModel : InferenceGenerationViewMod
[JsonIgnore]
private string outputUri;
+ [ObservableProperty]
+ [JsonIgnore]
+ private bool isGenerating;
+
public InferenceImageToVideoViewModel(
INotificationService notificationService,
IInferenceClientManager inferenceClientManager,
@@ -94,6 +98,7 @@ public partial class InferenceImageToVideoViewModel : InferenceGenerationViewMod
samplerCard.SelectedSampler = ComfySampler.Euler;
samplerCard.SelectedScheduler = ComfyScheduler.Karras;
samplerCard.IsDenoiseStrengthEnabled = true;
+ samplerCard.DenoiseStrength = 1.0f;
});
BatchSizeCardViewModel = vmFactory.Get();
@@ -118,6 +123,11 @@ public partial class InferenceImageToVideoViewModel : InferenceGenerationViewMod
EventManager.Instance.ImageFileAdded += OnImageFileAdded;
}
+ public override void OnUnloaded()
+ {
+ EventManager.Instance.ImageFileAdded -= OnImageFileAdded;
+ }
+
private void OnImageFileAdded(object? sender, FilePath e)
{
if (!e.Extension.Contains("gif"))
@@ -219,11 +229,15 @@ public partial class InferenceImageToVideoViewModel : InferenceGenerationViewMod
batchArgs.Add(generationArgs);
}
+ IsGenerating = true;
+
// Run batches
foreach (var args in batchArgs)
{
await RunGeneration(args, cancellationToken);
}
+
+ IsGenerating = false;
}
///
diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml
index 92a5866c..c3497945 100644
--- a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml
+++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml
@@ -94,10 +94,17 @@
-
-
+ IsVisible="{Binding ElementName=Dock, Path=DataContext.IsGenerating}"
+ DataContext="{Binding ImageGalleryCardViewModel}"/>
+
+
+
+
EnumerateAnimatedWebp(Stream webpSource)
{
- using var webp = new MagickImageCollection(filePath, MagickFormat.WebP);
- var path = filePath.ToString().Replace(".webp", ".gif");
- await webp.WriteAsync(path, MagickFormat.Gif).ConfigureAwait(false);
+ using var webp = new SKManagedStream(webpSource);
+ using var codec = SKCodec.Create(webp);
+
+ var info = new SKImageInfo(codec.Info.Width, codec.Info.Height);
+
+ for (var i = 0; i < codec.FrameCount; i++)
+ {
+ using var tempSurface = new SKBitmap(info);
+
+ codec.GetFrameInfo(i, out var frameInfo);
+
+ var decodeInfo = info.WithAlphaType(frameInfo.AlphaType);
+
+ tempSurface.TryAllocPixels(decodeInfo);
+
+ var result = codec.GetPixels(decodeInfo, tempSurface.GetPixels(), new SKCodecOptions(i));
+
+ if (result != SKCodecResult.Success)
+ throw new InvalidDataException($"Could not decode frame {i} of {codec.FrameCount}.");
+
+ using var peekPixels = tempSurface.PeekPixels();
+
+ yield return peekPixels.GetReadableBitmapData(WorkingColorSpace.Default);
+ }
+ }
+
+ public static Task ConvertAnimatedWebpToGifAsync(Stream webpSource, Stream gifOutput)
+ {
+ var gifBitmaps = EnumerateAnimatedWebp(webpSource);
+
+ return GifEncoder.EncodeAnimationAsync(
+ new AnimatedGifConfiguration(gifBitmaps, TimeSpan.FromMilliseconds(150))
+ {
+ Quantizer = OptimizedPaletteQuantizer.Wu(alphaThreshold: 0),
+ AllowDeltaFrames = true
+ },
+ gifOutput
+ );
}
}
diff --git a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs
index 1abde8dd..3f327f28 100644
--- a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs
+++ b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs
@@ -48,19 +48,9 @@ public record LocalImageFile
///
public string FileNameWithoutExtension => Path.GetFileNameWithoutExtension(AbsolutePath);
- public (
- string? Parameters,
- string? ParametersJson,
- string? SMProject,
- string? ComfyNodes
- ) ReadMetadata()
+ public (string? Parameters, string? ParametersJson, string? SMProject, string? ComfyNodes) ReadMetadata()
{
- using var stream = new FileStream(
- AbsolutePath,
- FileMode.Open,
- FileAccess.Read,
- FileShare.Read
- );
+ using var stream = new FileStream(AbsolutePath, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new BinaryReader(stream);
var parameters = ImageMetadata.ReadTextChunk(reader, "parameters");
@@ -79,8 +69,7 @@ public record LocalImageFile
public static LocalImageFile FromPath(FilePath filePath)
{
// TODO: Support other types
- const LocalImageFileType imageType =
- LocalImageFileType.Inference | LocalImageFileType.TextToImage;
+ const LocalImageFileType imageType = LocalImageFileType.Inference | LocalImageFileType.TextToImage;
// Get metadata
using var stream = filePath.Info.OpenRead();
@@ -115,6 +104,5 @@ public record LocalImageFile
};
}
- public static readonly HashSet SupportedImageExtensions =
- new() { ".png", ".jpg", ".jpeg", ".webp" };
+ public static readonly HashSet SupportedImageExtensions = [".png", ".jpg", ".jpeg", ".gif"];
}
diff --git a/StabilityMatrix.Core/StabilityMatrix.Core.csproj b/StabilityMatrix.Core/StabilityMatrix.Core.csproj
index ab540ac6..d64996a3 100644
--- a/StabilityMatrix.Core/StabilityMatrix.Core.csproj
+++ b/StabilityMatrix.Core/StabilityMatrix.Core.csproj
@@ -29,6 +29,8 @@
+
+
@@ -51,6 +53,7 @@
+
From 280fc8c89f857610bfe69ec0001b9d3a2994a03c Mon Sep 17 00:00:00 2001
From: Ionite
Date: Sun, 17 Dec 2023 18:02:58 -0500
Subject: [PATCH 017/116] Add animated webp rendering
---
Avalonia.Gif/Avalonia.Gif.csproj | 1 +
Avalonia.Gif/GifImage.cs | 9 +-
Avalonia.Gif/GifInstance.cs | 2 +-
Avalonia.Gif/IGifInstance.cs | 15 ++
Avalonia.Gif/WebpInstance.cs | 180 ++++++++++++++++++
.../Base/InferenceGenerationViewModelBase.cs | 8 +-
.../InferenceImageToVideoViewModel.cs | 2 +-
.../Models/Database/LocalImageFile.cs | 2 +-
8 files changed, 208 insertions(+), 11 deletions(-)
create mode 100644 Avalonia.Gif/IGifInstance.cs
create mode 100644 Avalonia.Gif/WebpInstance.cs
diff --git a/Avalonia.Gif/Avalonia.Gif.csproj b/Avalonia.Gif/Avalonia.Gif.csproj
index 2d72f153..49c6015e 100644
--- a/Avalonia.Gif/Avalonia.Gif.csproj
+++ b/Avalonia.Gif/Avalonia.Gif.csproj
@@ -11,6 +11,7 @@
+
diff --git a/Avalonia.Gif/GifImage.cs b/Avalonia.Gif/GifImage.cs
index d53cbd19..0ce8dca8 100644
--- a/Avalonia.Gif/GifImage.cs
+++ b/Avalonia.Gif/GifImage.cs
@@ -32,7 +32,7 @@ namespace Avalonia.Gif
IterationCount
>("IterationCount", IterationCount.Infinite);
- private GifInstance? _gifInstance;
+ private IGifInstance? _gifInstance;
public static readonly StyledProperty StretchDirectionProperty = AvaloniaProperty.Register<
GifImage,
@@ -166,7 +166,7 @@ namespace Avalonia.Gif
{
private TimeSpan _animationElapsed;
private TimeSpan? _lastServerTime;
- private GifInstance? _currentInstance;
+ private IGifInstance? _currentInstance;
private bool _running;
public static readonly object StopMessage = new(),
@@ -184,7 +184,7 @@ namespace Avalonia.Gif
{
_running = false;
}
- else if (message is GifInstance instance)
+ else if (message is IGifInstance instance)
{
_currentInstance?.Dispose();
_currentInstance = instance;
@@ -288,7 +288,8 @@ namespace Avalonia.Gif
private void UpdateGifInstance(object source)
{
_gifInstance?.Dispose();
- _gifInstance = new GifInstance(source);
+ _gifInstance = new WebpInstance(source);
+ // _gifInstance = new GifInstance(source);
_gifInstance.IterationCount = IterationCount;
_customVisual?.SendHandlerMessage(_gifInstance);
}
diff --git a/Avalonia.Gif/GifInstance.cs b/Avalonia.Gif/GifInstance.cs
index 30e002d1..b97badaa 100644
--- a/Avalonia.Gif/GifInstance.cs
+++ b/Avalonia.Gif/GifInstance.cs
@@ -11,7 +11,7 @@ using Avalonia.Platform;
namespace Avalonia.Gif
{
- public class GifInstance : IDisposable
+ public class GifInstance : IGifInstance
{
public IterationCount IterationCount { get; set; }
public bool AutoStart { get; private set; } = true;
diff --git a/Avalonia.Gif/IGifInstance.cs b/Avalonia.Gif/IGifInstance.cs
new file mode 100644
index 00000000..667f9163
--- /dev/null
+++ b/Avalonia.Gif/IGifInstance.cs
@@ -0,0 +1,15 @@
+using Avalonia.Animation;
+using Avalonia.Media.Imaging;
+
+namespace Avalonia.Gif;
+
+public interface IGifInstance : IDisposable
+{
+ IterationCount IterationCount { get; set; }
+ bool AutoStart { get; }
+ CancellationTokenSource CurrentCts { get; }
+ int GifFrameCount { get; }
+ PixelSize GifPixelSize { get; }
+ bool IsDisposed { get; }
+ WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed);
+}
diff --git a/Avalonia.Gif/WebpInstance.cs b/Avalonia.Gif/WebpInstance.cs
new file mode 100644
index 00000000..b9256934
--- /dev/null
+++ b/Avalonia.Gif/WebpInstance.cs
@@ -0,0 +1,180 @@
+using Avalonia.Animation;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Gif;
+
+public class WebpInstance : IGifInstance
+{
+ public IterationCount IterationCount { get; set; }
+ public bool AutoStart { get; private set; } = true;
+
+ private readonly WriteableBitmap? _targetBitmap;
+ private TimeSpan _totalTime;
+ private readonly List _frameTimes;
+ private uint _iterationCount;
+ private int _currentFrameIndex;
+
+ private SKCodec? _codec;
+
+ public CancellationTokenSource CurrentCts { get; }
+
+ internal WebpInstance(object newValue)
+ : this(
+ newValue switch
+ {
+ Stream s => s,
+ Uri u => GetStreamFromUri(u),
+ string str => GetStreamFromString(str),
+ _ => throw new InvalidDataException("Unsupported source object")
+ }
+ ) { }
+
+ public WebpInstance(string uri)
+ : this(GetStreamFromString(uri)) { }
+
+ public WebpInstance(Uri uri)
+ : this(GetStreamFromUri(uri)) { }
+
+ public WebpInstance(Stream currentStream)
+ {
+ if (!currentStream.CanSeek)
+ throw new InvalidDataException("The provided stream is not seekable.");
+
+ if (!currentStream.CanRead)
+ throw new InvalidOperationException("Can't read the stream provided.");
+
+ currentStream.Seek(0, SeekOrigin.Begin);
+
+ CurrentCts = new CancellationTokenSource();
+
+ var managedStream = new SKManagedStream(currentStream);
+ _codec = SKCodec.Create(managedStream);
+
+ var pixSize = new PixelSize(_codec.Info.Width, _codec.Info.Height);
+
+ _targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque);
+ GifPixelSize = pixSize;
+
+ _totalTime = TimeSpan.Zero;
+
+ _frameTimes = _codec
+ .FrameInfo
+ .Select(frame =>
+ {
+ _totalTime = _totalTime.Add(TimeSpan.FromMilliseconds(frame.Duration));
+ return _totalTime;
+ })
+ .ToList();
+
+ RenderFrame(_codec, _targetBitmap, 0);
+ }
+
+ private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index)
+ {
+ codec.GetFrameInfo(index, out var frameInfo);
+
+ var info = new SKImageInfo(codec.Info.Width, codec.Info.Height);
+ var decodeInfo = info.WithAlphaType(frameInfo.AlphaType);
+
+ using var frameBuffer = targetBitmap.Lock();
+
+ var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index));
+
+ if (result != SKCodecResult.Success)
+ throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}.");
+ }
+
+ private static void RenderFrame(SKCodec codec, WriteableBitmap targetBitmap, int index, int priorIndex)
+ {
+ codec.GetFrameInfo(index, out var frameInfo);
+
+ var info = new SKImageInfo(codec.Info.Width, codec.Info.Height);
+ var decodeInfo = info.WithAlphaType(frameInfo.AlphaType);
+
+ using var frameBuffer = targetBitmap.Lock();
+
+ var result = codec.GetPixels(decodeInfo, frameBuffer.Address, new SKCodecOptions(index, priorIndex));
+
+ if (result != SKCodecResult.Success)
+ throw new InvalidDataException($"Could not decode frame {index} of {codec.FrameCount}.");
+ }
+
+ private static Stream GetStreamFromString(string str)
+ {
+ if (!Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var res))
+ {
+ throw new InvalidCastException("The string provided can't be converted to URI.");
+ }
+
+ return GetStreamFromUri(res);
+ }
+
+ private static Stream GetStreamFromUri(Uri uri)
+ {
+ var uriString = uri.OriginalString.Trim();
+
+ if (!uriString.StartsWith("resm") && !uriString.StartsWith("avares"))
+ {
+ return new FileStream(uriString, FileMode.Open, FileAccess.Read);
+ }
+
+ return AssetLoader.Open(uri);
+ }
+
+ public int GifFrameCount => _frameTimes.Count;
+
+ public PixelSize GifPixelSize { get; }
+
+ public void Dispose()
+ {
+ IsDisposed = true;
+ CurrentCts.Cancel();
+ _targetBitmap?.Dispose();
+ _codec?.Dispose();
+ }
+
+ public bool IsDisposed { get; private set; }
+
+ public WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed)
+ {
+ if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value)
+ {
+ return null;
+ }
+
+ if (CurrentCts.IsCancellationRequested || _targetBitmap is null)
+ {
+ return null;
+ }
+
+ var elapsedTicks = stopwatchElapsed.Ticks;
+ var timeModulus = TimeSpan.FromTicks(elapsedTicks % _totalTime.Ticks);
+ var targetFrame = _frameTimes.FirstOrDefault(x => timeModulus < x);
+ var currentFrame = _frameTimes.IndexOf(targetFrame);
+ if (currentFrame == -1)
+ currentFrame = 0;
+
+ if (_currentFrameIndex == currentFrame)
+ return _targetBitmap;
+
+ _iterationCount = (uint)(elapsedTicks / _totalTime.Ticks);
+
+ return ProcessFrameIndex(currentFrame);
+ }
+
+ internal WriteableBitmap ProcessFrameIndex(int frameIndex)
+ {
+ if (_codec is null)
+ throw new InvalidOperationException("The codec is null.");
+
+ if (_targetBitmap is null)
+ throw new InvalidOperationException("The target bitmap is null.");
+
+ RenderFrame(_codec, _targetBitmap, frameIndex, _currentFrameIndex);
+ _currentFrameIndex = frameIndex;
+
+ return _targetBitmap;
+ }
+}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
index 51ea578b..99183592 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
@@ -409,14 +409,14 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
);
// convert to gif
- var inputStream = File.OpenRead(webpFilePath);
+ /*var inputStream = File.OpenRead(webpFilePath);
var gifFilePath = webpFilePath.ToString().Replace(".webp", ".gif");
var outputStream = File.OpenWrite(gifFilePath);
await GifConverter.ConvertAnimatedWebpToGifAsync(inputStream, outputStream);
await inputStream.DisposeAsync();
await outputStream.FlushAsync();
- await outputStream.DisposeAsync();
+ await outputStream.DisposeAsync();*/
// if (File.Exists(gifFilePath))
// {
@@ -424,8 +424,8 @@ public abstract partial class InferenceGenerationViewModelBase : InferenceTabVie
// File.Delete(webpFilePath);
// }
- outputImages.Add(new ImageSource(gifFilePath));
- EventManager.Instance.OnImageFileAdded(gifFilePath);
+ outputImages.Add(new ImageSource(webpFilePath));
+ EventManager.Instance.OnImageFileAdded(webpFilePath);
}
else
{
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
index b1e6d838..4be708a7 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
@@ -130,7 +130,7 @@ public partial class InferenceImageToVideoViewModel : InferenceGenerationViewMod
private void OnImageFileAdded(object? sender, FilePath e)
{
- if (!e.Extension.Contains("gif"))
+ if (!e.Extension.Equals(".webp", StringComparison.OrdinalIgnoreCase))
return;
OutputUri = e;
diff --git a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs
index 3f327f28..a050b6c6 100644
--- a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs
+++ b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs
@@ -104,5 +104,5 @@ public record LocalImageFile
};
}
- public static readonly HashSet SupportedImageExtensions = [".png", ".jpg", ".jpeg", ".gif"];
+ public static readonly HashSet SupportedImageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp"];
}
From 0f52bcb3a8971582499a61996b9779813871a1cb Mon Sep 17 00:00:00 2001
From: Ionite
Date: Mon, 18 Dec 2023 15:40:53 -0500
Subject: [PATCH 018/116] Fix combobox styles
---
.../ControlThemes/BetterComboBoxStyles.axaml | 82 +++++++++++++++----
.../Styles/FAComboBoxStyles.axaml | 50 ++++++-----
2 files changed, 91 insertions(+), 41 deletions(-)
diff --git a/StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml b/StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml
index 887299ca..e2e71476 100644
--- a/StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml
+++ b/StabilityMatrix.Avalonia/Styles/ControlThemes/BetterComboBoxStyles.axaml
@@ -9,8 +9,8 @@
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
+
+
-
-
-
-
-
-
-
-
diff --git a/StabilityMatrix.Avalonia/Styles/FAComboBoxStyles.axaml b/StabilityMatrix.Avalonia/Styles/FAComboBoxStyles.axaml
index d0c2007e..d0c54b78 100644
--- a/StabilityMatrix.Avalonia/Styles/FAComboBoxStyles.axaml
+++ b/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">
-
-
+
+
+
+
-
+
-
-
+ ColumnDefinitions="Auto,*"
+ ColumnSpacing="6"
+ RowSpacing="0">
-
+
-
-
+
+
-
+
-
+
-
+
From f6a1a241eb29c6cda399f195661e920d41704dc9 Mon Sep 17 00:00:00 2001
From: Ionite
Date: Tue, 19 Dec 2023 15:51:06 -0500
Subject: [PATCH 019/116] Improved Model card spacing a bit
---
.../Controls/Inference/ModelCard.axaml | 20 ++++++++++---------
1 file changed, 11 insertions(+), 9 deletions(-)
diff --git a/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
index 4e5a3ed2..725f8b69 100644
--- a/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
+++ b/StabilityMatrix.Avalonia/Controls/Inference/ModelCard.axaml
@@ -8,6 +8,7 @@
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">
@@ -22,11 +23,14 @@
-
+
@@ -34,7 +38,7 @@
+ Margin="0,0,0,0">
@@ -60,7 +64,6 @@
-
+
From ee66f0dc492bb3242ef9ad7e15b55ebd30234cf4 Mon Sep 17 00:00:00 2001
From: Ionite
Date: Tue, 19 Dec 2023 15:51:35 -0500
Subject: [PATCH 020/116] Made default markdown dialog size larger
---
StabilityMatrix.Avalonia/DialogHelper.cs | 47 ++++++++----------------
1 file changed, 15 insertions(+), 32 deletions(-)
diff --git a/StabilityMatrix.Avalonia/DialogHelper.cs b/StabilityMatrix.Avalonia/DialogHelper.cs
index 0cdfeabb..7ad27bcb 100644
--- a/StabilityMatrix.Avalonia/DialogHelper.cs
+++ b/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 textFields
)
{
- return CreateTextEntryDialog(
- title,
- new MarkdownScrollViewer { Markdown = description },
- textFields
- );
+ return CreateTextEntryDialog(title, new MarkdownScrollViewer { Markdown = description }, textFields);
}
///
@@ -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()
+ var firstTextBox = stackPanel
+ .Children.OfType()
.FirstOrDefault()
.FindDescendantOfType();
firstTextBox!.Focus();
@@ -254,16 +242,16 @@ public static class DialogHelper
Content = viewer,
CloseButtonText = Resources.Action_Close,
IsPrimaryButtonEnabled = false,
+ MinDialogWidth = 800,
+ MaxDialogHeight = 1000,
+ MaxDialogWidth = 1000
};
}
///
/// Create a dialog for displaying an ApiException
///
- 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;
From d533be332995933007f1ec9e268634077d3c40a1 Mon Sep 17 00:00:00 2001
From: JT
Date: Sat, 23 Dec 2023 17:53:37 -0800
Subject: [PATCH 021/116] handle window size/position outside of shutdown, load
pngs faster, start of saving some img2vid stuff, webp metadata reading, and
different settings write method (that still doesn't entirely work and
occasionally adds extra right curly brackets to the end of the file)
---
StabilityMatrix.Avalonia/App.axaml.cs | 224 ++++++++----------
.../FallbackRamCachedWebImageLoader.cs | 18 ++
.../Video/SvdImgToVidConditioningViewModel.cs | 11 +-
.../ViewModels/MainWindowViewModel.cs | 11 +-
.../Settings/MainSettingsViewModel.cs | 5 +
.../Views/MainWindow.axaml.cs | 124 +++++++---
StabilityMatrix.Core/Helper/ImageMetadata.cs | 122 +++++++++-
StabilityMatrix.Core/Helper/MyTiffFile.cs | 6 +
StabilityMatrix.Core/Helper/Utilities.cs | 23 +-
.../Services/SettingsManager.cs | 14 +-
.../StabilityMatrix.Core.csproj | 1 +
11 files changed, 383 insertions(+), 176 deletions(-)
create mode 100644 StabilityMatrix.Core/Helper/MyTiffFile.cs
diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs
index 742b10a3..08e8ba51 100644
--- a/StabilityMatrix.Avalonia/App.axaml.cs
+++ b/StabilityMatrix.Avalonia/App.axaml.cs
@@ -80,7 +80,8 @@ public sealed class App : Application
public static TopLevel TopLevel => TopLevel.GetTopLevel(VisualRoot)!;
- internal static bool IsHeadlessMode => TopLevel.TryGetPlatformHandle()?.HandleDescriptor is null or "STUB";
+ internal static bool IsHeadlessMode =>
+ TopLevel.TryGetPlatformHandle()?.HandleDescriptor is null or "STUB";
[NotNull]
public static IStorageProvider? StorageProvider { get; internal set; }
@@ -117,8 +118,7 @@ public sealed class App : Application
{
// Remove DataAnnotations validation plugin since we're using INotifyDataErrorInfo from MvvmToolkit
var dataValidationPluginsToRemove = BindingPlugins
- .DataValidators
- .OfType()
+ .DataValidators.OfType()
.ToArray();
foreach (var plugin in dataValidationPluginsToRemove)
@@ -161,22 +161,19 @@ public sealed class App : Application
DesktopLifetime.MainWindow = setupWindow;
- setupWindow
- .ShowAsyncCts
- .Token
- .Register(() =>
+ setupWindow.ShowAsyncCts.Token.Register(() =>
+ {
+ if (setupWindow.Result == ContentDialogResult.Primary)
+ {
+ settingsManager.SetEulaAccepted();
+ ShowMainWindow();
+ DesktopLifetime.MainWindow.Show();
+ }
+ else
{
- if (setupWindow.Result == ContentDialogResult.Primary)
- {
- settingsManager.SetEulaAccepted();
- ShowMainWindow();
- DesktopLifetime.MainWindow.Show();
- }
- else
- {
- Shutdown();
- }
- });
+ Shutdown();
+ }
+ });
}
else
{
@@ -297,7 +294,9 @@ public sealed class App : Application
var serviceManager = new ServiceManager();
var serviceManagedTypes = exportedTypes
- .Select(t => new { t, attributes = t.GetCustomAttributes(typeof(ManagedServiceAttribute), true) })
+ .Select(
+ t => new { t, attributes = t.GetCustomAttributes(typeof(ManagedServiceAttribute), true) }
+ )
.Where(t1 => t1.attributes is { Length: > 0 })
.Select(t1 => t1.t)
.ToList();
@@ -322,8 +321,7 @@ public sealed class App : Application
services.AddMessagePipeNamedPipeInterprocess("StabilityMatrix");
var exportedTypes = AppDomain
- .CurrentDomain
- .GetAssemblies()
+ .CurrentDomain.GetAssemblies()
.Where(a => a.FullName?.StartsWith("StabilityMatrix") == true)
.SelectMany(a => a.GetExportedTypes())
.ToArray();
@@ -332,7 +330,8 @@ public sealed class App : Application
.Select(t => new { t, attributes = t.GetCustomAttributes(typeof(TransientAttribute), false) })
.Where(
t1 =>
- t1.attributes is { Length: > 0 } && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
+ t1.attributes is { Length: > 0 }
+ && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
)
.Select(t1 => new { Type = t1.t, Attribute = (TransientAttribute)t1.attributes[0] });
@@ -352,9 +351,12 @@ public sealed class App : Application
.Select(t => new { t, attributes = t.GetCustomAttributes(typeof(SingletonAttribute), false) })
.Where(
t1 =>
- t1.attributes is { Length: > 0 } && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
+ t1.attributes is { Length: > 0 }
+ && !t1.t.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)
)
- .Select(t1 => new { Type = t1.t, Attributes = t1.attributes.Cast().ToArray() });
+ .Select(
+ t1 => new { Type = t1.t, Attributes = t1.attributes.Cast().ToArray() }
+ );
foreach (var typePair in singletonTypes)
{
@@ -386,7 +388,9 @@ public sealed class App : Application
// Rich presence
services.AddSingleton();
- services.AddSingleton(provider => provider.GetRequiredService());
+ services.AddSingleton(
+ provider => provider.GetRequiredService()
+ );
Config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
@@ -557,7 +561,11 @@ public sealed class App : Application
#if DEBUG
builder.AddNLog(
ConfigureLogging(),
- new NLogProviderOptions { IgnoreEmptyEventId = false, CaptureEventId = EventIdCaptureType.Legacy }
+ new NLogProviderOptions
+ {
+ IgnoreEmptyEventId = false,
+ CaptureEventId = EventIdCaptureType.Legacy
+ }
);
#else
builder.AddNLog(ConfigureLogging());
@@ -591,8 +599,6 @@ public sealed class App : Application
if (e.Cancel)
return;
- var mainWindow = (MainWindow)sender!;
-
// Show confirmation if package running
var launchPageViewModel = Services.GetRequiredService();
launchPageViewModel.OnMainWindowClosing(e);
@@ -602,66 +608,38 @@ public sealed class App : Application
// Check if we need to dispose IAsyncDisposables
if (
- !isAsyncDisposeComplete
- && Services.GetServices().ToList() is { Count: > 0 } asyncDisposables
+ isAsyncDisposeComplete
+ || Services.GetServices().ToList() is not { Count: > 0 } asyncDisposables
)
- {
- // Cancel shutdown for now
- e.Cancel = true;
- isAsyncDisposeComplete = true;
+ return;
- Debug.WriteLine("OnShutdownRequested Canceled: Disposing IAsyncDisposables");
+ // Cancel shutdown for now
+ e.Cancel = true;
+ isAsyncDisposeComplete = true;
- Task.Run(async () =>
+ Debug.WriteLine("OnShutdownRequested Canceled: Disposing IAsyncDisposables");
+
+ Task.Run(async () =>
+ {
+ foreach (var disposable in asyncDisposables)
{
- foreach (var disposable in asyncDisposables)
+ Debug.WriteLine($"Disposing IAsyncDisposable ({disposable.GetType().Name})");
+ try
{
- Debug.WriteLine($"Disposing IAsyncDisposable ({disposable.GetType().Name})");
- try
- {
- await disposable.DisposeAsync().ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Debug.Fail(ex.ToString());
- }
+ await disposable.DisposeAsync().ConfigureAwait(false);
}
- })
- .ContinueWith(_ =>
- {
- // Shutdown again
- Dispatcher.UIThread.Invoke(() => Shutdown());
- })
- .SafeFireAndForget();
-
- return;
- }
-
- OnMainWindowClosingTerminal(mainWindow);
- }
-
- ///
- /// Called at the end of before the main window is closed.
- ///
- private static void OnMainWindowClosingTerminal(Window sender)
- {
- var settingsManager = Services.GetRequiredService();
-
- // Save window position
- var validWindowPosition = sender.Screens.All.Any(screen => screen.Bounds.Contains(sender.Position));
-
- settingsManager.Transaction(
- s =>
+ catch (Exception ex)
+ {
+ Debug.Fail(ex.ToString());
+ }
+ }
+ })
+ .ContinueWith(_ =>
{
- s.WindowSettings = new WindowSettings(
- sender.Width,
- sender.Height,
- validWindowPosition ? sender.Position.X : 0,
- validWindowPosition ? sender.Position.Y : 0
- );
- },
- ignoreMissingLibraryDir: true
- );
+ // Shutdown again
+ Dispatcher.UIThread.Invoke(() => Shutdown());
+ })
+ .SafeFireAndForget();
}
private static void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs args)
@@ -714,10 +692,12 @@ public sealed class App : Application
.WriteTo(
new FileTarget
{
- Layout = "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}",
+ Layout =
+ "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}",
ArchiveOldFileOnStartup = true,
FileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log",
- ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log",
+ ArchiveFileName =
+ "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log",
ArchiveNumbering = ArchiveNumberingMode.Rolling,
MaxArchiveFiles = 2
}
@@ -730,7 +710,9 @@ public sealed class App : Application
builder.ForLogger("Microsoft.Extensions.Http.*").WriteToNil(NLog.LogLevel.Warn);
// Disable console trace logging by default
- builder.ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel").WriteToNil(NLog.LogLevel.Debug);
+ builder
+ .ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel")
+ .WriteToNil(NLog.LogLevel.Debug);
// Disable LoadableViewModelBase trace logging by default
builder
@@ -751,20 +733,18 @@ public sealed class App : Application
// Sentry
if (SentrySdk.IsEnabled)
{
- LogManager
- .Configuration
- .AddSentry(o =>
- {
- o.InitializeSdk = false;
- o.Layout = "${message}";
- o.ShutdownTimeoutSeconds = 5;
- o.IncludeEventDataOnBreadcrumbs = true;
- o.BreadcrumbLayout = "${logger}: ${message}";
- // Debug and higher are stored as breadcrumbs (default is Info)
- o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug;
- // Error and higher is sent as event (default is Error)
- o.MinimumEventLevel = NLog.LogLevel.Error;
- });
+ LogManager.Configuration.AddSentry(o =>
+ {
+ o.InitializeSdk = false;
+ o.Layout = "${message}";
+ o.ShutdownTimeoutSeconds = 5;
+ o.IncludeEventDataOnBreadcrumbs = true;
+ o.BreadcrumbLayout = "${logger}: ${message}";
+ // Debug and higher are stored as breadcrumbs (default is Info)
+ o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug;
+ // Error and higher is sent as event (default is Error)
+ o.MinimumEventLevel = NLog.LogLevel.Error;
+ });
}
LogManager.ReconfigExistingLoggers();
@@ -803,34 +783,36 @@ public sealed class App : Application
results.Add(ms);
}
- Dispatcher
- .UIThread
- .InvokeAsync(async () =>
- {
- var dest = await StorageProvider.SaveFilePickerAsync(
- new FilePickerSaveOptions() { SuggestedFileName = "screenshot.png", ShowOverwritePrompt = true }
- );
+ Dispatcher.UIThread.InvokeAsync(async () =>
+ {
+ var dest = await StorageProvider.SaveFilePickerAsync(
+ new FilePickerSaveOptions()
+ {
+ SuggestedFileName = "screenshot.png",
+ ShowOverwritePrompt = true
+ }
+ );
- if (dest?.TryGetLocalPath() is { } localPath)
+ if (dest?.TryGetLocalPath() is { } localPath)
+ {
+ var localFile = new FilePath(localPath);
+ foreach (var (i, stream) in results.Enumerate())
{
- var localFile = new FilePath(localPath);
- foreach (var (i, stream) in results.Enumerate())
+ var name = localFile.NameWithoutExtension;
+ if (results.Count > 1)
{
- var name = localFile.NameWithoutExtension;
- if (results.Count > 1)
- {
- name += $"_{i + 1}";
- }
-
- localFile = localFile.Directory!.JoinFile(name + ".png");
- localFile.Create();
-
- await using var fileStream = localFile.Info.OpenWrite();
- stream.Seek(0, SeekOrigin.Begin);
- await stream.CopyToAsync(fileStream);
+ name += $"_{i + 1}";
}
+
+ localFile = localFile.Directory!.JoinFile(name + ".png");
+ localFile.Create();
+
+ await using var fileStream = localFile.Info.OpenWrite();
+ stream.Seek(0, SeekOrigin.Begin);
+ await stream.CopyToAsync(fileStream);
}
- });
+ }
+ });
}
[Conditional("DEBUG")]
diff --git a/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs b/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
index f8ae8235..7dc26555 100644
--- a/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
+++ b/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
@@ -1,12 +1,15 @@
using System;
using System.Collections.Concurrent;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader.Loaders;
using Avalonia.Media.Imaging;
using StabilityMatrix.Core.Extensions;
+using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Avalonia;
@@ -42,6 +45,11 @@ public class FallbackRamCachedWebImageLoader : RamCachedWebImageLoader
{
try
{
+ if (url.EndsWith("png", StringComparison.OrdinalIgnoreCase))
+ {
+ return await LoadPngAsync(url);
+ }
+
return new Bitmap(url);
}
catch (Exception e)
@@ -98,4 +106,14 @@ public class FallbackRamCachedWebImageLoader : RamCachedWebImageLoader
}
}
}
+
+ private async Task LoadPngAsync(string url)
+ {
+ using var fileStream = new BinaryReader(File.OpenRead(url));
+ var imageBytes = ImageMetadata.BuildImageWithoutMetadata(fileStream).ToArray();
+ using var memoryStream = new MemoryStream();
+ await memoryStream.WriteAsync(imageBytes, 0, imageBytes.Length);
+ memoryStream.Position = 0;
+ return new Bitmap(memoryStream);
+ }
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs
index 4932ad55..ae4cbcc5 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs
@@ -41,13 +41,15 @@ public partial class SvdImgToVidConditioningViewModel
public void LoadStateFromParameters(GenerationParameters parameters)
{
- // TODO
+ Width = parameters.Width;
+ Height = parameters.Height;
+ // TODO: add more metadata
}
public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
{
- // TODO
- return parameters;
+ // TODO: add more metadata
+ return parameters with { Width = Width, Height = Height, };
}
public void ApplyStep(ModuleApplyStepEventArgs e)
@@ -58,8 +60,7 @@ public partial class SvdImgToVidConditioningViewModel
{
Name = e.Nodes.GetUniqueName("LinearCfgGuidance"),
Model =
- e.Builder.Connections.BaseModel
- ?? throw new ValidationException("Model not selected"),
+ e.Builder.Connections.BaseModel ?? throw new ValidationException("Model not selected"),
MinCfg = MinCfg
}
);
diff --git a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
index 842df99e..c546d1ee 100644
--- a/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Reactive.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
@@ -10,6 +11,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
+using KGySoft.CoreLibraries;
using NLog;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
@@ -112,10 +114,7 @@ public partial class MainWindowViewModel : ViewModelBase
var startupTime = CodeTimer.FormatTime(Program.StartupTimer.Elapsed);
Logger.Info($"App started ({startupTime})");
- if (
- Program.Args.DebugOneClickInstall
- || settingsManager.Settings.InstalledPackages.Count == 0
- )
+ if (Program.Args.DebugOneClickInstall || settingsManager.Settings.InstalledPackages.Count == 0)
{
var viewModel = dialogFactory.Get();
var dialog = new BetterContentDialog
@@ -148,8 +147,8 @@ public partial class MainWindowViewModel : ViewModelBase
.Where(p => p.GetType().GetCustomAttributes(typeof(PreloadAttribute), true).Any())
)
{
- Dispatcher.UIThread
- .InvokeAsync(
+ Dispatcher
+ .UIThread.InvokeAsync(
async () =>
{
var stopwatch = Stopwatch.StartNew();
diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
index d1e25903..36d97593 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
@@ -24,6 +24,7 @@ using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData.Binding;
+using ExifLibrary;
using FluentAvalonia.UI.Controls;
using NLog;
using SkiaSharp;
@@ -708,6 +709,10 @@ public partial class MainSettingsViewModel : PageViewModelBase
if (files.Count == 0)
return;
+ var data = await ImageMetadata.ReadTextChunkFromWebp(files[0].TryGetLocalPath(), ExifTag.Model);
+
+ return;
+
var metadata = ImageMetadata.ParseFile(files[0].TryGetLocalPath()!);
var textualTags = metadata.GetTextualData()?.ToArray();
diff --git a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
index c1263426..bd62a302 100644
--- a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
@@ -4,6 +4,9 @@ using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Threading;
using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls;
@@ -34,8 +37,11 @@ using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
+using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Processes;
+using StabilityMatrix.Core.Services;
+using TeachingTip = FluentAvalonia.UI.Controls.TeachingTip;
#if DEBUG
using StabilityMatrix.Avalonia.Diagnostics.Views;
#endif
@@ -48,6 +54,7 @@ public partial class MainWindow : AppWindowBase
{
private readonly INotificationService notificationService;
private readonly INavigationService navigationService;
+ private readonly ISettingsManager settingsManager;
private FlyoutBase? progressFlyout;
@@ -61,11 +68,13 @@ public partial class MainWindow : AppWindowBase
public MainWindow(
INotificationService notificationService,
- INavigationService navigationService
+ INavigationService navigationService,
+ ISettingsManager settingsManager
)
{
this.notificationService = notificationService;
this.navigationService = navigationService;
+ this.settingsManager = settingsManager;
InitializeComponent();
@@ -82,6 +91,47 @@ public partial class MainWindow : AppWindowBase
EventManager.Instance.ToggleProgressFlyout += (_, _) => progressFlyout?.Hide();
EventManager.Instance.CultureChanged += (_, _) => SetDefaultFonts();
EventManager.Instance.UpdateAvailable += OnUpdateAvailable;
+
+ Observable
+ .FromEventPattern(this, nameof(SizeChanged))
+ .Where(x => x.EventArgs.PreviousSize != x.EventArgs.NewSize)
+ .Throttle(TimeSpan.FromMilliseconds(100))
+ .Select(x => x.EventArgs.NewSize)
+ .ObserveOn(SynchronizationContext.Current)
+ .Subscribe(newSize =>
+ {
+ var validWindowPosition = Screens.All.Any(screen => screen.Bounds.Contains(Position));
+
+ settingsManager.Transaction(
+ s =>
+ {
+ s.WindowSettings = new WindowSettings(
+ newSize.Width,
+ newSize.Height,
+ validWindowPosition ? Position.X : 0,
+ validWindowPosition ? Position.Y : 0
+ );
+ },
+ ignoreMissingLibraryDir: true
+ );
+ });
+
+ Observable
+ .FromEventPattern(this, nameof(PositionChanged))
+ .Where(x => Screens.All.Any(screen => screen.Bounds.Contains(x.EventArgs.Point)))
+ .Throttle(TimeSpan.FromMilliseconds(100))
+ .Select(x => x.EventArgs.Point)
+ .ObserveOn(SynchronizationContext.Current)
+ .Subscribe(position =>
+ {
+ settingsManager.Transaction(
+ s =>
+ {
+ s.WindowSettings = new WindowSettings(Width, Height, position.X, position.Y);
+ },
+ ignoreMissingLibraryDir: true
+ );
+ });
}
///
@@ -132,15 +182,16 @@ public partial class MainWindow : AppWindowBase
return;
// Navigate to first page
- Dispatcher
- .UIThread
- .Post(
- () =>
- navigationService.NavigateTo(
- vm.Pages[0],
- new BetterSlideNavigationTransition { Effect = SlideNavigationTransitionEffect.FromBottom }
- )
- );
+ Dispatcher.UIThread.Post(
+ () =>
+ navigationService.NavigateTo(
+ vm.Pages[0],
+ new BetterSlideNavigationTransition
+ {
+ Effect = SlideNavigationTransitionEffect.FromBottom
+ }
+ )
+ );
// Check show update teaching tip
if (vm.UpdateViewModel.IsUpdateAvailable)
@@ -165,27 +216,24 @@ public partial class MainWindow : AppWindowBase
var mainViewModel = (MainWindowViewModel)DataContext!;
mainViewModel.SelectedCategory = mainViewModel
- .Pages
- .Concat(mainViewModel.FooterPages)
+ .Pages.Concat(mainViewModel.FooterPages)
.FirstOrDefault(x => x.GetType() == e.ViewModelType);
}
private void OnUpdateAvailable(object? sender, UpdateInfo? updateInfo)
{
- Dispatcher
- .UIThread
- .Post(() =>
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (DataContext is MainWindowViewModel vm && vm.ShouldShowUpdateAvailableTeachingTip(updateInfo))
{
- if (DataContext is MainWindowViewModel vm && vm.ShouldShowUpdateAvailableTeachingTip(updateInfo))
- {
- var target = this.FindControl("FooterUpdateItem")!;
- var tip = this.FindControl("UpdateAvailableTeachingTip")!;
+ var target = this.FindControl("FooterUpdateItem")!;
+ var tip = this.FindControl("UpdateAvailableTeachingTip")!;
- tip.Target = target;
- tip.Subtitle = $"{Compat.AppVersion.ToDisplayString()} -> {updateInfo.Version}";
- tip.IsOpen = true;
- }
- });
+ tip.Target = target;
+ tip.Subtitle = $"{Compat.AppVersion.ToDisplayString()} -> {updateInfo.Version}";
+ tip.IsOpen = true;
+ }
+ });
}
public void SetDefaultFonts()
@@ -282,18 +330,16 @@ public partial class MainWindow : AppWindowBase
private void OnImageLoadFailed(object? sender, ImageLoadFailedEventArgs e)
{
- Dispatcher
- .UIThread
- .Post(() =>
- {
- var fileName = Path.GetFileName(e.Url);
- var displayName = string.IsNullOrEmpty(fileName) ? e.Url : fileName;
- notificationService.ShowPersistent(
- "Failed to load image",
- $"Could not load '{displayName}'\n({e.Exception.Message})",
- NotificationType.Warning
- );
- });
+ Dispatcher.UIThread.Post(() =>
+ {
+ var fileName = Path.GetFileName(e.Url);
+ var displayName = string.IsNullOrEmpty(fileName) ? e.Url : fileName;
+ notificationService.ShowPersistent(
+ "Failed to load image",
+ $"Could not load '{displayName}'\n({e.Exception.Message})",
+ NotificationType.Warning
+ );
+ });
}
private void TryEnableMicaEffect()
@@ -319,7 +365,11 @@ public partial class MainWindow : AppWindowBase
else if (ActualThemeVariant == ThemeVariant.Light)
{
// Similar effect here
- var color = this.TryFindResource("SolidBackgroundFillColorBase", ThemeVariant.Light, out var value)
+ var color = this.TryFindResource(
+ "SolidBackgroundFillColorBase",
+ ThemeVariant.Light,
+ out var value
+ )
? (Color2)(Color)value!
: new Color2(243, 243, 243);
diff --git a/StabilityMatrix.Core/Helper/ImageMetadata.cs b/StabilityMatrix.Core/Helper/ImageMetadata.cs
index a10dae53..aca320e7 100644
--- a/StabilityMatrix.Core/Helper/ImageMetadata.cs
+++ b/StabilityMatrix.Core/Helper/ImageMetadata.cs
@@ -1,7 +1,10 @@
-using System.Text;
+using System.Diagnostics;
+using System.Text;
using System.Text.Json;
+using ExifLibrary;
using MetadataExtractor;
using MetadataExtractor.Formats.Png;
+using Microsoft.VisualBasic;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
@@ -13,10 +16,13 @@ public class ImageMetadata
{
private IReadOnlyList? Directories { get; set; }
- private static readonly byte[] PngHeader = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
+ private static readonly byte[] PngHeader = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
private static readonly byte[] Idat = "IDAT"u8.ToArray();
private static readonly byte[] Text = "tEXt"u8.ToArray();
+ private static readonly byte[] Riff = "RIFF"u8.ToArray();
+ private static readonly byte[] Webp = "WEBP"u8.ToArray();
+
public static ImageMetadata ParseFile(FilePath path)
{
return new ImageMetadata { Directories = ImageMetadataReader.ReadMetadata(path) };
@@ -179,4 +185,116 @@ public class ImageMetadata
return string.Empty;
}
+
+ public static IEnumerable BuildImageWithoutMetadata(BinaryReader byteStream)
+ {
+ var bytes = new List();
+ byteStream.BaseStream.Position = 0;
+
+ // Read first 8 bytes and make sure they match the png header
+ if (!byteStream.ReadBytes(8).SequenceEqual(PngHeader))
+ {
+ return Array.Empty();
+ }
+ bytes.AddRange(PngHeader);
+
+ var ihdrStuff = byteStream.ReadBytes(25);
+ bytes.AddRange(ihdrStuff);
+
+ while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4)
+ {
+ var chunkSizeBytes = byteStream.ReadBytes(4);
+ var chunkSize = BitConverter.ToInt32(chunkSizeBytes.Reverse().ToArray());
+ var chunkTypeBytes = byteStream.ReadBytes(4);
+ var chunkType = Encoding.UTF8.GetString(chunkTypeBytes);
+
+ if (chunkType != Encoding.UTF8.GetString(Idat))
+ {
+ // skip chunk data
+ byteStream.BaseStream.Position += chunkSize;
+ // skip crc
+ byteStream.BaseStream.Position += 4;
+ continue;
+ }
+
+ bytes.AddRange(chunkSizeBytes);
+ bytes.AddRange(chunkTypeBytes);
+ var idatBytes = byteStream.ReadBytes(chunkSize);
+ bytes.AddRange(idatBytes);
+ var crcBytes = byteStream.ReadBytes(4);
+ bytes.AddRange(crcBytes);
+ }
+
+ // Add IEND chunk
+ bytes.AddRange([0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82]);
+
+ return bytes;
+ }
+
+ public static async Task ReadTextChunkFromWebp(FilePath filePath, ExifTag exifTag)
+ {
+ var sw = Stopwatch.StartNew();
+ try
+ {
+ await using var memoryStream = Utilities.GetMemoryStreamFromFile(filePath);
+ if (memoryStream is null)
+ return string.Empty;
+
+ var exifChunks = GetExifChunks(memoryStream);
+ if (exifChunks.Length == 0)
+ return string.Empty;
+
+ // write exifChunks to new memoryStream but skip first 6 bytes
+ using var newMemoryStream = new MemoryStream(exifChunks[6..]);
+ newMemoryStream.Seek(0, SeekOrigin.Begin);
+
+ var img = new MyTiffFile(newMemoryStream, Encoding.UTF8);
+ return img.Properties[exifTag]?.Value?.ToString() ?? string.Empty;
+ }
+ finally
+ {
+ sw.Stop();
+ Console.WriteLine($"ReadTextChunkFromWebp took {sw.ElapsedMilliseconds}ms");
+ }
+ }
+
+ private static byte[] GetExifChunks(MemoryStream memoryStream)
+ {
+ using var byteStream = new BinaryReader(memoryStream);
+ byteStream.BaseStream.Position = 0;
+
+ // Read first 8 bytes and make sure they match the RIFF header
+ if (!byteStream.ReadBytes(4).SequenceEqual(Riff))
+ {
+ return Array.Empty();
+ }
+
+ // skip 4 bytes then read next 4 for webp header
+ byteStream.BaseStream.Position += 4;
+ if (!byteStream.ReadBytes(4).SequenceEqual(Webp))
+ {
+ return Array.Empty();
+ }
+
+ while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4)
+ {
+ var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4));
+ var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).ToArray());
+
+ if (chunkType != "EXIF")
+ {
+ // skip chunk data
+ byteStream.BaseStream.Position += chunkSize;
+ continue;
+ }
+
+ var exifStart = byteStream.BaseStream.Position;
+ var exifBytes = byteStream.ReadBytes(chunkSize);
+ var exif = Encoding.UTF8.GetString(exifBytes);
+ Debug.WriteLine($"Found exif chunk of size {chunkSize}");
+ return exifBytes;
+ }
+
+ return Array.Empty();
+ }
}
diff --git a/StabilityMatrix.Core/Helper/MyTiffFile.cs b/StabilityMatrix.Core/Helper/MyTiffFile.cs
new file mode 100644
index 00000000..dd65110b
--- /dev/null
+++ b/StabilityMatrix.Core/Helper/MyTiffFile.cs
@@ -0,0 +1,6 @@
+using System.Text;
+using ExifLibrary;
+
+namespace StabilityMatrix.Core.Helper;
+
+public class MyTiffFile(MemoryStream stream, Encoding encoding) : TIFFFile(stream, encoding);
diff --git a/StabilityMatrix.Core/Helper/Utilities.cs b/StabilityMatrix.Core/Helper/Utilities.cs
index 07afca73..5e8bfc2e 100644
--- a/StabilityMatrix.Core/Helper/Utilities.cs
+++ b/StabilityMatrix.Core/Helper/Utilities.cs
@@ -13,8 +13,12 @@ public static class Utilities
: $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
- public static void CopyDirectory(string sourceDir, string destinationDir, bool recursive,
- bool includeReparsePoints = false)
+ public static void CopyDirectory(
+ string sourceDir,
+ string destinationDir,
+ bool recursive,
+ bool includeReparsePoints = false
+ )
{
// Get information about the source directory
var dir = new DirectoryInfo(sourceDir);
@@ -35,11 +39,13 @@ public static class Utilities
foreach (var file in dir.GetFiles())
{
var targetFilePath = Path.Combine(destinationDir, file.Name);
- if (file.FullName == targetFilePath) continue;
+ if (file.FullName == targetFilePath)
+ continue;
file.CopyTo(targetFilePath, true);
}
- if (!recursive) return;
+ if (!recursive)
+ return;
// If recursive and copying subdirectories, recursively call this method
foreach (var subDir in dirs)
@@ -48,4 +54,13 @@ public static class Utilities
CopyDirectory(subDir.FullName, newDestinationDir, true);
}
}
+
+ public static MemoryStream? GetMemoryStreamFromFile(string filePath)
+ {
+ var fileBytes = File.ReadAllBytes(filePath);
+ var stream = new MemoryStream(fileBytes);
+ stream.Position = 0;
+
+ return stream;
+ }
}
diff --git a/StabilityMatrix.Core/Services/SettingsManager.cs b/StabilityMatrix.Core/Services/SettingsManager.cs
index 0c0c56fa..2b0849c3 100644
--- a/StabilityMatrix.Core/Services/SettingsManager.cs
+++ b/StabilityMatrix.Core/Services/SettingsManager.cs
@@ -627,6 +627,7 @@ public class SettingsManager : ISettingsManager
if (fileStream.Length == 0)
{
Logger.Warn("Settings file is empty, using default settings");
+ isLoaded = true;
return;
}
@@ -674,7 +675,18 @@ public class SettingsManager : ISettingsManager
SettingsSerializerContext.Default.Settings
);
- File.WriteAllBytes(SettingsPath, jsonBytes);
+ if (jsonBytes.Length == 0)
+ {
+ Logger.Error("JsonSerializer returned empty bytes for some reason");
+ return;
+ }
+
+ using var fs = File.Open(SettingsPath, FileMode.Open);
+ if (fs.CanWrite)
+ {
+ fs.Write(jsonBytes, 0, jsonBytes.Length);
+ }
+ fs.Close();
}
finally
{
diff --git a/StabilityMatrix.Core/StabilityMatrix.Core.csproj b/StabilityMatrix.Core/StabilityMatrix.Core.csproj
index d64996a3..e2304c75 100644
--- a/StabilityMatrix.Core/StabilityMatrix.Core.csproj
+++ b/StabilityMatrix.Core/StabilityMatrix.Core.csproj
@@ -27,6 +27,7 @@
+
From 1b06732c92c64d478376030b6fa725664401cf59 Mon Sep 17 00:00:00 2001
From: JT
Date: Sat, 23 Dec 2023 18:04:20 -0800
Subject: [PATCH 022/116] Fix merge/build errors
---
.../ViewModels/Inference/ModelCardViewModel.cs | 7 +++++--
.../Inference/Video/ImgToVidModelCardViewModel.cs | 8 +++-----
.../Video/SvdImgToVidConditioningViewModel.cs | 13 ++++++++-----
.../Video/VideoOutputSettingsCardViewModel.cs | 4 ++--
4 files changed, 18 insertions(+), 14 deletions(-)
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
index d68302c0..1fed6926 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/ModelCardViewModel.cs
@@ -10,8 +10,8 @@ using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models;
-using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
+using StabilityMatrix.Core.Models.Api.Comfy.NodeTypes;
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
@@ -41,6 +41,7 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
[ObservableProperty]
private bool disableSettings;
+ [ObservableProperty]
private bool isClipSkipEnabled;
[NotifyDataErrorInfo]
@@ -91,7 +92,9 @@ public partial class ModelCardViewModel(IInferenceClientManager clientManager)
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")
}
);
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/ImgToVidModelCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/ImgToVidModelCardViewModel.cs
index c5513e6e..2bbf8021 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/ImgToVidModelCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/ImgToVidModelCardViewModel.cs
@@ -24,14 +24,12 @@ public class ImgToVidModelCardViewModel : ModelCardViewModel
new ComfyNodeBuilder.ImageOnlyCheckpointLoader
{
Name = "ImageOnlyCheckpointLoader",
- CkptName =
- SelectedModel?.RelativePath
- ?? throw new ValidationException("Model not selected")
+ CkptName = SelectedModel?.RelativePath ?? throw new ValidationException("Model not selected")
}
);
- e.Builder.Connections.BaseModel = imgToVidLoader.Output1;
+ e.Builder.Connections.Base.Model = imgToVidLoader.Output1;
e.Builder.Connections.BaseClipVision = imgToVidLoader.Output2;
- e.Builder.Connections.BaseVAE = imgToVidLoader.Output3;
+ e.Builder.Connections.Base.VAE = imgToVidLoader.Output3;
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs
index ae4cbcc5..9ff667e6 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs
@@ -7,6 +7,7 @@ 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.Video;
@@ -60,12 +61,12 @@ public partial class SvdImgToVidConditioningViewModel
{
Name = e.Nodes.GetUniqueName("LinearCfgGuidance"),
Model =
- e.Builder.Connections.BaseModel ?? throw new ValidationException("Model not selected"),
+ e.Builder.Connections.Base.Model ?? throw new ValidationException("Model not selected"),
MinCfg = MinCfg
}
);
- e.Builder.Connections.BaseModel = cfgGuidanceNode.Output;
+ e.Builder.Connections.Base.Model = cfgGuidanceNode.Output;
// then do the SVD stuff
var svdImgToVidConditioningNode = e.Nodes.AddTypedNode(
@@ -73,7 +74,7 @@ public partial class SvdImgToVidConditioningViewModel
{
ClipVision = e.Builder.Connections.BaseClipVision!,
InitImage = e.Builder.GetPrimaryAsImage(),
- Vae = e.Builder.Connections.BaseVAE!,
+ Vae = e.Builder.Connections.Base.VAE!,
Name = e.Nodes.GetUniqueName("SvdImgToVidConditioning"),
Width = Width,
Height = Height,
@@ -84,8 +85,10 @@ public partial class SvdImgToVidConditioningViewModel
}
);
- e.Builder.Connections.BaseConditioning = svdImgToVidConditioningNode.Output1;
- e.Builder.Connections.BaseNegativeConditioning = svdImgToVidConditioningNode.Output2;
+ e.Builder.Connections.Base.Conditioning = new ConditioningConnections(
+ svdImgToVidConditioningNode.Output1,
+ svdImgToVidConditioningNode.Output2
+ );
e.Builder.Connections.Primary = svdImgToVidConditioningNode.Output3;
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/VideoOutputSettingsCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/VideoOutputSettingsCardViewModel.cs
index 3fd77079..c0cdea9b 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/VideoOutputSettingsCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/VideoOutputSettingsCardViewModel.cs
@@ -55,8 +55,8 @@ public partial class VideoOutputSettingsCardViewModel
_ =>
e.Builder.GetPrimaryAsImage(
e.Builder.Connections.PrimaryVAE
- ?? e.Builder.Connections.RefinerVAE
- ?? e.Builder.Connections.BaseVAE
+ ?? e.Builder.Connections.Refiner.VAE
+ ?? e.Builder.Connections.Base.VAE
?? throw new ArgumentException("No Primary, Refiner, or Base VAE")
),
image => image
From 9e28cb6965ecf872e9764cc5c354b4845abe35fb Mon Sep 17 00:00:00 2001
From: JT
Date: Mon, 25 Dec 2023 02:04:17 -0800
Subject: [PATCH 023/116] save metadata
---
.../FallbackRamCachedWebImageLoader.cs | 2 +
.../Base/InferenceGenerationViewModelBase.cs | 16 ++
.../CheckpointBrowserCardViewModel.cs | 62 ++++----
.../Settings/MainSettingsViewModel.cs | 2 +-
StabilityMatrix.Core/Helper/ImageMetadata.cs | 142 +++++++++++++++++-
.../Models/Database/LocalModelFile.cs | 6 +-
.../Services/MetadataImportService.cs | 3 +-
.../Services/SettingsManager.cs | 2 +
8 files changed, 194 insertions(+), 41 deletions(-)
diff --git a/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs b/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
index 9c001a92..50dbe733 100644
--- a/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
+++ b/StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
@@ -1,7 +1,9 @@
using System;
using System.Collections.Concurrent;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader.Loaders;
diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
index f756933f..17970867 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
@@ -420,6 +420,22 @@ public abstract partial class InferenceGenerationViewModelBase
outputImages.Add(new ImageSource(filePath));
EventManager.Instance.OnImageFileAdded(filePath);
}
+ else if (comfyImage.FileName.EndsWith(".webp"))
+ {
+ var bytesWithMetadata = ImageMetadata.AddMetadataToWebp(imageArray, parameters);
+
+ // Write using generated name
+ var filePath = await WriteOutputImageAsync(
+ new MemoryStream(bytesWithMetadata.ToArray()),
+ args,
+ i + 1,
+ images.Count,
+ fileExtension: Path.GetExtension(comfyImage.FileName).Replace(".", "")
+ );
+
+ outputImages.Add(new ImageSource(filePath));
+ EventManager.Instance.OnImageFileAdded(filePath);
+ }
else
{
// Write using generated name
diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
index 4dc4acbc..afdeb085 100644
--- a/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
@@ -21,6 +21,7 @@ using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
+using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
@@ -120,30 +121,24 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
var latestVersionInstalled =
latestVersion.Files != null
- && latestVersion
- .Files
- .Any(
- file =>
- file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null }
- && installedModels.Contains(file.Hashes.BLAKE3)
- );
+ && latestVersion.Files.Any(
+ file =>
+ file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null }
+ && installedModels.Contains(file.Hashes.BLAKE3)
+ );
// check if any of the ModelVersion.Files.Hashes.BLAKE3 hashes are in the installedModels list
var anyVersionInstalled =
latestVersionInstalled
- || CivitModel
- .ModelVersions
- .Any(
- version =>
- version.Files != null
- && version
- .Files
- .Any(
- file =>
- file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null }
- && installedModels.Contains(file.Hashes.BLAKE3)
- )
- );
+ || CivitModel.ModelVersions.Any(
+ version =>
+ version.Files != null
+ && version.Files.Any(
+ file =>
+ file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null }
+ && installedModels.Contains(file.Hashes.BLAKE3)
+ )
+ );
UpdateCardText = latestVersionInstalled
? "Installed"
@@ -154,7 +149,6 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
ShowUpdateCard = anyVersionInstalled;
}
- // Choose and load image based on nsfw setting
private void UpdateImage()
{
var nsfwEnabled = settingsManager.Settings.ModelBrowserNsfwEnabled;
@@ -162,21 +156,17 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
var images = version?.Images;
// Try to find a valid image
- var image = images?.FirstOrDefault(image => nsfwEnabled || image.Nsfw == "None");
+ var image = images
+ ?.Where(img => LocalModelFile.SupportedImageExtensions.Any(img.Url.Contains))
+ .FirstOrDefault(image => nsfwEnabled || image.Nsfw == "None");
if (image != null)
{
- // var imageStream = await downloadService.GetImageStreamFromUrl(image.Url);
- // Dispatcher.UIThread.Post(() => { CardImage = new Bitmap(imageStream); });
CardImage = new Uri(image.Url);
return;
}
// If no valid image found, use no image
CardImage = Assets.NoImage;
-
- // var assetStream = AssetLoader.Open(new Uri("avares://StabilityMatrix.Avalonia/Assets/noimage.png"));
- // Otherwise Default image
- // Dispatcher.UIThread.Post(() => { CardImage = new Bitmap(assetStream); });
}
[RelayCommand]
@@ -270,8 +260,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
{
var prunedDescription =
model
- .Description
- ?.Replace("
", $"{Environment.NewLine}{Environment.NewLine}")
+ .Description?.Replace("
", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("
", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("
", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("", $"{Environment.NewLine}{Environment.NewLine}")
@@ -318,9 +307,9 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
var imageExtension = Path.GetExtension(image.Url).TrimStart('.');
if (imageExtension is "jpg" or "jpeg" or "png")
{
- var imageDownloadPath = modelFilePath
- .Directory!
- .JoinFile($"{modelFilePath.NameWithoutExtension}.preview.{imageExtension}");
+ var imageDownloadPath = modelFilePath.Directory!.JoinFile(
+ $"{modelFilePath.NameWithoutExtension}.preview.{imageExtension}"
+ );
var imageTask = downloadService.DownloadToFileAsync(image.Url, imageDownloadPath);
await notificationService.TryAsync(imageTask, "Could not download preview image");
@@ -358,7 +347,8 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
}
// Get latest version file
- var modelFile = selectedFile ?? modelVersion.Files?.FirstOrDefault(x => x.Type == CivitFileType.Model);
+ var modelFile =
+ selectedFile ?? modelVersion.Files?.FirstOrDefault(x => x.Type == CivitFileType.Model);
if (modelFile is null)
{
notificationService.Show(
@@ -374,7 +364,9 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory);
- var downloadDirectory = rootModelsDirectory.JoinDir(model.Type.ConvertTo().GetStringValue());
+ var downloadDirectory = rootModelsDirectory.JoinDir(
+ model.Type.ConvertTo().GetStringValue()
+ );
// Folders might be missing if user didn't install any packages yet
downloadDirectory.Create();
diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
index 36d97593..aadc47da 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
@@ -709,7 +709,7 @@ public partial class MainSettingsViewModel : PageViewModelBase
if (files.Count == 0)
return;
- var data = await ImageMetadata.ReadTextChunkFromWebp(files[0].TryGetLocalPath(), ExifTag.Model);
+ var data = ImageMetadata.ReadTextChunkFromWebp(files[0].TryGetLocalPath(), ExifTag.ImageDescription);
return;
diff --git a/StabilityMatrix.Core/Helper/ImageMetadata.cs b/StabilityMatrix.Core/Helper/ImageMetadata.cs
index 439242bf..df9d1a62 100644
--- a/StabilityMatrix.Core/Helper/ImageMetadata.cs
+++ b/StabilityMatrix.Core/Helper/ImageMetadata.cs
@@ -1,7 +1,10 @@
-using System.Text;
+using System.Diagnostics;
+using System.Text;
using System.Text.Json;
+using ExifLibrary;
using MetadataExtractor;
using MetadataExtractor.Formats.Png;
+using Microsoft.VisualBasic;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
@@ -13,10 +16,13 @@ public class ImageMetadata
{
private IReadOnlyList? Directories { get; set; }
- private static readonly byte[] PngHeader = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
+ private static readonly byte[] PngHeader = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
private static readonly byte[] Idat = "IDAT"u8.ToArray();
private static readonly byte[] Text = "tEXt"u8.ToArray();
+ private static readonly byte[] Riff = "RIFF"u8.ToArray();
+ private static readonly byte[] Webp = "WEBP"u8.ToArray();
+
public static ImageMetadata ParseFile(FilePath path)
{
return new ImageMetadata { Directories = ImageMetadataReader.ReadMetadata(path) };
@@ -227,4 +233,136 @@ public class ImageMetadata
memoryStream.Position = 0;
return memoryStream;
}
+
+ public static string ReadTextChunkFromWebp(FilePath filePath, ExifTag exifTag)
+ {
+ var sw = Stopwatch.StartNew();
+ try
+ {
+ var exifChunks = GetExifChunks(filePath);
+ if (exifChunks.Length == 0)
+ return string.Empty;
+
+ // write exifChunks to new memoryStream but skip first 6 bytes
+ using var newMemoryStream = new MemoryStream(exifChunks[6..]);
+ newMemoryStream.Seek(0, SeekOrigin.Begin);
+
+ var img = new MyTiffFile(newMemoryStream, Encoding.UTF8);
+ return img.Properties[exifTag]?.Value?.ToString() ?? string.Empty;
+ }
+ finally
+ {
+ sw.Stop();
+ Console.WriteLine($"ReadTextChunkFromWebp took {sw.ElapsedMilliseconds}ms");
+ }
+ }
+
+ public static IEnumerable AddMetadataToWebp(
+ byte[] inputImage,
+ GenerationParameters generationParameters
+ )
+ {
+ using var byteStream = new BinaryReader(new MemoryStream(inputImage));
+ byteStream.BaseStream.Position = 0;
+
+ // Read first 8 bytes and make sure they match the RIFF header
+ if (!byteStream.ReadBytes(4).SequenceEqual(Riff))
+ {
+ return Array.Empty();
+ }
+
+ // skip 4 bytes then read next 4 for webp header
+ byteStream.BaseStream.Position += 4;
+ if (!byteStream.ReadBytes(4).SequenceEqual(Webp))
+ {
+ return Array.Empty();
+ }
+
+ while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4)
+ {
+ var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4));
+ var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).ToArray());
+
+ if (chunkType != "EXIF")
+ {
+ // skip chunk data
+ byteStream.BaseStream.Position += chunkSize;
+ continue;
+ }
+
+ var exifStart = byteStream.BaseStream.Position - 8;
+ var exifBytes = byteStream.ReadBytes(chunkSize);
+ Debug.WriteLine($"Found exif chunk of size {chunkSize}");
+
+ using var stream = new MemoryStream(exifBytes[6..]);
+ var img = new MyTiffFile(stream, Encoding.UTF8);
+ img.Properties.Set(ExifTag.ImageDescription, JsonSerializer.Serialize(generationParameters));
+
+ using var newStream = new MemoryStream();
+ img.Save(newStream);
+ newStream.Seek(0, SeekOrigin.Begin);
+ var newExifBytes = exifBytes[..6].Concat(newStream.ToArray());
+ var newExifSize = newExifBytes.Count();
+ var newChunkSize = BitConverter.GetBytes(newExifSize);
+ var newChunk = "EXIF"u8.ToArray().Concat(newChunkSize).Concat(newExifBytes).ToArray();
+
+ var inputEndIndex = (int)exifStart;
+ var newImage = inputImage[..inputEndIndex].Concat(newChunk).ToArray();
+
+ // webp or tiff or something requires even number of bytes
+ if (newImage.Length % 2 != 0)
+ {
+ newImage = newImage.Concat(new byte[] { 0x00 }).ToArray();
+ }
+
+ var newImageSize = BitConverter.GetBytes(newImage.Length - 8);
+ newImage[4] = newImageSize[0];
+ newImage[5] = newImageSize[1];
+ newImage[6] = newImageSize[2];
+ newImage[7] = newImageSize[3];
+ return newImage;
+ }
+
+ return Array.Empty();
+ }
+
+ private static byte[] GetExifChunks(FilePath imagePath)
+ {
+ using var byteStream = new BinaryReader(File.OpenRead(imagePath));
+ byteStream.BaseStream.Position = 0;
+
+ // Read first 8 bytes and make sure they match the RIFF header
+ if (!byteStream.ReadBytes(4).SequenceEqual(Riff))
+ {
+ return Array.Empty();
+ }
+
+ // skip 4 bytes then read next 4 for webp header
+ byteStream.BaseStream.Position += 4;
+ if (!byteStream.ReadBytes(4).SequenceEqual(Webp))
+ {
+ return Array.Empty();
+ }
+
+ while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4)
+ {
+ var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4));
+ var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).ToArray());
+
+ if (chunkType != "EXIF")
+ {
+ // skip chunk data
+ byteStream.BaseStream.Position += chunkSize;
+ continue;
+ }
+
+ var exifStart = byteStream.BaseStream.Position;
+ var exifBytes = byteStream.ReadBytes(chunkSize);
+ var exif = Encoding.UTF8.GetString(exifBytes);
+ Debug.WriteLine($"Found exif chunk of size {chunkSize}");
+ return exifBytes;
+ }
+
+ return Array.Empty();
+ }
}
diff --git a/StabilityMatrix.Core/Models/Database/LocalModelFile.cs b/StabilityMatrix.Core/Models/Database/LocalModelFile.cs
index ab3b7898..462b9def 100644
--- a/StabilityMatrix.Core/Models/Database/LocalModelFile.cs
+++ b/StabilityMatrix.Core/Models/Database/LocalModelFile.cs
@@ -63,7 +63,9 @@ public class LocalModelFile
if (PreviewImageFullPath != null)
return PreviewImageFullPath;
- return PreviewImageRelativePath == null ? null : Path.Combine(rootModelDirectory, PreviewImageRelativePath);
+ return PreviewImageRelativePath == null
+ ? null
+ : Path.Combine(rootModelDirectory, PreviewImageRelativePath);
}
[BsonIgnore]
@@ -113,6 +115,6 @@ public class LocalModelFile
".pth",
".bin"
];
- public static readonly HashSet SupportedImageExtensions = [".png", ".jpg", ".jpeg", ".gif"];
+ public static readonly HashSet SupportedImageExtensions = [".png", ".jpg", ".jpeg", ".webp"];
public static readonly HashSet SupportedMetadataExtensions = [".json"];
}
diff --git a/StabilityMatrix.Core/Services/MetadataImportService.cs b/StabilityMatrix.Core/Services/MetadataImportService.cs
index 5a88fb83..ca1803b4 100644
--- a/StabilityMatrix.Core/Services/MetadataImportService.cs
+++ b/StabilityMatrix.Core/Services/MetadataImportService.cs
@@ -1,4 +1,5 @@
-using System.Text.Json;
+using System.Diagnostics;
+using System.Text.Json;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
diff --git a/StabilityMatrix.Core/Services/SettingsManager.cs b/StabilityMatrix.Core/Services/SettingsManager.cs
index 2b0849c3..61e04aa7 100644
--- a/StabilityMatrix.Core/Services/SettingsManager.cs
+++ b/StabilityMatrix.Core/Services/SettingsManager.cs
@@ -685,6 +685,8 @@ public class SettingsManager : ISettingsManager
if (fs.CanWrite)
{
fs.Write(jsonBytes, 0, jsonBytes.Length);
+ fs.Flush();
+ fs.SetLength(jsonBytes.Length);
}
fs.Close();
}
From 96f722a9da2413517aa9a587a437f5f9beb07ab8 Mon Sep 17 00:00:00 2001
From: ionite34
Date: Tue, 26 Dec 2023 17:06:28 +0800
Subject: [PATCH 024/116] Add WebpAnimation support in ImageBoxView
---
.../Controls/AdvancedImageBoxView.axaml | 49 ++++++++++++-------
.../Controls/AdvancedImageBoxView.axaml.cs | 13 +++--
.../Controls/DataTemplateSelector.cs | 46 +++++++++++++++++
.../Models/ITemplateKey.cs | 11 +++++
.../Models/ImageSource.cs | 19 ++++++-
.../Models/ImageSourceTemplateType.cs | 8 +++
6 files changed, 122 insertions(+), 24 deletions(-)
create mode 100644 StabilityMatrix.Avalonia/Controls/DataTemplateSelector.cs
create mode 100644 StabilityMatrix.Avalonia/Models/ITemplateKey.cs
create mode 100644 StabilityMatrix.Avalonia/Models/ImageSourceTemplateType.cs
diff --git a/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml b/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
index eb681d53..698daab8 100644
--- a/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
+++ b/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
@@ -8,30 +8,43 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:gif="clr-namespace:Avalonia.Gif;assembly=Avalonia.Gif"
d:DataContext="{x:Static mocks:DesignData.SampleImageSource}"
d:DesignHeight="450"
d:DesignWidth="800"
x:DataType="models:ImageSource"
mc:Ignorable="d">
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
("CopyMenuItem")!;
- copyMenuItem.Command = new AsyncRelayCommand(FlyoutCopy);
+
+ if (this.FindControl("CopyMenuItem") is { } copyMenuItem)
+ {
+ copyMenuItem.Command = new AsyncRelayCommand(FlyoutCopy);
+ }
}
-
+
private static async Task FlyoutCopy(Bitmap? image)
{
- if (image is null || !Compat.IsWindows) return;
+ if (image is null || !Compat.IsWindows)
+ return;
await Task.Run(() =>
{
diff --git a/StabilityMatrix.Avalonia/Controls/DataTemplateSelector.cs b/StabilityMatrix.Avalonia/Controls/DataTemplateSelector.cs
new file mode 100644
index 00000000..4e9ac24f
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Controls/DataTemplateSelector.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+using Avalonia.Metadata;
+using JetBrains.Annotations;
+using StabilityMatrix.Avalonia.Models;
+
+namespace StabilityMatrix.Avalonia.Controls;
+
+///
+/// Selector for objects implementing
+///
+[PublicAPI]
+public class DataTemplateSelector : IDataTemplate
+ where TKey : notnull
+{
+ ///
+ /// Key that is used when no other key matches
+ ///
+ public TKey? DefaultKey { get; set; }
+
+ [Content]
+ public Dictionary Templates { get; } = new();
+
+ public bool Match(object? data) => data is ITemplateKey;
+
+ ///
+ public Control Build(object? data)
+ {
+ if (data is not ITemplateKey key)
+ throw new ArgumentException(null, nameof(data));
+
+ if (Templates.TryGetValue(key.TemplateKey, out var template))
+ {
+ return template.Build(data)!;
+ }
+
+ if (DefaultKey is not null && Templates.TryGetValue(DefaultKey, out var defaultTemplate))
+ {
+ return defaultTemplate.Build(data)!;
+ }
+
+ throw new ArgumentException(null, nameof(data));
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Models/ITemplateKey.cs b/StabilityMatrix.Avalonia/Models/ITemplateKey.cs
new file mode 100644
index 00000000..34c998d7
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Models/ITemplateKey.cs
@@ -0,0 +1,11 @@
+using StabilityMatrix.Avalonia.Controls;
+
+namespace StabilityMatrix.Avalonia.Models;
+
+///
+/// Implements a template key for
+///
+public interface ITemplateKey
+{
+ T TemplateKey { get; }
+}
diff --git a/StabilityMatrix.Avalonia/Models/ImageSource.cs b/StabilityMatrix.Avalonia/Models/ImageSource.cs
index 4a8fffb7..f20d316a 100644
--- a/StabilityMatrix.Avalonia/Models/ImageSource.cs
+++ b/StabilityMatrix.Avalonia/Models/ImageSource.cs
@@ -12,7 +12,7 @@ using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Avalonia.Models;
-public record ImageSource : IDisposable
+public record ImageSource : IDisposable, ITemplateKey
{
private Hash? contentHashBlake3;
@@ -55,6 +55,23 @@ public record ImageSource : IDisposable
Bitmap = bitmap;
}
+ ///
+ public ImageSourceTemplateType TemplateKey
+ {
+ get
+ {
+ var ext = LocalFile?.Extension ?? Path.GetExtension(RemoteUrl?.ToString());
+
+ if (ext is not null && ext.Equals(".webp", StringComparison.OrdinalIgnoreCase))
+ {
+ // TODO: Check if webp is animated
+ return ImageSourceTemplateType.WebpAnimation;
+ }
+
+ return ImageSourceTemplateType.Image;
+ }
+ }
+
[JsonIgnore]
public Task BitmapAsync => GetBitmapAsync();
diff --git a/StabilityMatrix.Avalonia/Models/ImageSourceTemplateType.cs b/StabilityMatrix.Avalonia/Models/ImageSourceTemplateType.cs
new file mode 100644
index 00000000..14ab15d4
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Models/ImageSourceTemplateType.cs
@@ -0,0 +1,8 @@
+namespace StabilityMatrix.Avalonia.Models;
+
+public enum ImageSourceTemplateType
+{
+ Default,
+ Image,
+ WebpAnimation
+}
From 25a1a40f6174906c39dd5a860c9edd6108e78825 Mon Sep 17 00:00:00 2001
From: ionite34
Date: Tue, 26 Dec 2023 17:16:23 +0800
Subject: [PATCH 025/116] Use ImageGalleryCard for ImageToVideo
---
.../InferenceImageToVideoViewModel.cs | 59 +++++--------------
.../Inference/InferenceImageToVideoView.axaml | 7 ---
2 files changed, 16 insertions(+), 50 deletions(-)
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
index 4be708a7..c53af66d 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
@@ -30,7 +30,9 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(InferenceImageToVideoView), persistent: true)]
[ManagedService]
[Transient]
-public partial class InferenceImageToVideoViewModel : InferenceGenerationViewModelBase, IParametersLoadableState
+public partial class InferenceImageToVideoViewModel
+ : InferenceGenerationViewModelBase,
+ IParametersLoadableState
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@@ -61,14 +63,6 @@ public partial class InferenceImageToVideoViewModel : InferenceGenerationViewMod
[JsonPropertyName("VideoOutput")]
public VideoOutputSettingsCardViewModel VideoOutputSettingsCardViewModel { get; }
- [ObservableProperty]
- [JsonIgnore]
- private string outputUri;
-
- [ObservableProperty]
- [JsonIgnore]
- private bool isGenerating;
-
public InferenceImageToVideoViewModel(
INotificationService notificationService,
IInferenceClientManager inferenceClientManager,
@@ -118,24 +112,6 @@ public partial class InferenceImageToVideoViewModel : InferenceGenerationViewMod
);
}
- public override void OnLoaded()
- {
- EventManager.Instance.ImageFileAdded += OnImageFileAdded;
- }
-
- public override void OnUnloaded()
- {
- EventManager.Instance.ImageFileAdded -= OnImageFileAdded;
- }
-
- private void OnImageFileAdded(object? sender, FilePath e)
- {
- if (!e.Extension.Equals(".webp", StringComparison.OrdinalIgnoreCase))
- return;
-
- OutputUri = e;
- }
-
///
protected override void BuildPrompt(BuildPromptEventArgs args)
{
@@ -153,17 +129,15 @@ public partial class InferenceImageToVideoViewModel : InferenceGenerationViewMod
ModelCardViewModel.ApplyStep(args);
// Setup latent from image
- var imageLoad = builder
- .Nodes
- .AddTypedNode(
- new ComfyNodeBuilder.LoadImage
- {
- Name = builder.Nodes.GetUniqueName("ControlNet_LoadImage"),
- Image =
- SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference")
- ?? throw new ValidationException()
- }
- );
+ var imageLoad = builder.Nodes.AddTypedNode(
+ new ComfyNodeBuilder.LoadImage
+ {
+ Name = builder.Nodes.GetUniqueName("ControlNet_LoadImage"),
+ Image =
+ SelectImageCardViewModel.ImageSource?.GetHashGuidFileNameCached("Inference")
+ ?? throw new ValidationException()
+ }
+ );
builder.Connections.Primary = imageLoad.Output1;
builder.Connections.PrimarySize = SelectImageCardViewModel.CurrentBitmapSize;
@@ -190,7 +164,10 @@ public partial class InferenceImageToVideoViewModel : InferenceGenerationViewMod
}
///
- protected override async Task GenerateImageImpl(GenerateOverrides overrides, CancellationToken cancellationToken)
+ protected override async Task GenerateImageImpl(
+ GenerateOverrides overrides,
+ CancellationToken cancellationToken
+ )
{
if (!await CheckClientConnectedWithPrompt() || !ClientManager.IsConnected)
{
@@ -229,15 +206,11 @@ public partial class InferenceImageToVideoViewModel : InferenceGenerationViewMod
batchArgs.Add(generationArgs);
}
- IsGenerating = true;
-
// Run batches
foreach (var args in batchArgs)
{
await RunGeneration(args, cancellationToken);
}
-
- IsGenerating = false;
}
///
diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml
index c3497945..0dcb6e0b 100644
--- a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml
+++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToVideoView.axaml
@@ -96,14 +96,7 @@
DataContext="{Binding ElementName=Dock, Path=DataContext}">
-
-
-
Date: Tue, 26 Dec 2023 02:43:07 -0800
Subject: [PATCH 026/116] save/load img2vid and show webps in dialog thing
---
.../Models/Inference/VideoOutputMethod.cs | 5 +-
.../Base/InferenceGenerationViewModelBase.cs | 19 +++++-
.../InferenceImageToVideoViewModel.cs | 7 ++-
.../Video/SvdImgToVidConditioningViewModel.cs | 16 ++++-
.../Video/VideoOutputSettingsCardViewModel.cs | 20 ++++++-
.../Settings/MainSettingsViewModel.cs | 5 +-
.../Views/Dialogs/ImageViewerDialog.axaml | 59 +++++++++++++------
StabilityMatrix.Core/Helper/ImageMetadata.cs | 48 ++++++++-------
.../Models/Database/LocalImageFile.cs | 44 +++++++++++++-
.../Models/GenerationParameters.cs | 9 +++
10 files changed, 177 insertions(+), 55 deletions(-)
diff --git a/StabilityMatrix.Avalonia/Models/Inference/VideoOutputMethod.cs b/StabilityMatrix.Avalonia/Models/Inference/VideoOutputMethod.cs
index b2ffcf99..134a3c0b 100644
--- a/StabilityMatrix.Avalonia/Models/Inference/VideoOutputMethod.cs
+++ b/StabilityMatrix.Avalonia/Models/Inference/VideoOutputMethod.cs
@@ -1,5 +1,8 @@
-namespace StabilityMatrix.Avalonia.Models.Inference;
+using System.Text.Json.Serialization;
+namespace StabilityMatrix.Avalonia.Models.Inference;
+
+[JsonConverter(typeof(JsonStringEnumConverter))]
public enum VideoOutputMethod
{
Fastest,
diff --git a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
index 17970867..7a50ee01 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs
@@ -5,6 +5,8 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
+using System.Management;
+using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
@@ -12,6 +14,8 @@ using AsyncAwaitBestPractices;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input;
+using ExifLibrary;
+using MetadataExtractor.Formats.Exif;
using NLog;
using Refit;
using SkiaSharp;
@@ -422,7 +426,20 @@ public abstract partial class InferenceGenerationViewModelBase
}
else if (comfyImage.FileName.EndsWith(".webp"))
{
- var bytesWithMetadata = ImageMetadata.AddMetadataToWebp(imageArray, parameters);
+ var opts = new JsonSerializerOptions
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ Converters = { new JsonStringEnumConverter() }
+ };
+ var paramsJson = JsonSerializer.Serialize(parameters, opts);
+ var smProject = JsonSerializer.Serialize(project, opts);
+ var metadata = new Dictionary
+ {
+ { ExifTag.ImageDescription, paramsJson },
+ { ExifTag.Software, smProject }
+ };
+
+ var bytesWithMetadata = ImageMetadata.AddMetadataToWebp(imageArray, metadata);
// Write using generated name
var filePath = await WriteOutputImageAsync(
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
index c53af66d..b64a4801 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/InferenceImageToVideoViewModel.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.Drawing;
using System.Linq;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
@@ -23,8 +22,6 @@ using StabilityMatrix.Core.Models.Api.Comfy.Nodes;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Services;
-#pragma warning disable CS0657 // Not a valid attribute location for this declaration
-
namespace StabilityMatrix.Avalonia.ViewModels.Inference;
[View(typeof(InferenceImageToVideoView), persistent: true)]
@@ -218,6 +215,8 @@ public partial class InferenceImageToVideoViewModel
{
SamplerCardViewModel.LoadStateFromParameters(parameters);
ModelCardViewModel.LoadStateFromParameters(parameters);
+ SvdImgToVidConditioningViewModel.LoadStateFromParameters(parameters);
+ VideoOutputSettingsCardViewModel.LoadStateFromParameters(parameters);
SeedCardViewModel.Seed = Convert.ToInt64(parameters.Seed);
}
@@ -227,6 +226,8 @@ public partial class InferenceImageToVideoViewModel
{
parameters = SamplerCardViewModel.SaveStateToParameters(parameters);
parameters = ModelCardViewModel.SaveStateToParameters(parameters);
+ parameters = SvdImgToVidConditioningViewModel.SaveStateToParameters(parameters);
+ parameters = VideoOutputSettingsCardViewModel.SaveStateToParameters(parameters);
parameters.Seed = (ulong)SeedCardViewModel.Seed;
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs
index 9ff667e6..1bb70b26 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/SvdImgToVidConditioningViewModel.cs
@@ -44,13 +44,23 @@ public partial class SvdImgToVidConditioningViewModel
{
Width = parameters.Width;
Height = parameters.Height;
- // TODO: add more metadata
+ NumFrames = parameters.FrameCount;
+ MotionBucketId = parameters.MotionBucketId;
+ Fps = parameters.Fps;
+ AugmentationLevel = parameters.AugmentationLevel;
+ MinCfg = parameters.MinCfg;
}
public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
{
- // TODO: add more metadata
- return parameters with { Width = Width, Height = Height, };
+ return parameters with
+ {
+ FrameCount = NumFrames,
+ MotionBucketId = MotionBucketId,
+ Fps = Fps,
+ AugmentationLevel = AugmentationLevel,
+ MinCfg = MinCfg,
+ };
}
public void ApplyStep(ModuleApplyStepEventArgs e)
diff --git a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/VideoOutputSettingsCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/VideoOutputSettingsCardViewModel.cs
index c0cdea9b..d44f469a 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Inference/Video/VideoOutputSettingsCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Inference/Video/VideoOutputSettingsCardViewModel.cs
@@ -37,13 +37,27 @@ public partial class VideoOutputSettingsCardViewModel
public void LoadStateFromParameters(GenerationParameters parameters)
{
- // TODO
+ Fps = parameters.OutputFps;
+ Lossless = parameters.Lossless;
+ Quality = parameters.VideoQuality;
+
+ if (string.IsNullOrWhiteSpace(parameters.VideoOutputMethod))
+ return;
+
+ SelectedMethod = Enum.TryParse(parameters.VideoOutputMethod, true, out var method)
+ ? method
+ : VideoOutputMethod.Default;
}
public GenerationParameters SaveStateToParameters(GenerationParameters parameters)
{
- // TODO
- return parameters;
+ return parameters with
+ {
+ OutputFps = Fps,
+ Lossless = Lossless,
+ VideoQuality = Quality,
+ VideoOutputMethod = SelectedMethod.ToString(),
+ };
}
public void ApplyStep(ModuleApplyStepEventArgs e)
diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
index aadc47da..bf241a12 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
@@ -26,6 +26,7 @@ using CommunityToolkit.Mvvm.Input;
using DynamicData.Binding;
using ExifLibrary;
using FluentAvalonia.UI.Controls;
+using MetadataExtractor.Formats.Exif;
using NLog;
using SkiaSharp;
using StabilityMatrix.Avalonia.Animations;
@@ -709,10 +710,6 @@ public partial class MainSettingsViewModel : PageViewModelBase
if (files.Count == 0)
return;
- var data = ImageMetadata.ReadTextChunkFromWebp(files[0].TryGetLocalPath(), ExifTag.ImageDescription);
-
- return;
-
var metadata = ImageMetadata.ParseFile(files[0].TryGetLocalPath()!);
var textualTags = metadata.GetTextualData()?.ToArray();
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml
index 39152677..d39924d2 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml
@@ -10,6 +10,9 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vmDialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
+ xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
+ xmlns:gif="clr-namespace:Avalonia.Gif;assembly=Avalonia.Gif"
+ xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
d:DataContext="{x:Static mocks:DesignData.ImageViewerViewModel}"
@@ -46,24 +49,46 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
RowDefinitions="*,Auto">
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /// Reads an EXIF tag from a webp file and returns the value as string
+ ///
+ /// The webp file to read EXIF data from
+ /// Use constants for the tag you'd like to search for
+ ///
+ public static string ReadTextChunkFromWebp(FilePath filePath, int exifTag)
{
- var sw = Stopwatch.StartNew();
- try
- {
- var exifChunks = GetExifChunks(filePath);
- if (exifChunks.Length == 0)
- return string.Empty;
-
- // write exifChunks to new memoryStream but skip first 6 bytes
- using var newMemoryStream = new MemoryStream(exifChunks[6..]);
- newMemoryStream.Seek(0, SeekOrigin.Begin);
-
- var img = new MyTiffFile(newMemoryStream, Encoding.UTF8);
- return img.Properties[exifTag]?.Value?.ToString() ?? string.Empty;
- }
- finally
- {
- sw.Stop();
- Console.WriteLine($"ReadTextChunkFromWebp took {sw.ElapsedMilliseconds}ms");
- }
+ var exifDirs = WebPMetadataReader.ReadMetadata(filePath).OfType().FirstOrDefault();
+ return exifDirs is null ? string.Empty : exifDirs.GetString(exifTag) ?? string.Empty;
}
public static IEnumerable AddMetadataToWebp(
byte[] inputImage,
- GenerationParameters generationParameters
+ Dictionary exifTagData
)
{
using var byteStream = new BinaryReader(new MemoryStream(inputImage));
@@ -296,7 +295,11 @@ public class ImageMetadata
using var stream = new MemoryStream(exifBytes[6..]);
var img = new MyTiffFile(stream, Encoding.UTF8);
- img.Properties.Set(ExifTag.ImageDescription, JsonSerializer.Serialize(generationParameters));
+
+ foreach (var (key, value) in exifTagData)
+ {
+ img.Properties.Set(key, value);
+ }
using var newStream = new MemoryStream();
img.Save(newStream);
@@ -315,6 +318,7 @@ public class ImageMetadata
newImage = newImage.Concat(new byte[] { 0x00 }).ToArray();
}
+ // no clue why the minus 8 is needed but it is
var newImageSize = BitConverter.GetBytes(newImage.Length - 8);
newImage[4] = newImageSize[0];
newImage[5] = newImageSize[1];
diff --git a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs
index a050b6c6..76b3ff78 100644
--- a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs
+++ b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs
@@ -1,4 +1,5 @@
using DynamicData.Tests;
+using MetadataExtractor.Formats.Exif;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.FileInterfaces;
using JsonSerializer = System.Text.Json.JsonSerializer;
@@ -50,6 +51,17 @@ public record LocalImageFile
public (string? Parameters, string? ParametersJson, string? SMProject, string? ComfyNodes) ReadMetadata()
{
+ if (AbsolutePath.EndsWith("webp"))
+ {
+ var paramsJson = ImageMetadata.ReadTextChunkFromWebp(
+ AbsolutePath,
+ ExifDirectoryBase.TagImageDescription
+ );
+ var smProj = ImageMetadata.ReadTextChunkFromWebp(AbsolutePath, ExifDirectoryBase.TagSoftware);
+
+ return (null, paramsJson, smProj, null);
+ }
+
using var stream = new FileStream(AbsolutePath, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new BinaryReader(stream);
@@ -71,6 +83,29 @@ public record LocalImageFile
// TODO: Support other types
const LocalImageFileType imageType = LocalImageFileType.Inference | LocalImageFileType.TextToImage;
+ if (filePath.Extension.Contains("webp"))
+ {
+ var paramsJson = ImageMetadata.ReadTextChunkFromWebp(
+ filePath,
+ ExifDirectoryBase.TagImageDescription
+ );
+ var parameters = string.IsNullOrWhiteSpace(paramsJson)
+ ? null
+ : JsonSerializer.Deserialize(paramsJson);
+
+ filePath.Info.Refresh();
+
+ return new LocalImageFile
+ {
+ AbsolutePath = filePath,
+ ImageType = imageType,
+ CreatedAt = filePath.Info.CreationTimeUtc,
+ LastModifiedAt = filePath.Info.LastWriteTimeUtc,
+ GenerationParameters = parameters,
+ ImageSize = new Size(parameters?.Width ?? 0, parameters?.Height ?? 0)
+ };
+ }
+
// Get metadata
using var stream = filePath.Info.OpenRead();
using var reader = new BinaryReader(stream);
@@ -104,5 +139,12 @@ public record LocalImageFile
};
}
- public static readonly HashSet SupportedImageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp"];
+ public static readonly HashSet SupportedImageExtensions =
+ [
+ ".png",
+ ".jpg",
+ ".jpeg",
+ ".gif",
+ ".webp"
+ ];
}
diff --git a/StabilityMatrix.Core/Models/GenerationParameters.cs b/StabilityMatrix.Core/Models/GenerationParameters.cs
index c0b5aa9b..46f7459a 100644
--- a/StabilityMatrix.Core/Models/GenerationParameters.cs
+++ b/StabilityMatrix.Core/Models/GenerationParameters.cs
@@ -20,6 +20,15 @@ public partial record GenerationParameters
public int Width { get; set; }
public string? ModelHash { get; set; }
public string? ModelName { get; set; }
+ public int FrameCount { get; set; }
+ public int MotionBucketId { get; set; }
+ public int VideoQuality { get; set; }
+ public bool Lossless { get; set; }
+ public int Fps { get; set; }
+ public double OutputFps { get; set; }
+ public double MinCfg { get; set; }
+ public double AugmentationLevel { get; set; }
+ public string? VideoOutputMethod { get; set; }
public static bool TryParse(
string? text,
From b13bea5b6fad141c7b15f72099ce1951dd9ceb1d Mon Sep 17 00:00:00 2001
From: JT
Date: Tue, 26 Dec 2023 02:51:51 -0800
Subject: [PATCH 027/116] add webps to outputs page
---
.../ViewModels/OutputsPageViewModel.cs | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
index b49ff6d1..99bfca1b 100644
--- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
@@ -93,6 +93,8 @@ public partial class OutputsPageViewModel : PageViewModelBase
? Resources.Label_OneImageSelected
: string.Format(Resources.Label_NumImagesSelected, NumItemsSelected);
+ private string[] allowedExtensions = [".png", ".webp"];
+
public OutputsPageViewModel(
ISettingsManager settingsManager,
IPackageFactory packageFactory,
@@ -434,11 +436,14 @@ public partial class OutputsPageViewModel : PageViewModelBase
var directory = category.Tag.ToString();
- foreach (var path in Directory.EnumerateFiles(directory, "*.png", SearchOption.AllDirectories))
+ foreach (var path in Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories))
{
try
{
var file = new FilePath(path);
+ if (!allowedExtensions.Contains(file.Extension))
+ continue;
+
var newPath = settingsManager.ConsolidatedImagesDirectory + file.Name;
if (file.FullPath == newPath)
continue;
@@ -496,7 +501,8 @@ public partial class OutputsPageViewModel : PageViewModelBase
}
var files = Directory
- .EnumerateFiles(directory, "*.png", SearchOption.AllDirectories)
+ .EnumerateFiles(directory, "*.*", SearchOption.AllDirectories)
+ .Where(path => allowedExtensions.Contains(new FilePath(path).Extension))
.Select(file => LocalImageFile.FromPath(file))
.ToList();
From b4e4886249b8ebf886078cac16978427b4751b7c Mon Sep 17 00:00:00 2001
From: Ionite
Date: Tue, 26 Dec 2023 05:58:04 -0500
Subject: [PATCH 028/116] Update CHANGELOG.md
---
CHANGELOG.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 820add42..d08547fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
+## v2.8.0-dev.1
+### Added
+#### Inference
+- Added image and model details in model selection boxes
+- Added CLIP Skip setting, toggleable from the model settings button
+
## v2.7.5
### Fixed
- Fixed Python Packages manager crash when pip list returns warnings in json
From 7ef86731a69109d5004928c81243ffb6562aae0b Mon Sep 17 00:00:00 2001
From: JT
Date: Tue, 26 Dec 2023 03:23:16 -0800
Subject: [PATCH 029/116] moved strings to resources & added send to
img2img/vid options in output browser
---
.../VideoGenerationSettingsCard.axaml | 10 +-
.../Controls/VideoOutputSettingsCard.axaml | 8 +-
.../FallbackRamCachedWebImageLoader.cs | 2 -
.../Languages/Resources.Designer.cs | 81 +++++++++
.../Languages/Resources.resx | 27 +++
.../ViewModels/InferenceViewModel.cs | 159 +++++++++++++-----
.../ViewModels/OutputsPageViewModel.cs | 12 ++
.../Views/OutputsPage.axaml | 16 +-
StabilityMatrix.Core/Helper/EventManager.cs | 23 +--
.../Models/Database/LocalImageFile.cs | 2 +-
10 files changed, 263 insertions(+), 77 deletions(-)
diff --git a/StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml b/StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml
index 229bc551..f9f38ba9 100644
--- a/StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml
+++ b/StabilityMatrix.Avalonia/Controls/VideoGenerationSettingsCard.axaml
@@ -24,7 +24,7 @@
Grid.Column="0"
Margin="0,0,8,0"
VerticalAlignment="Center"
- Text="Frames" />
+ Text="{x:Static lang:Resources.Label_Frames}" />
+ Text="{x:Static lang:Resources.Label_Fps}" />
+ Text="{x:Static lang:Resources.Label_MinCfg}" />
+ Text="{x:Static lang:Resources.Label_MotionBucketId}" />
+ Text="{x:Static lang:Resources.Label_AugmentationLevel}" />
+ Text="{x:Static lang:Resources.Label_Fps}" />
+ Text="{x:Static lang:Resources.Label_Lossless}" />
+ Text="{x:Static lang:Resources.Label_VideoQuality}" />
+ Text="{x:Static lang:Resources.Label_VideoOutputMethod}" />
+ /// Looks up a localized string similar to Augmentation Level.
+ ///
+ public static string Label_AugmentationLevel {
+ get {
+ return ResourceManager.GetString("Label_AugmentationLevel", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Auto Completion.
///
@@ -1193,6 +1202,24 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Frames Per Second.
+ ///
+ public static string Label_Fps {
+ get {
+ return ResourceManager.GetString("Label_Fps", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Frames.
+ ///
+ public static string Label_Frames {
+ get {
+ return ResourceManager.GetString("Label_Frames", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to General.
///
@@ -1238,6 +1265,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Image to Video.
+ ///
+ public static string Label_ImageToVideo {
+ get {
+ return ResourceManager.GetString("Label_ImageToVideo", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Image Viewer.
///
@@ -1436,6 +1472,24 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Lossless.
+ ///
+ public static string Label_Lossless {
+ get {
+ return ResourceManager.GetString("Label_Lossless", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Min CFG.
+ ///
+ public static string Label_MinCfg {
+ get {
+ return ResourceManager.GetString("Label_MinCfg", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Missing Image File.
///
@@ -1490,6 +1544,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Motion Bucket ID.
+ ///
+ public static string Label_MotionBucketId {
+ get {
+ return ResourceManager.GetString("Label_MotionBucketId", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Networks (Lora / LyCORIS).
///
@@ -2165,6 +2228,24 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
+ ///
+ /// Looks up a localized string similar to Method.
+ ///
+ public static string Label_VideoOutputMethod {
+ get {
+ return ResourceManager.GetString("Label_VideoOutputMethod", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Quality.
+ ///
+ public static string Label_VideoQuality {
+ get {
+ return ResourceManager.GetString("Label_VideoQuality", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Waiting to connect....
///
diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx
index c0a18936..d9641154 100644
--- a/StabilityMatrix.Avalonia/Languages/Resources.resx
+++ b/StabilityMatrix.Avalonia/Languages/Resources.resx
@@ -900,4 +900,31 @@
CLIP Skip
+
+ Image to Video
+
+
+ Frames Per Second
+
+
+ Min CFG
+
+
+ Lossless
+
+
+ Frames
+
+
+ Motion Bucket ID
+
+
+ Augmentation Level
+
+
+ Method
+
+
+ Quality
+
diff --git a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
index e3966c1f..a0959d5e 100644
--- a/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/InferenceViewModel.cs
@@ -53,7 +53,8 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
private readonly ILiteDbContext liteDbContext;
public override string Title => "Inference";
- public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.AppGeneric, IsFilled = true };
+ public override IconSource IconSource =>
+ new SymbolIconSource { Symbol = Symbol.AppGeneric, IsFilled = true };
public RefreshBadgeViewModel ConnectionBadge { get; } =
new()
@@ -110,6 +111,8 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
// "Send to Inference"
EventManager.Instance.InferenceTextToImageRequested += OnInferenceTextToImageRequested;
EventManager.Instance.InferenceUpscaleRequested += OnInferenceUpscaleRequested;
+ EventManager.Instance.InferenceImageToImageRequested += OnInferenceImageToImageRequested;
+ EventManager.Instance.InferenceImageToVideoRequested += OnInferenceImageToVideoRequested;
MenuSaveAsCommand.WithConditionalNotificationErrorHandler(notificationService);
MenuOpenProjectCommand.WithConditionalNotificationErrorHandler(notificationService);
@@ -126,47 +129,43 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
IDisposable? onStartupComplete = null;
- Dispatcher
- .UIThread
- .Post(() =>
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (e.CurrentPackagePair?.BasePackage is ComfyUI package)
{
- if (e.CurrentPackagePair?.BasePackage is ComfyUI package)
- {
- IsWaitingForConnection = true;
- onStartupComplete = Observable
- .FromEventPattern(package, nameof(package.StartupComplete))
- .Take(1)
- .Subscribe(_ =>
+ IsWaitingForConnection = true;
+ onStartupComplete = Observable
+ .FromEventPattern(package, nameof(package.StartupComplete))
+ .Take(1)
+ .Subscribe(_ =>
+ {
+ Dispatcher.UIThread.Post(() =>
{
- Dispatcher
- .UIThread
- .Post(() =>
- {
- if (ConnectCommand.CanExecute(null))
- {
- Logger.Trace("On package launch - starting connection");
- ConnectCommand.Execute(null);
- }
- IsWaitingForConnection = false;
- });
+ if (ConnectCommand.CanExecute(null))
+ {
+ Logger.Trace("On package launch - starting connection");
+ ConnectCommand.Execute(null);
+ }
+ IsWaitingForConnection = false;
});
- }
- else
+ });
+ }
+ else
+ {
+ // Cancel any pending connection
+ if (ConnectCancelCommand.CanExecute(null))
{
- // Cancel any pending connection
- if (ConnectCancelCommand.CanExecute(null))
- {
- ConnectCancelCommand.Execute(null);
- }
- onStartupComplete?.Dispose();
- onStartupComplete = null;
- IsWaitingForConnection = false;
-
- // Disconnect
- Logger.Trace("On package close - disconnecting");
- DisconnectCommand.Execute(null);
+ ConnectCancelCommand.Execute(null);
}
- });
+ onStartupComplete?.Dispose();
+ onStartupComplete = null;
+ IsWaitingForConnection = false;
+
+ // Disconnect
+ Logger.Trace("On package close - disconnecting");
+ DisconnectCommand.Execute(null);
+ }
+ });
}
public override void OnLoaded()
@@ -216,9 +215,14 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
);
// Set not open
- await liteDbContext
- .InferenceProjects
- .UpdateAsync(project with { IsOpen = false, IsSelected = false, CurrentTabIndex = -1 });
+ await liteDbContext.InferenceProjects.UpdateAsync(
+ project with
+ {
+ IsOpen = false,
+ IsSelected = false,
+ CurrentTabIndex = -1
+ }
+ );
}
}
}
@@ -249,6 +253,16 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
Dispatcher.UIThread.Post(() => AddUpscalerTabFromImage(e).SafeFireAndForget());
}
+ private void OnInferenceImageToImageRequested(object? sender, LocalImageFile e)
+ {
+ Dispatcher.UIThread.Post(() => AddImageToImageFromImage(e).SafeFireAndForget());
+ }
+
+ private void OnInferenceImageToVideoRequested(object? sender, LocalImageFile e)
+ {
+ Dispatcher.UIThread.Post(() => AddImageToVideoFromImage(e).SafeFireAndForget());
+ }
+
///
/// Update the database with current tabs
///
@@ -272,7 +286,11 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
entry.IsOpen = tab == SelectedTab;
entry.CurrentTabIndex = i;
- Logger.Trace("SyncTabStatesWithDatabase updated entry for tab '{Title}': {@Entry}", tab.TabTitle, entry);
+ Logger.Trace(
+ "SyncTabStatesWithDatabase updated entry for tab '{Title}': {@Entry}",
+ tab.TabTitle,
+ entry
+ );
await liteDbContext.InferenceProjects.UpsertAsync(entry);
}
}
@@ -287,7 +305,9 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
return;
}
- var entry = await liteDbContext.InferenceProjects.FindOneAsync(p => p.FilePath == projectFile.ToString());
+ var entry = await liteDbContext.InferenceProjects.FindOneAsync(
+ p => p.FilePath == projectFile.ToString()
+ );
// Create if not found
entry ??= new InferenceProjectEntry { Id = Guid.NewGuid(), FilePath = projectFile.ToString() };
@@ -295,7 +315,11 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
entry.IsOpen = tab == SelectedTab;
entry.CurrentTabIndex = Tabs.IndexOf(tab);
- Logger.Trace("SyncTabStatesWithDatabase updated entry for tab '{Title}': {@Entry}", tab.TabTitle, entry);
+ Logger.Trace(
+ "SyncTabStatesWithDatabase updated entry for tab '{Title}': {@Entry}",
+ tab.TabTitle,
+ entry
+ );
await liteDbContext.InferenceProjects.UpsertAsync(entry);
}
@@ -408,7 +432,10 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
return;
}
- await notificationService.TryAsync(ClientManager.CloseAsync(), "Could not disconnect from ComfyUI backend");
+ await notificationService.TryAsync(
+ ClientManager.CloseAsync(),
+ "Could not disconnect from ComfyUI backend"
+ );
}
///
@@ -464,7 +491,11 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
await using var stream = await result.OpenWriteAsync();
stream.SetLength(0); // Overwrite fully
- await JsonSerializer.SerializeAsync(stream, document, new JsonSerializerOptions { WriteIndented = true });
+ await JsonSerializer.SerializeAsync(
+ stream,
+ document,
+ new JsonSerializerOptions { WriteIndented = true }
+ );
}
catch (Exception e)
{
@@ -512,7 +543,11 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
await using var stream = projectFile.Info.OpenWrite();
stream.SetLength(0); // Overwrite fully
- await JsonSerializer.SerializeAsync(stream, document, new JsonSerializerOptions { WriteIndented = true });
+ await JsonSerializer.SerializeAsync(
+ stream,
+ document,
+ new JsonSerializerOptions { WriteIndented = true }
+ );
}
catch (Exception e)
{
@@ -624,6 +659,38 @@ public partial class InferenceViewModel : PageViewModelBase, IAsyncDisposable
await SyncTabStatesWithDatabase();
}
+ private async Task AddImageToImageFromImage(LocalImageFile imageFile)
+ {
+ var imgToImgVm = vmFactory.Get();
+ imgToImgVm.SelectImageCardViewModel.ImageSource = new ImageSource(imageFile.AbsolutePath);
+
+ if (imageFile.GenerationParameters != null)
+ {
+ imgToImgVm.LoadStateFromParameters(imageFile.GenerationParameters);
+ }
+
+ Tabs.Add(imgToImgVm);
+ SelectedTab = imgToImgVm;
+
+ await SyncTabStatesWithDatabase();
+ }
+
+ private async Task AddImageToVideoFromImage(LocalImageFile imageFile)
+ {
+ var imgToVidVm = vmFactory.Get();
+ imgToVidVm.SelectImageCardViewModel.ImageSource = new ImageSource(imageFile.AbsolutePath);
+
+ if (imageFile.GenerationParameters != null)
+ {
+ imgToVidVm.LoadStateFromParameters(imageFile.GenerationParameters);
+ }
+
+ Tabs.Add(imgToVidVm);
+ SelectedTab = imgToVidVm;
+
+ await SyncTabStatesWithDatabase();
+ }
+
///
/// Menu "Open Project" command.
///
diff --git a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
index 99bfca1b..f1186751 100644
--- a/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
@@ -315,6 +315,18 @@ public partial class OutputsPageViewModel : PageViewModelBase
EventManager.Instance.OnInferenceUpscaleRequested(vm.ImageFile);
}
+ public void SendToImageToImage(OutputImageViewModel vm)
+ {
+ navigationService.NavigateTo();
+ EventManager.Instance.OnInferenceImageToImageRequested(vm.ImageFile);
+ }
+
+ public void SendToImageToVideo(OutputImageViewModel vm)
+ {
+ navigationService.NavigateTo();
+ EventManager.Instance.OnInferenceImageToVideoRequested(vm.ImageFile);
+ }
+
public void ClearSelection()
{
foreach (var output in Outputs)
diff --git a/StabilityMatrix.Avalonia/Views/OutputsPage.axaml b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml
index b9f29cfc..22fdbc37 100644
--- a/StabilityMatrix.Avalonia/Views/OutputsPage.axaml
+++ b/StabilityMatrix.Avalonia/Views/OutputsPage.axaml
@@ -219,19 +219,11 @@
IconSource="FullScreenMaximize"
Text="{x:Static lang:Resources.Label_TextToImage}" />
-
+
diff --git a/StabilityMatrix.Core/Helper/EventManager.cs b/StabilityMatrix.Core/Helper/EventManager.cs
index 72a2b3c3..22c09cc2 100644
--- a/StabilityMatrix.Core/Helper/EventManager.cs
+++ b/StabilityMatrix.Core/Helper/EventManager.cs
@@ -37,15 +37,14 @@ public class EventManager
public event EventHandler? ImageFileAdded;
public event EventHandler? InferenceTextToImageRequested;
public event EventHandler? InferenceUpscaleRequested;
+ public event EventHandler? InferenceImageToImageRequested;
+ public event EventHandler? InferenceImageToVideoRequested;
- public void OnGlobalProgressChanged(int progress) =>
- GlobalProgressChanged?.Invoke(this, progress);
+ public void OnGlobalProgressChanged(int progress) => GlobalProgressChanged?.Invoke(this, progress);
- public void OnInstalledPackagesChanged() =>
- InstalledPackagesChanged?.Invoke(this, EventArgs.Empty);
+ public void OnInstalledPackagesChanged() => InstalledPackagesChanged?.Invoke(this, EventArgs.Empty);
- public void OnOneClickInstallFinished(bool skipped) =>
- OneClickInstallFinished?.Invoke(this, skipped);
+ public void OnOneClickInstallFinished(bool skipped) => OneClickInstallFinished?.Invoke(this, skipped);
public void OnTeachingTooltipNeeded() => TeachingTooltipNeeded?.Invoke(this, EventArgs.Empty);
@@ -53,11 +52,9 @@ public class EventManager
public void OnUpdateAvailable(UpdateInfo args) => UpdateAvailable?.Invoke(this, args);
- public void OnPackageLaunchRequested(Guid packageId) =>
- PackageLaunchRequested?.Invoke(this, packageId);
+ public void OnPackageLaunchRequested(Guid packageId) => PackageLaunchRequested?.Invoke(this, packageId);
- public void OnScrollToBottomRequested() =>
- ScrollToBottomRequested?.Invoke(this, EventArgs.Empty);
+ public void OnScrollToBottomRequested() => ScrollToBottomRequested?.Invoke(this, EventArgs.Empty);
public void OnProgressChanged(ProgressItem progress) => ProgressChanged?.Invoke(this, progress);
@@ -83,4 +80,10 @@ public class EventManager
public void OnInferenceUpscaleRequested(LocalImageFile imageFile) =>
InferenceUpscaleRequested?.Invoke(this, imageFile);
+
+ public void OnInferenceImageToImageRequested(LocalImageFile imageFile) =>
+ InferenceImageToImageRequested?.Invoke(this, imageFile);
+
+ public void OnInferenceImageToVideoRequested(LocalImageFile imageFile) =>
+ InferenceImageToVideoRequested?.Invoke(this, imageFile);
}
diff --git a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs
index 76b3ff78..54153635 100644
--- a/StabilityMatrix.Core/Models/Database/LocalImageFile.cs
+++ b/StabilityMatrix.Core/Models/Database/LocalImageFile.cs
@@ -83,7 +83,7 @@ public record LocalImageFile
// TODO: Support other types
const LocalImageFileType imageType = LocalImageFileType.Inference | LocalImageFileType.TextToImage;
- if (filePath.Extension.Contains("webp"))
+ if (filePath.Extension.Equals(".webp", StringComparison.OrdinalIgnoreCase))
{
var paramsJson = ImageMetadata.ReadTextChunkFromWebp(
filePath,
From 8cd3ec1b981d46a8f2829c71b80cf56699f78683 Mon Sep 17 00:00:00 2001
From: JT
Date: Tue, 26 Dec 2023 18:24:05 -0800
Subject: [PATCH 030/116] chagenlog & preview fix
---
CHANGELOG.md | 10 ++++++++++
.../Controls/AdvancedImageBoxView.axaml | 2 +-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c61ffeee..6280e8c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,16 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
+## v2.8.0-dev.2
+### Added
+#### Inference
+- Added Image to Video project type
+#### Output Browser
+- Added support for webp files
+- Added "Send to Image to Image" and "Send to Image to Video" options to the context menu
+### Changed
+- Changed how settings file is written to disk to reduce potential data loss risk
+
## v2.8.0-dev.1
### Added
#### Inference
diff --git a/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml b/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
index 698daab8..73b43f25 100644
--- a/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
+++ b/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml
@@ -21,7 +21,7 @@
+ SourceUri="{Binding LocalFile.FullPath}"/>
From 9275a7ad326a909b022b041aae8c621336661baf Mon Sep 17 00:00:00 2001
From: JT
Date: Tue, 26 Dec 2023 18:31:48 -0800
Subject: [PATCH 031/116] remove unused imageMagick lib
---
StabilityMatrix.Core/StabilityMatrix.Core.csproj | 1 -
1 file changed, 1 deletion(-)
diff --git a/StabilityMatrix.Core/StabilityMatrix.Core.csproj b/StabilityMatrix.Core/StabilityMatrix.Core.csproj
index e2304c75..12b0de97 100644
--- a/StabilityMatrix.Core/StabilityMatrix.Core.csproj
+++ b/StabilityMatrix.Core/StabilityMatrix.Core.csproj
@@ -34,7 +34,6 @@
-
From 7a939db678aaaaf02e0ee0631ffe4e454696c4dd Mon Sep 17 00:00:00 2001
From: ionite34
Date: Thu, 28 Dec 2023 11:24:12 +0800
Subject: [PATCH 032/116] Fix pip install error on macos
---
StabilityMatrix.Core/Python/PyRunner.cs | 19 +++++--------------
1 file changed, 5 insertions(+), 14 deletions(-)
diff --git a/StabilityMatrix.Core/Python/PyRunner.cs b/StabilityMatrix.Core/Python/PyRunner.cs
index b053a281..6e384449 100644
--- a/StabilityMatrix.Core/Python/PyRunner.cs
+++ b/StabilityMatrix.Core/Python/PyRunner.cs
@@ -12,13 +12,7 @@ using StabilityMatrix.Core.Python.Interop;
namespace StabilityMatrix.Core.Python;
[SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Global")]
-public record struct PyVersionInfo(
- int Major,
- int Minor,
- int Micro,
- string ReleaseLevel,
- int Serial
-);
+public record struct PyVersionInfo(int Major, int Minor, int Micro, string ReleaseLevel, int Serial);
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
[Singleton(typeof(IPyRunner))]
@@ -32,8 +26,7 @@ public class PyRunner : IPyRunner
// This is same for all platforms
public const string PythonDirName = "Python310";
- public static string PythonDir =>
- Path.Combine(GlobalConfig.LibraryDir, "Assets", PythonDirName);
+ public static string PythonDir => Path.Combine(GlobalConfig.LibraryDir, "Assets", PythonDirName);
///
/// Path to the Python Linked library relative from the Python directory.
@@ -61,8 +54,7 @@ public class PyRunner : IPyRunner
public static string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc");
- public static string VenvPath =>
- Path.Combine(PythonDir, "Scripts", "virtualenv" + Compat.ExeExtension);
+ public static string VenvPath => Path.Combine(PythonDir, "Scripts", "virtualenv" + Compat.ExeExtension);
public static bool PipInstalled => File.Exists(PipExePath);
public static bool VenvInstalled => File.Exists(VenvPath);
@@ -107,8 +99,7 @@ public class PyRunner : IPyRunner
await RunInThreadWithLock(() =>
{
var sys =
- Py.Import("sys") as PyModule
- ?? throw new NullReferenceException("sys module not found");
+ Py.Import("sys") as PyModule ?? throw new NullReferenceException("sys module not found");
sys.Set("stdout", StdOutStream);
sys.Set("stderr", StdErrStream);
})
@@ -141,7 +132,7 @@ public class PyRunner : IPyRunner
throw new FileNotFoundException("pip not found", PipExePath);
}
var result = await ProcessRunner
- .GetProcessResultAsync(PipExePath, $"install {package}")
+ .GetProcessResultAsync(PythonExePath, $"-m pip install {package}")
.ConfigureAwait(false);
result.EnsureSuccessExitCode();
}
From 868edbffd1dce0f341389dd02b000cb191077366 Mon Sep 17 00:00:00 2001
From: ionite34
Date: Thu, 28 Dec 2023 11:35:11 +0800
Subject: [PATCH 033/116] Allow multiple arguments in pip install args
---
.../ViewModels/Dialogs/PythonPackagesViewModel.cs | 15 +++++----------
1 file changed, 5 insertions(+), 10 deletions(-)
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesViewModel.cs
index b7d00f8e..6f00313b 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/PythonPackagesViewModel.cs
@@ -140,10 +140,7 @@ public partial class PythonPackagesViewModel : ContentDialogViewModelBase
? UpgradePackageVersion(
item.Package.Name,
item.SelectedVersion,
- PythonPackagesItemViewModel.GetKnownIndexUrl(
- item.Package.Name,
- item.SelectedVersion
- ),
+ PythonPackagesItemViewModel.GetKnownIndexUrl(item.Package.Name, item.SelectedVersion),
isDowngrade: item.CanDowngrade
)
: Task.CompletedTask;
@@ -167,9 +164,7 @@ public partial class PythonPackagesViewModel : ContentDialogViewModelBase
Resources.Label_ConfirmQuestion
);
- dialog.PrimaryButtonText = isDowngrade
- ? Resources.Action_Downgrade
- : Resources.Action_Upgrade;
+ dialog.PrimaryButtonText = isDowngrade ? Resources.Action_Downgrade : Resources.Action_Upgrade;
dialog.IsPrimaryButtonEnabled = true;
dialog.DefaultButton = ContentDialogButton.Primary;
dialog.CloseButtonText = Resources.Action_Cancel;
@@ -225,7 +220,7 @@ public partial class PythonPackagesViewModel : ContentDialogViewModelBase
var dialog = DialogHelper.CreateTextEntryDialog("Install Package", "", fields);
var result = await dialog.ShowAsync();
- if (result != ContentDialogResult.Primary || fields[0].Text is not { } packageName)
+ if (result != ContentDialogResult.Primary || fields[0].Text is not { } packageArgs)
{
return;
}
@@ -236,14 +231,14 @@ public partial class PythonPackagesViewModel : ContentDialogViewModelBase
{
VenvDirectory = VenvPath,
WorkingDirectory = VenvPath.Parent,
- Args = new[] { "install", packageName }
+ Args = new ProcessArgs(packageArgs).Prepend("install")
}
};
var runner = new PackageModificationRunner
{
ShowDialogOnStart = true,
- ModificationCompleteMessage = $"Installed Python Package '{packageName}'"
+ ModificationCompleteMessage = $"Installed Python Package '{packageArgs}'"
};
EventManager.Instance.OnPackageInstallProgressAdded(runner);
await runner.ExecuteSteps(steps);
From b0a4f2e82ee1e7ff7806c3ae9ccf51d805037a66 Mon Sep 17 00:00:00 2001
From: ionite34
Date: Thu, 28 Dec 2023 11:38:18 +0800
Subject: [PATCH 034/116] Update CHANGELOG.md
---
CHANGELOG.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 48a70fca..2f95aaa6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,10 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
+## v2.7.7
+### Changed
+- Python Packages install dialog now allows entering multiple arguments or option flags
+
## v2.7.6
### Added
- Added SDXL Turbo and Stable Video Diffusion to the Hugging Face tab
From f94bc50a2ca4062dcebec9af4bfdedd001fac76c Mon Sep 17 00:00:00 2001
From: ionite34
Date: Thu, 28 Dec 2023 11:40:35 +0800
Subject: [PATCH 035/116] Skip URI registration on macos
---
StabilityMatrix.Avalonia/Helpers/UriHandler.cs | 17 ++++++++++++-----
1 file changed, 12 insertions(+), 5 deletions(-)
diff --git a/StabilityMatrix.Avalonia/Helpers/UriHandler.cs b/StabilityMatrix.Avalonia/Helpers/UriHandler.cs
index 75dd4a5f..3b16d078 100644
--- a/StabilityMatrix.Avalonia/Helpers/UriHandler.cs
+++ b/StabilityMatrix.Avalonia/Helpers/UriHandler.cs
@@ -58,17 +58,19 @@ public class UriHandler
public void RegisterUriScheme()
{
+ // Not supported on macos
+
if (Compat.IsWindows)
{
RegisterUriSchemeWin();
}
- else
+ else if (Compat.IsLinux)
{
- // Try to register on unix but ignore errors
+ // Try to register on linux but ignore errors
// Library does not support some distros
try
{
- RegisterUriSchemeUnix();
+ RegisterUriSchemeLinux();
}
catch (Exception e)
{
@@ -98,9 +100,14 @@ public class UriHandler
}
}
- private void RegisterUriSchemeUnix()
+ [SupportedOSPlatform("linux")]
+ private void RegisterUriSchemeLinux()
{
- var service = URISchemeServiceFactory.GetURISchemeSerivce(Scheme, Description, Compat.AppCurrentPath.FullPath);
+ var service = URISchemeServiceFactory.GetURISchemeSerivce(
+ Scheme,
+ Description,
+ Compat.AppCurrentPath.FullPath
+ );
service.Set();
}
}
From b60ccf9f36f788d6ce35beacc44f65b818eab80f Mon Sep 17 00:00:00 2001
From: ionite34
Date: Thu, 28 Dec 2023 12:05:44 +0800
Subject: [PATCH 036/116] Formatting
---
.../Models/Packages/ComfyUI.cs | 47 +++++++++++++++----
1 file changed, 38 insertions(+), 9 deletions(-)
diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
index 81a415c4..5fbef8ac 100644
--- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
+++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
@@ -142,7 +142,14 @@ public class ComfyUI(
public override string MainBranch => "master";
public override IEnumerable AvailableTorchVersions =>
- new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm, TorchVersion.Mps };
+ new[]
+ {
+ TorchVersion.Cpu,
+ TorchVersion.Cuda,
+ TorchVersion.DirectMl,
+ TorchVersion.Rocm,
+ TorchVersion.Mps
+ };
public override async Task InstallPackage(
string installLocation,
@@ -161,7 +168,9 @@ public class ComfyUI(
await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false);
- progress?.Report(new ProgressReport(-1f, "Installing Package Requirements...", isIndeterminate: true));
+ progress?.Report(
+ new ProgressReport(-1f, "Installing Package Requirements...", isIndeterminate: true)
+ );
var pipArgs = new PipInstallArgs();
@@ -181,7 +190,12 @@ public class ComfyUI(
TorchVersion.Cpu => "cpu",
TorchVersion.Cuda => "cu121",
TorchVersion.Rocm => "rocm5.6",
- _ => throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null)
+ _
+ => throw new ArgumentOutOfRangeException(
+ nameof(torchVersion),
+ torchVersion,
+ null
+ )
}
)
};
@@ -239,16 +253,23 @@ public class ComfyUI(
}
}
- public override Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) =>
+ public override Task SetupModelFolders(
+ DirectoryPath installDirectory,
+ SharedFolderMethod sharedFolderMethod
+ ) =>
sharedFolderMethod switch
{
- SharedFolderMethod.Symlink => base.SetupModelFolders(installDirectory, SharedFolderMethod.Symlink),
+ SharedFolderMethod.Symlink
+ => base.SetupModelFolders(installDirectory, SharedFolderMethod.Symlink),
SharedFolderMethod.Configuration => SetupModelFoldersConfig(installDirectory),
SharedFolderMethod.None => Task.CompletedTask,
_ => throw new ArgumentOutOfRangeException(nameof(sharedFolderMethod), sharedFolderMethod, null)
};
- public override Task RemoveModelFolderLinks(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod)
+ public override Task RemoveModelFolderLinks(
+ DirectoryPath installDirectory,
+ SharedFolderMethod sharedFolderMethod
+ )
{
return sharedFolderMethod switch
{
@@ -286,7 +307,9 @@ public class ComfyUI(
throw new Exception("Invalid extra_model_paths.yaml");
}
// check if we have a child called "stability_matrix"
- var stabilityMatrixNode = mappingNode.Children.FirstOrDefault(c => c.Key.ToString() == "stability_matrix");
+ var stabilityMatrixNode = mappingNode.Children.FirstOrDefault(
+ c => c.Key.ToString() == "stability_matrix"
+ );
if (stabilityMatrixNode.Key != null)
{
@@ -337,7 +360,11 @@ public class ComfyUI(
{ "hypernetworks", Path.Combine(modelsDir, "Hypernetwork") },
{
"controlnet",
- string.Join('\n', Path.Combine(modelsDir, "ControlNet"), Path.Combine(modelsDir, "T2IAdapter"))
+ string.Join(
+ '\n',
+ Path.Combine(modelsDir, "ControlNet"),
+ Path.Combine(modelsDir, "T2IAdapter")
+ )
},
{ "clip", Path.Combine(modelsDir, "CLIP") },
{ "clip_vision", Path.Combine(modelsDir, "InvokeClipVision") },
@@ -401,7 +428,9 @@ public class ComfyUI(
mappingNode.Children.Remove("stability_matrix");
- var serializer = new SerializerBuilder().WithNamingConvention(UnderscoredNamingConvention.Instance).Build();
+ var serializer = new SerializerBuilder()
+ .WithNamingConvention(UnderscoredNamingConvention.Instance)
+ .Build();
var yamlData = serializer.Serialize(mappingNode);
await extraPathsYamlPath.WriteAllTextAsync(yamlData).ConfigureAwait(false);
From 661e387a88eccbce6f8959673abb74c37b61d96a Mon Sep 17 00:00:00 2001
From: ionite34
Date: Thu, 28 Dec 2023 12:06:26 +0800
Subject: [PATCH 037/116] Add cross attention method args for comfy
---
StabilityMatrix.Core/Models/Packages/ComfyUI.cs | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
index 5fbef8ac..fdb0fde1 100644
--- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
+++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
@@ -118,6 +118,18 @@ public class ComfyUI(
Options = ["--cpu"]
},
new LaunchOptionDefinition
+ {
+ Name = "Cross Attention Method",
+ Type = LaunchOptionType.Bool,
+ InitialValue = Compat.IsMacOS ? "--use-pytorch-cross-attention" : null,
+ Options =
+ [
+ "--use-split-cross-attention",
+ "--use-quad-cross-attention",
+ "--use-pytorch-cross-attention"
+ ]
+ },
+ new LaunchOptionDefinition
{
Name = "Disable Xformers",
Type = LaunchOptionType.Bool,
From 7d247166eedd25e1b90c7e4946ae55ff390b98e6 Mon Sep 17 00:00:00 2001
From: ionite34
Date: Thu, 28 Dec 2023 12:17:12 +0800
Subject: [PATCH 038/116] Add Mps recommend in BasePackage
---
.../Models/Packages/BasePackage.cs | 35 +++++++++++++++----
1 file changed, 29 insertions(+), 6 deletions(-)
diff --git a/StabilityMatrix.Core/Models/Packages/BasePackage.cs b/StabilityMatrix.Core/Models/Packages/BasePackage.cs
index 063693e7..10aab608 100644
--- a/StabilityMatrix.Core/Models/Packages/BasePackage.cs
+++ b/StabilityMatrix.Core/Models/Packages/BasePackage.cs
@@ -85,11 +85,20 @@ public abstract class BasePackage
public abstract SharedFolderMethod RecommendedSharedFolderMethod { get; }
- public abstract Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod);
+ public abstract Task SetupModelFolders(
+ DirectoryPath installDirectory,
+ SharedFolderMethod sharedFolderMethod
+ );
- public abstract Task UpdateModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod);
+ public abstract Task UpdateModelFolders(
+ DirectoryPath installDirectory,
+ SharedFolderMethod sharedFolderMethod
+ );
- public abstract Task RemoveModelFolderLinks(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod);
+ public abstract Task RemoveModelFolderLinks(
+ DirectoryPath installDirectory,
+ SharedFolderMethod sharedFolderMethod
+ );
public abstract Task SetupOutputFolderLinks(DirectoryPath installDirectory);
public abstract Task RemoveOutputFolderLinks(DirectoryPath installDirectory);
@@ -117,6 +126,11 @@ public abstract class BasePackage
return TorchVersion.DirectMl;
}
+ if (Compat.IsMacOS && Compat.IsArm && AvailableTorchVersions.Contains(TorchVersion.Mps))
+ {
+ return TorchVersion.Mps;
+ }
+
return TorchVersion.Cpu;
}
@@ -142,7 +156,11 @@ public abstract class BasePackage
public abstract Dictionary>? SharedOutputFolders { get; }
public abstract Task GetAllVersionOptions();
- public abstract Task?> GetAllCommits(string branch, int page = 1, int perPage = 10);
+ public abstract Task?> GetAllCommits(
+ string branch,
+ int page = 1,
+ int perPage = 10
+ );
public abstract Task GetLatestVersion(bool includePrerelease = false);
public abstract string MainBranch { get; }
public event EventHandler? Exited;
@@ -153,7 +171,9 @@ public abstract class BasePackage
public void OnStartupComplete(string url) => StartupComplete?.Invoke(this, url);
public virtual PackageVersionType AvailableVersionTypes =>
- ShouldIgnoreReleases ? PackageVersionType.Commit : PackageVersionType.GithubRelease | PackageVersionType.Commit;
+ ShouldIgnoreReleases
+ ? PackageVersionType.Commit
+ : PackageVersionType.GithubRelease | PackageVersionType.Commit;
protected async Task InstallCudaTorch(
PyVenvRunner venvRunner,
@@ -194,6 +214,9 @@ public abstract class BasePackage
{
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CPU", isIndeterminate: true));
- return venvRunner.PipInstall(new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision(), onConsoleOutput);
+ return venvRunner.PipInstall(
+ new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision(),
+ onConsoleOutput
+ );
}
}
From 5e0a9480090acfc72958b34e1a6a945972b69133 Mon Sep 17 00:00:00 2001
From: ionite34
Date: Thu, 28 Dec 2023 12:17:28 +0800
Subject: [PATCH 039/116] Add force fp config for comfy
---
StabilityMatrix.Core/Models/Packages/ComfyUI.cs | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
index fdb0fde1..5352c1b7 100644
--- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
+++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
@@ -130,6 +130,13 @@ public class ComfyUI(
]
},
new LaunchOptionDefinition
+ {
+ Name = "Force Floating Point Precision",
+ Type = LaunchOptionType.Bool,
+ InitialValue = Compat.IsMacOS ? "--force-fp16" : null,
+ Options = ["--force-fp32", "--force-fp16"]
+ },
+ new LaunchOptionDefinition
{
Name = "Disable Xformers",
Type = LaunchOptionType.Bool,
From a125a0bd594742753ad56c41e06f8229424fc48a Mon Sep 17 00:00:00 2001
From: ionite34
Date: Thu, 28 Dec 2023 12:19:15 +0800
Subject: [PATCH 040/116] Add VAE precision to comfy args
---
StabilityMatrix.Core/Models/Packages/ComfyUI.cs | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
index 5352c1b7..517a2032 100644
--- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
+++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
@@ -137,6 +137,12 @@ public class ComfyUI(
Options = ["--force-fp32", "--force-fp16"]
},
new LaunchOptionDefinition
+ {
+ Name = "VAE Precision",
+ Type = LaunchOptionType.Bool,
+ Options = ["--fp16-vae", "--fp32-vae", "--bf16-vae"]
+ },
+ new LaunchOptionDefinition
{
Name = "Disable Xformers",
Type = LaunchOptionType.Bool,
From 384d1f37462cf905b87e6f1c57c47424e242bd9b Mon Sep 17 00:00:00 2001
From: ionite34
Date: Thu, 28 Dec 2023 12:23:26 +0800
Subject: [PATCH 041/116] No set cpu on macos
---
StabilityMatrix.Core/Models/Packages/ComfyUI.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
index 517a2032..18d2e97c 100644
--- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
+++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs
@@ -114,7 +114,8 @@ public class ComfyUI(
{
Name = "Use CPU only",
Type = LaunchOptionType.Bool,
- InitialValue = !HardwareHelper.HasNvidiaGpu() && !HardwareHelper.HasAmdGpu(),
+ InitialValue =
+ !Compat.IsMacOS && !HardwareHelper.HasNvidiaGpu() && !HardwareHelper.HasAmdGpu(),
Options = ["--cpu"]
},
new LaunchOptionDefinition
From d12cab97b16723dcbe8b267913460ac88de35716 Mon Sep 17 00:00:00 2001
From: ionite34
Date: Thu, 28 Dec 2023 12:23:57 +0800
Subject: [PATCH 042/116] Update CHANGELOG.md
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2f95aaa6..dac83e8c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.7.7
+### Added
+- Added ComfyUI launch argument configs: Cross Attention Method, Force Floating Point Precision, VAE Precision
### Changed
- Python Packages install dialog now allows entering multiple arguments or option flags
From 790b2043d6a474d67b2d0e385f1e96c7add7f419 Mon Sep 17 00:00:00 2001
From: Ionite
Date: Wed, 27 Dec 2023 23:37:48 -0500
Subject: [PATCH 043/116] Add model index functions
---
.../DesignData/MockModelIndexService.cs | 17 ++++-
.../Models/CommandItem.cs | 33 +++++++++
.../Views/Settings/MainSettingsPage.axaml | 18 +++++
.../Models/Database/LocalModelFile.cs | 45 +++++++-----
.../Services/IModelIndexService.cs | 18 +++--
.../Services/ModelIndexService.cs | 68 +++++++++++++------
6 files changed, 153 insertions(+), 46 deletions(-)
create mode 100644 StabilityMatrix.Avalonia/Models/CommandItem.cs
diff --git a/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs b/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs
index 3081441c..92bfc564 100644
--- a/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs
+++ b/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database;
@@ -25,9 +26,21 @@ public class MockModelIndexService : IModelIndexService
}
///
- public Task> GetModelsOfType(SharedFolderType type)
+ public Task> FindAsync(SharedFolderType type)
{
- return Task.FromResult>(new List());
+ return Task.FromResult(Enumerable.Empty());
+ }
+
+ ///
+ public Task> FindByHashAsync(string hashBlake3)
+ {
+ return Task.FromResult(Enumerable.Empty());
+ }
+
+ ///
+ public Task RemoveModelAsync(LocalModelFile model)
+ {
+ return Task.FromResult(false);
}
///
diff --git a/StabilityMatrix.Avalonia/Models/CommandItem.cs b/StabilityMatrix.Avalonia/Models/CommandItem.cs
new file mode 100644
index 00000000..5c2089d9
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Models/CommandItem.cs
@@ -0,0 +1,33 @@
+using System.Diagnostics.Contracts;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using System.Windows.Input;
+using StabilityMatrix.Core.Extensions;
+
+namespace StabilityMatrix.Avalonia.Models;
+
+public partial record CommandItem
+{
+ public ICommand Command { get; init; }
+
+ public string DisplayName { get; init; }
+
+ public CommandItem(ICommand command, [CallerArgumentExpression("command")] string? commandName = null)
+ {
+ Command = command;
+ DisplayName = commandName == null ? "" : ProcessName(commandName);
+ }
+
+ [Pure]
+ private static string ProcessName(string name)
+ {
+ name = name.StripEnd("Command");
+
+ name = SpaceTitleCaseRegex().Replace(name, "$1 $2");
+
+ return name;
+ }
+
+ [GeneratedRegex("([a-z])_?([A-Z])")]
+ private static partial Regex SpaceTitleCaseRegex();
+}
diff --git a/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml b/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml
index ccf9c439..abb3a4d0 100644
--- a/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml
+++ b/StabilityMatrix.Avalonia/Views/Settings/MainSettingsPage.axaml
@@ -20,6 +20,8 @@
xmlns:update="clr-namespace:StabilityMatrix.Core.Models.Update;assembly=StabilityMatrix.Core"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vmSettings="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Settings"
+ xmlns:input="clr-namespace:System.Windows.Input;assembly=System.ObjectModel"
+ xmlns:ct="clr-namespace:CommunityToolkit.Mvvm.Input;assembly=CommunityToolkit.Mvvm"
d:DataContext="{x:Static mocks:DesignData.MainSettingsViewModel}"
d:DesignHeight="700"
d:DesignWidth="800"
@@ -566,6 +568,22 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Core/Models/Database/LocalModelFile.cs b/StabilityMatrix.Core/Models/Database/LocalModelFile.cs
index 462b9def..01db400b 100644
--- a/StabilityMatrix.Core/Models/Database/LocalModelFile.cs
+++ b/StabilityMatrix.Core/Models/Database/LocalModelFile.cs
@@ -6,13 +6,13 @@ namespace StabilityMatrix.Core.Models.Database;
///
/// Represents a locally indexed model file.
///
-public class LocalModelFile
+public record LocalModelFile
{
///
/// Relative path to the file from the root model directory.
///
[BsonId]
- public required string RelativePath { get; set; }
+ public required string RelativePath { get; init; }
///
/// Type of the model file.
@@ -53,20 +53,10 @@ public class LocalModelFile
public string RelativePathFromSharedFolder =>
Path.GetRelativePath(SharedFolderType.GetStringValue(), RelativePath);
- public string GetFullPath(string rootModelDirectory)
- {
- return Path.Combine(rootModelDirectory, RelativePath);
- }
-
- public string? GetPreviewImageFullPath(string rootModelDirectory)
- {
- if (PreviewImageFullPath != null)
- return PreviewImageFullPath;
-
- return PreviewImageRelativePath == null
- ? null
- : Path.Combine(rootModelDirectory, PreviewImageRelativePath);
- }
+ ///
+ /// Blake3 hash of the file.
+ ///
+ public string? HashBlake3 => ConnectedModelInfo?.Hashes.BLAKE3;
[BsonIgnore]
public string FullPathGlobal => GetFullPath(GlobalConfig.LibraryDir.JoinDir("Models"));
@@ -75,6 +65,10 @@ public class LocalModelFile
public string? PreviewImageFullPathGlobal =>
PreviewImageFullPath ?? GetPreviewImageFullPath(GlobalConfig.LibraryDir.JoinDir("Models"));
+ [BsonIgnore]
+ public Uri? PreviewImageUriGlobal =>
+ PreviewImageFullPathGlobal == null ? null : new Uri(PreviewImageFullPathGlobal);
+
[BsonIgnore]
public string DisplayModelName => ConnectedModelInfo?.ModelName ?? FileNameWithoutExtension;
@@ -84,7 +78,22 @@ public class LocalModelFile
[BsonIgnore]
public string DisplayModelFileName => FileName;
- protected bool Equals(LocalModelFile other)
+ public string GetFullPath(string rootModelDirectory)
+ {
+ return Path.Combine(rootModelDirectory, RelativePath);
+ }
+
+ public string? GetPreviewImageFullPath(string rootModelDirectory)
+ {
+ if (PreviewImageFullPath != null)
+ return PreviewImageFullPath;
+
+ return PreviewImageRelativePath == null
+ ? null
+ : Path.Combine(rootModelDirectory, PreviewImageRelativePath);
+ }
+
+ /*protected bool Equals(LocalModelFile other)
{
return RelativePath == other.RelativePath;
}
@@ -105,7 +114,7 @@ public class LocalModelFile
public override int GetHashCode()
{
return RelativePath.GetHashCode();
- }
+ }*/
public static readonly HashSet SupportedCheckpointExtensions =
[
diff --git a/StabilityMatrix.Core/Services/IModelIndexService.cs b/StabilityMatrix.Core/Services/IModelIndexService.cs
index 83c7940b..69013b5b 100644
--- a/StabilityMatrix.Core/Services/IModelIndexService.cs
+++ b/StabilityMatrix.Core/Services/IModelIndexService.cs
@@ -12,18 +12,28 @@ public interface IModelIndexService
///
Task RefreshIndex();
+ ///
+ /// Starts a background task to refresh the local model file index.
+ ///
+ void BackgroundRefreshIndex();
+
///
/// Get all models of the specified type from the existing (in-memory) index.
///
IEnumerable GetFromModelIndex(SharedFolderType types);
///
- /// Get all models of the specified type from the existing index.
+ /// Find all models of the specified SharedFolderType.
///
- Task> GetModelsOfType(SharedFolderType type);
+ Task> FindAsync(SharedFolderType type);
///
- /// Starts a background task to refresh the local model file index.
+ /// Find all models with the specified Blake3 hash.
///
- void BackgroundRefreshIndex();
+ Task> FindByHashAsync(string hashBlake3);
+
+ ///
+ /// Remove a model from the index.
+ ///
+ Task RemoveModelAsync(LocalModelFile model);
}
diff --git a/StabilityMatrix.Core/Services/ModelIndexService.cs b/StabilityMatrix.Core/Services/ModelIndexService.cs
index 67a67669..bd0182b3 100644
--- a/StabilityMatrix.Core/Services/ModelIndexService.cs
+++ b/StabilityMatrix.Core/Services/ModelIndexService.cs
@@ -1,6 +1,9 @@
-using System.Diagnostics;
+using System.Collections.Concurrent;
+using System.Collections.Immutable;
+using System.Diagnostics;
using System.Text;
using AsyncAwaitBestPractices;
+using AutoCtor;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Database;
@@ -13,45 +16,44 @@ using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Core.Services;
[Singleton(typeof(IModelIndexService))]
-public class ModelIndexService : IModelIndexService
+[AutoConstruct]
+public partial class ModelIndexService : IModelIndexService
{
private readonly ILogger logger;
- private readonly ILiteDbContext liteDbContext;
private readonly ISettingsManager settingsManager;
+ private readonly ILiteDbContext liteDbContext;
public Dictionary> ModelIndex { get; private set; } = new();
- public ModelIndexService(
- ILogger logger,
- ILiteDbContext liteDbContext,
- ISettingsManager settingsManager
- )
+ [AutoPostConstruct]
+ private void Initialize()
{
- this.logger = logger;
- this.liteDbContext = liteDbContext;
- this.settingsManager = settingsManager;
+ // Start background index when library dir is set
+ settingsManager.RegisterOnLibraryDirSet(_ => BackgroundRefreshIndex());
}
- ///
- /// Deletes all entries in the local model file index.
- ///
- private async Task ClearIndex()
+ public IEnumerable GetFromModelIndex(SharedFolderType types)
{
- await liteDbContext.LocalModelFiles.DeleteAllAsync().ConfigureAwait(false);
+ return ModelIndex.Where(kvp => (kvp.Key & types) != 0).SelectMany(kvp => kvp.Value);
}
- public IEnumerable GetFromModelIndex(SharedFolderType types)
+ ///
+ public async Task> FindAsync(SharedFolderType type)
{
- return ModelIndex.Where(kvp => (kvp.Key & types) != 0).SelectMany(kvp => kvp.Value);
+ await liteDbContext.LocalModelFiles.EnsureIndexAsync(m => m.SharedFolderType).ConfigureAwait(false);
+
+ return await liteDbContext
+ .LocalModelFiles.FindAsync(m => m.SharedFolderType == type)
+ .ConfigureAwait(false);
}
///
- public async Task> GetModelsOfType(SharedFolderType type)
+ public async Task> FindByHashAsync(string hashBlake3)
{
+ await liteDbContext.LocalModelFiles.EnsureIndexAsync(m => m.HashBlake3).ConfigureAwait(false);
+
return await liteDbContext
- .LocalModelFiles.Query()
- .Where(m => m.SharedFolderType == type)
- .ToArrayAsync()
+ .LocalModelFiles.FindAsync(m => m.HashBlake3 == hashBlake3)
.ConfigureAwait(false);
}
@@ -86,6 +88,7 @@ public class ModelIndexService : IModelIndexService
var added = 0;
var newIndex = new Dictionary>();
+
foreach (
var file in modelsDir
.Info.EnumerateFiles("*.*", SearchOption.AllDirectories)
@@ -161,6 +164,7 @@ public class ModelIndexService : IModelIndexService
// Add to index
var list = newIndex.GetOrAdd(sharedFolderType);
list.Add(localModel);
+
added++;
}
@@ -195,4 +199,24 @@ public class ModelIndexService : IModelIndexService
logger.LogError(ex, "Error in background model indexing");
});
}
+
+ ///
+ public async Task RemoveModelAsync(LocalModelFile model)
+ {
+ // Remove from database
+ if (await liteDbContext.LocalModelFiles.DeleteAsync(model.RelativePath).ConfigureAwait(false))
+ {
+ // Remove from index
+ if (ModelIndex.TryGetValue(model.SharedFolderType, out var list))
+ {
+ list.Remove(model);
+ }
+
+ EventManager.Instance.OnModelIndexChanged();
+
+ return true;
+ }
+
+ return false;
+ }
}
From a69177337521cb7d014c6deb1f6b068f5947d1d7 Mon Sep 17 00:00:00 2001
From: ionite34
Date: Thu, 28 Dec 2023 12:59:35 +0800
Subject: [PATCH 044/116] Add AutoCtor
---
StabilityMatrix.Core/StabilityMatrix.Core.csproj | 1 +
1 file changed, 1 insertion(+)
diff --git a/StabilityMatrix.Core/StabilityMatrix.Core.csproj b/StabilityMatrix.Core/StabilityMatrix.Core.csproj
index 12b0de97..44f76ba6 100644
--- a/StabilityMatrix.Core/StabilityMatrix.Core.csproj
+++ b/StabilityMatrix.Core/StabilityMatrix.Core.csproj
@@ -17,6 +17,7 @@
+
From b9efeb46b28cd92836da6f0aba5416f0a605d1da Mon Sep 17 00:00:00 2001
From: ionite34
Date: Thu, 28 Dec 2023 13:01:59 +0800
Subject: [PATCH 045/116] Add find local model from index debug command
---
.../Settings/MainSettingsViewModel.cs | 66 +++++++++++++++++++
1 file changed, 66 insertions(+)
diff --git a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
index bf241a12..7f39fb44 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
@@ -48,6 +48,7 @@ using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models;
+using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Python;
@@ -754,6 +755,71 @@ public partial class MainSettingsViewModel : PageViewModelBase
}
#endregion
+ #region Debug Commands
+
+ public CommandItem[] DebugCommands => [new CommandItem(DebugFindLocalModelFromIndexCommand)];
+
+ [RelayCommand]
+ private async Task DebugFindLocalModelFromIndex()
+ {
+ var textFields = new TextBoxField[]
+ {
+ new() { Label = "Blake3 Hash" },
+ new() { Label = "SharedFolderType" }
+ };
+
+ var dialog = DialogHelper.CreateTextEntryDialog("Find Local Model", "", textFields);
+
+ if (await dialog.ShowAsync() == ContentDialogResult.Primary)
+ {
+ Func>> modelGetter;
+
+ if (textFields.ElementAtOrDefault(0)?.Text is { } hash && !string.IsNullOrWhiteSpace(hash))
+ {
+ modelGetter = () => modelIndexService.FindByHashAsync(hash);
+ }
+ else if (textFields.ElementAtOrDefault(1)?.Text is { } type && !string.IsNullOrWhiteSpace(type))
+ {
+ modelGetter = () => modelIndexService.FindAsync(Enum.Parse(type));
+ }
+ else
+ {
+ return;
+ }
+
+ var timer = Stopwatch.StartNew();
+
+ var result = (await modelGetter()).ToImmutableArray();
+
+ timer.Stop();
+
+ if (result.Length != 0)
+ {
+ await DialogHelper
+ .CreateMarkdownDialog(
+ string.Join(
+ "\n\n",
+ result.Select(
+ (model, i) =>
+ $"[{i + 1}] {model.RelativePath.ToRepr()} "
+ + $"({model.DisplayModelName}, {model.DisplayModelVersion})"
+ )
+ ),
+ $"Found Models ({CodeTimer.FormatTime(timer.Elapsed)})"
+ )
+ .ShowAsync();
+ }
+ else
+ {
+ await DialogHelper
+ .CreateMarkdownDialog(":(", $"No models found ({CodeTimer.FormatTime(timer.Elapsed)})")
+ .ShowAsync();
+ }
+ }
+ }
+
+ #endregion
+
#region Info Section
public void OnVersionClick()
From 24ad22463f644562769921d9e1bdd32cb6a8ddc3 Mon Sep 17 00:00:00 2001
From: JT
Date: Wed, 27 Dec 2023 22:58:59 -0800
Subject: [PATCH 046/116] Added Delete button to model browser
SelectModelVersionDialog & added 'copy link to clipboard' for connected
models
---
CHANGELOG.md | 2 +
.../CheckpointManager/CheckpointFile.cs | 39 +++++--
.../Dialogs/ModelVersionViewModel.cs | 38 +++++--
.../Dialogs/SelectModelVersionViewModel.cs | 102 +++++++++++++++++-
.../Views/CheckpointsPage.axaml | 17 ++-
.../Dialogs/SelectModelVersionDialog.axaml | 24 +++++
6 files changed, 202 insertions(+), 20 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bbb88206..383a00d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
## v2.8.0-dev.3
### Added
- Added ComfyUI launch argument configs: Cross Attention Method, Force Floating Point Precision, VAE Precision
+- Added Delete button to the CivitAI Model Browser details dialog
+- Added "Copy Link to Clipboard" for connected models in the Checkpoints page
### Changed
- Python Packages install dialog now allows entering multiple arguments or option flags
diff --git a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
index 333a3a02..33ed693d 100644
--- a/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
@@ -70,7 +70,14 @@ public partial class CheckpointFile : ViewModelBase
public ObservableCollection Badges { get; set; } = new();
- public static readonly string[] SupportedCheckpointExtensions = { ".safetensors", ".pt", ".ckpt", ".pth", ".bin" };
+ public static readonly string[] SupportedCheckpointExtensions =
+ {
+ ".safetensors",
+ ".pt",
+ ".ckpt",
+ ".pth",
+ ".bin"
+ };
private static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".gif" };
private static readonly string[] SupportedMetadataExtensions = { ".json" };
@@ -95,7 +102,9 @@ public partial class CheckpointFile : ViewModelBase
{
if (string.IsNullOrEmpty(FilePath))
{
- throw new InvalidOperationException("Cannot get connected model info file path when FilePath is empty");
+ throw new InvalidOperationException(
+ "Cannot get connected model info file path when FilePath is empty"
+ );
}
var modelNameNoExt = Path.GetFileNameWithoutExtension((string?)FilePath);
var modelDir = Path.GetDirectoryName((string?)FilePath) ?? "";
@@ -224,10 +233,21 @@ public partial class CheckpointFile : ViewModelBase
return App.Clipboard.SetTextAsync(words);
}
+ [RelayCommand]
+ private Task CopyModelUrl()
+ {
+ return ConnectedModel == null
+ ? Task.CompletedTask
+ : App.Clipboard.SetTextAsync($"https://civitai.com/models/{ConnectedModel.ModelId}");
+ }
+
[RelayCommand]
private async Task FindConnectedMetadata(bool forceReimport = false)
{
- if (App.Services.GetService(typeof(IMetadataImportService)) is not IMetadataImportService importService)
+ if (
+ App.Services.GetService(typeof(IMetadataImportService))
+ is not IMetadataImportService importService
+ )
return;
IsLoading = true;
@@ -298,7 +318,9 @@ public partial class CheckpointFile : ViewModelBase
}
checkpointFile.PreviewImagePath = SupportedImageExtensions
- .Select(ext => Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}"))
+ .Select(
+ ext => Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}")
+ )
.Where(File.Exists)
.FirstOrDefault();
@@ -324,7 +346,11 @@ public partial class CheckpointFile : ViewModelBase
)
continue;
- var checkpointFile = new CheckpointFile { Title = Path.GetFileNameWithoutExtension(file), FilePath = file };
+ var checkpointFile = new CheckpointFile
+ {
+ Title = Path.GetFileNameWithoutExtension(file),
+ FilePath = file
+ };
var jsonPath = Path.Combine(
Path.GetDirectoryName(file) ?? "",
@@ -432,5 +458,6 @@ public partial class CheckpointFile : ViewModelBase
}
}
- public static IEqualityComparer FilePathComparer { get; } = new FilePathEqualityComparer();
+ public static IEqualityComparer FilePathComparer { get; } =
+ new FilePathEqualityComparer();
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ModelVersionViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/ModelVersionViewModel.cs
index 17740c40..73922420 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ModelVersionViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/ModelVersionViewModel.cs
@@ -8,20 +8,42 @@ namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
public partial class ModelVersionViewModel : ObservableObject
{
- [ObservableProperty] private CivitModelVersion modelVersion;
- [ObservableProperty] private ObservableCollection civitFileViewModels;
- [ObservableProperty] private bool isInstalled;
+ private readonly HashSet installedModelHashes;
+
+ [ObservableProperty]
+ private CivitModelVersion modelVersion;
+
+ [ObservableProperty]
+ private ObservableCollection civitFileViewModels;
+
+ [ObservableProperty]
+ private bool isInstalled;
public ModelVersionViewModel(HashSet installedModelHashes, CivitModelVersion modelVersion)
{
+ this.installedModelHashes = installedModelHashes;
ModelVersion = modelVersion;
- IsInstalled = ModelVersion.Files?.Any(file =>
- file is {Type: CivitFileType.Model, Hashes.BLAKE3: not null} &&
- installedModelHashes.Contains(file.Hashes.BLAKE3)) ?? false;
+ IsInstalled =
+ ModelVersion.Files?.Any(
+ file =>
+ file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null }
+ && installedModelHashes.Contains(file.Hashes.BLAKE3)
+ ) ?? false;
CivitFileViewModels = new ObservableCollection(
- ModelVersion.Files?.Select(file => new CivitFileViewModel(installedModelHashes, file)) ??
- new List());
+ ModelVersion.Files?.Select(file => new CivitFileViewModel(installedModelHashes, file))
+ ?? new List()
+ );
+ }
+
+ public void RefreshInstallStatus()
+ {
+ IsInstalled =
+ ModelVersion.Files?.Any(
+ file =>
+ file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null }
+ && installedModelHashes.Contains(file.Hashes.BLAKE3)
+ ) ?? false;
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
index 24b4327e..a7d2ff58 100644
--- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
@@ -3,11 +3,16 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
+using System.Threading.Tasks;
+using Avalonia.Controls.Notifications;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
+using StabilityMatrix.Avalonia.Controls;
+using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models;
+using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
@@ -22,6 +27,8 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
{
private readonly ISettingsManager settingsManager;
private readonly IDownloadService downloadService;
+ private readonly IModelIndexService modelIndexService;
+ private readonly INotificationService notificationService;
public required ContentDialog Dialog { get; set; }
public required IReadOnlyList Versions { get; set; }
@@ -58,10 +65,17 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
public int DisplayedPageNumber => SelectedImageIndex + 1;
- public SelectModelVersionViewModel(ISettingsManager settingsManager, IDownloadService downloadService)
+ public SelectModelVersionViewModel(
+ ISettingsManager settingsManager,
+ IDownloadService downloadService,
+ IModelIndexService modelIndexService,
+ INotificationService notificationService
+ )
{
this.settingsManager = settingsManager;
this.downloadService = downloadService;
+ this.modelIndexService = modelIndexService;
+ this.notificationService = notificationService;
}
public override void OnLoaded()
@@ -101,6 +115,8 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
partial void OnSelectedFileChanged(CivitFileViewModel? value)
{
+ if (value is { IsInstalled: true }) { }
+
var canImport = true;
if (settingsManager.IsLibraryDirSet)
{
@@ -129,6 +145,90 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
Dialog.Hide(ContentDialogResult.Primary);
}
+ public async Task Delete()
+ {
+ if (SelectedFile == null)
+ return;
+
+ var fileToDelete = SelectedFile;
+ var originalSelectedVersionVm = SelectedVersionViewModel;
+
+ var hash = fileToDelete.CivitFile.Hashes.BLAKE3;
+ if (string.IsNullOrWhiteSpace(hash))
+ {
+ notificationService.Show(
+ "Error deleting file",
+ "Could not delete model, hash is missing.",
+ NotificationType.Error
+ );
+ return;
+ }
+
+ var matchingModels = (await modelIndexService.FindByHashAsync(hash)).ToList();
+
+ if (matchingModels.Count == 0)
+ {
+ await modelIndexService.RefreshIndex();
+ matchingModels = (await modelIndexService.FindByHashAsync(hash)).ToList();
+
+ if (matchingModels.Count == 0)
+ {
+ notificationService.Show(
+ "Error deleting file",
+ "Could not delete model, model not found in index.",
+ NotificationType.Error
+ );
+ return;
+ }
+ }
+
+ var dialog = new BetterContentDialog
+ {
+ Title = Resources.Label_AreYouSure,
+ MaxDialogWidth = 750,
+ MaxDialogHeight = 850,
+ PrimaryButtonText = Resources.Action_Yes,
+ IsPrimaryButtonEnabled = true,
+ IsSecondaryButtonEnabled = false,
+ CloseButtonText = Resources.Action_Cancel,
+ DefaultButton = ContentDialogButton.Close,
+ Content =
+ $"The following files:\n{string.Join('\n', matchingModels.Select(x => $"- {x.FileName}"))}\n"
+ + "and all associated metadata files will be deleted. Are you sure?",
+ };
+
+ var result = await dialog.ShowAsync();
+ if (result == ContentDialogResult.Primary)
+ {
+ foreach (var localModel in matchingModels)
+ {
+ var checkpointPath = new FilePath(localModel.GetFullPath(settingsManager.ModelsDirectory));
+ if (File.Exists(checkpointPath))
+ {
+ File.Delete(checkpointPath);
+ }
+
+ var previewPath = localModel.GetPreviewImageFullPath(settingsManager.ModelsDirectory);
+ if (File.Exists(previewPath))
+ {
+ File.Delete(previewPath);
+ }
+
+ var cmInfoPath = checkpointPath.ToString().Replace(checkpointPath.Extension, ".cm-info.json");
+ if (File.Exists(cmInfoPath))
+ {
+ File.Delete(cmInfoPath);
+ }
+
+ await modelIndexService.RemoveModelAsync(localModel);
+ }
+
+ settingsManager.Transaction(settings => settings.InstalledModelHashes?.Remove(hash));
+ fileToDelete.IsInstalled = false;
+ originalSelectedVersionVm?.RefreshInstallStatus();
+ }
+ }
+
public void PreviousImage()
{
if (SelectedImageIndex > 0)
diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
index a044be5d..021dc27f 100644
--- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
+++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
@@ -61,15 +61,19 @@
-
-
+
+
+
True
+
+
diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml
index 632c8fd4..f118e90e 100644
--- a/StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml
+++ b/StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml
@@ -12,6 +12,8 @@
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:markupExtensions="clr-namespace:StabilityMatrix.Avalonia.MarkupExtensions"
+ xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:input="clr-namespace:FluentAvalonia.UI.Input;assembly=FluentAvalonia"
d:DataContext="{x:Static designData:DesignData.SelectModelVersionViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.SelectModelVersionDialog">
@@ -21,6 +23,11 @@
MinWidth="700"
RowDefinitions="Auto, *, Auto"
ColumnDefinitions="*,Auto,*">
+
+
+
+
+
+
+
+
+
+
+
From 1f6c9471023fc8a67276cc42bea27e60104e56a9 Mon Sep 17 00:00:00 2001
From: JT
Date: Thu, 28 Dec 2023 01:18:11 -0800
Subject: [PATCH 047/116] add onetrainer & moved tk/tcl env vars to
BaseGitPackage
---
CHANGELOG.md | 1 +
.../Models/Packages/BaseGitPackage.cs | 17 +++-
.../Models/Packages/KohyaSs.cs | 32 ++-----
.../Models/Packages/OneTrainer.cs | 92 +++++++++++++++++++
4 files changed, 117 insertions(+), 25 deletions(-)
create mode 100644 StabilityMatrix.Core/Models/Packages/OneTrainer.cs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 383a00d6..ba629a63 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
## v2.8.0-dev.3
### Added
+- Added new package: [OneTrainer](https://github.com/Nerogar/OneTrainer)
- Added ComfyUI launch argument configs: Cross Attention Method, Force Floating Point Precision, VAE Precision
- Added Delete button to the CivitAI Model Browser details dialog
- Added "Copy Link to Clipboard" for connected models in the Checkpoints page
diff --git a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs
index 373c19f4..ebe7856f 100644
--- a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs
+++ b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs
@@ -2,6 +2,7 @@
using System.IO.Compression;
using NLog;
using Octokit;
+using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models.Database;
@@ -147,10 +148,24 @@ public abstract class BaseGitPackage : BasePackage
await VenvRunner.DisposeAsync().ConfigureAwait(false);
}
+ // Set additional required environment variables
+ var env = new Dictionary();
+ if (SettingsManager.Settings.EnvironmentVariables is not null)
+ {
+ env.Update(SettingsManager.Settings.EnvironmentVariables);
+ }
+
+ if (Compat.IsWindows)
+ {
+ var tkPath = Path.Combine(SettingsManager.LibraryDir, "Assets", "Python310", "tcl", "tcl8.6");
+ env["TCL_LIBRARY"] = tkPath;
+ env["TK_LIBRARY"] = tkPath;
+ }
+
VenvRunner = new PyVenvRunner(venvPath)
{
WorkingDirectory = installedPackagePath,
- EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables,
+ EnvironmentVariables = env
};
if (!VenvRunner.Exists() || forceRecreate)
diff --git a/StabilityMatrix.Core/Models/Packages/KohyaSs.cs b/StabilityMatrix.Core/Models/Packages/KohyaSs.cs
index fa3adb34..2ca3cdc1 100644
--- a/StabilityMatrix.Core/Models/Packages/KohyaSs.cs
+++ b/StabilityMatrix.Core/Models/Packages/KohyaSs.cs
@@ -40,14 +40,16 @@ public class KohyaSs(
public override TorchVersion GetRecommendedTorchVersion() => TorchVersion.Cuda;
- public override string Disclaimer => "Nvidia GPU with at least 8GB VRAM is recommended. May be unstable on Linux.";
+ public override string Disclaimer =>
+ "Nvidia GPU with at least 8GB VRAM is recommended. May be unstable on Linux.";
public override PackageDifficulty InstallerSortOrder => PackageDifficulty.UltraNightmare;
public override bool OfferInOneClickInstaller => false;
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.None;
- public override IEnumerable AvailableTorchVersions => new[] { TorchVersion.Cuda };
- public override IEnumerable AvailableSharedFolderMethods => new[] { SharedFolderMethod.None };
+ public override IEnumerable AvailableTorchVersions => [TorchVersion.Cuda];
+ public override IEnumerable AvailableSharedFolderMethods =>
+ new[] { SharedFolderMethod.None };
public override List LaunchOptions =>
[
@@ -189,7 +191,9 @@ public class KohyaSs(
"""
);
- var replacementAcceleratePath = Compat.IsWindows ? @".\venv\scripts\accelerate" : "./venv/bin/accelerate";
+ var replacementAcceleratePath = Compat.IsWindows
+ ? @".\venv\scripts\accelerate"
+ : "./venv/bin/accelerate";
var replacer = scope.InvokeMethod(
"StringReplacer",
@@ -238,7 +242,6 @@ public class KohyaSs(
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}";
- VenvRunner.EnvironmentVariables = GetEnvVars(installedPackagePath);
VenvRunner.RunDetached(args.TrimEnd(), HandleConsoleOutput, OnExit);
}
@@ -246,23 +249,4 @@ public class KohyaSs(
public override Dictionary>? SharedOutputFolders { get; }
public override string MainBranch => "master";
-
- private Dictionary GetEnvVars(string installDirectory)
- {
- // Set additional required environment variables
- var env = new Dictionary();
- if (SettingsManager.Settings.EnvironmentVariables is not null)
- {
- env.Update(SettingsManager.Settings.EnvironmentVariables);
- }
-
- if (!Compat.IsWindows)
- return env;
-
- var tkPath = Path.Combine(SettingsManager.LibraryDir, "Assets", "Python310", "tcl", "tcl8.6");
- env["TCL_LIBRARY"] = tkPath;
- env["TK_LIBRARY"] = tkPath;
-
- return env;
- }
}
diff --git a/StabilityMatrix.Core/Models/Packages/OneTrainer.cs b/StabilityMatrix.Core/Models/Packages/OneTrainer.cs
new file mode 100644
index 00000000..fd339fa4
--- /dev/null
+++ b/StabilityMatrix.Core/Models/Packages/OneTrainer.cs
@@ -0,0 +1,92 @@
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+using StabilityMatrix.Core.Attributes;
+using StabilityMatrix.Core.Helper;
+using StabilityMatrix.Core.Helper.Cache;
+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 OneTrainer(
+ IGithubApiCache githubApi,
+ ISettingsManager settingsManager,
+ IDownloadService downloadService,
+ IPrerequisiteHelper prerequisiteHelper
+) : BaseGitPackage(githubApi, settingsManager, downloadService, prerequisiteHelper)
+{
+ public override string Name => "OneTrainer";
+ public override string DisplayName { get; set; } = "OneTrainer";
+ public override string Author => "Nerogar";
+ public override string Blurb =>
+ "OneTrainer is a one-stop solution for all your stable diffusion training needs";
+ public override string LicenseType => "AGPL-3.0";
+ public override string LicenseUrl => "https://github.com/Nerogar/OneTrainer/blob/master/LICENSE.txt";
+ public override string LaunchCommand => "scripts/train_ui.py";
+
+ public override Uri PreviewImageUri =>
+ new("https://github.com/Nerogar/OneTrainer/blob/master/resources/icons/icon.png?raw=true");
+
+ public override string OutputFolderName => string.Empty;
+ public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.None;
+ public override IEnumerable AvailableTorchVersions => [TorchVersion.Cuda];
+ public override IEnumerable AvailableSharedFolderMethods =>
+ new[] { SharedFolderMethod.None };
+ public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Nightmare;
+ public override bool OfferInOneClickInstaller => false;
+ public override bool ShouldIgnoreReleases => true;
+
+ public override async Task InstallPackage(
+ string installLocation,
+ TorchVersion torchVersion,
+ SharedFolderMethod selectedSharedFolderMethod,
+ DownloadPackageVersionOptions versionOptions,
+ IProgress? progress = null,
+ Action? onConsoleOutput = null
+ )
+ {
+ progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true));
+
+ await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv"));
+ venvRunner.WorkingDirectory = installLocation;
+ await venvRunner.Setup(true, onConsoleOutput).ConfigureAwait(false);
+
+ progress?.Report(new ProgressReport(-1f, "Installing requirements", isIndeterminate: true));
+
+ var pipArgs = new PipInstallArgs("-r", "requirements.txt");
+ await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false);
+ }
+
+ public override async Task RunPackage(
+ string installedPackagePath,
+ string command,
+ string arguments,
+ Action? onConsoleOutput
+ )
+ {
+ await SetupVenv(installedPackagePath).ConfigureAwait(false);
+ var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}";
+
+ VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit);
+ return;
+
+ void HandleExit(int i)
+ {
+ Debug.WriteLine($"Venv process exited with code {i}");
+ OnExit(i);
+ }
+
+ void HandleConsoleOutput(ProcessOutput s)
+ {
+ onConsoleOutput?.Invoke(s);
+ }
+ }
+
+ public override List LaunchOptions => [LaunchOptionDefinition.Extras];
+ public override Dictionary>? SharedFolders { get; }
+ public override Dictionary>? SharedOutputFolders { get; }
+ public override string MainBranch => "master";
+}
From c0d5e2acd8a1d877b008f01440a9153138e5175e Mon Sep 17 00:00:00 2001
From: ionite34
Date: Fri, 29 Dec 2023 11:17:27 +0800
Subject: [PATCH 048/116] Add macos sign notarize release CIs
---
.github/workflows/release.yml | 109 +++++++++++++++++++++++++++++++++-
Build/build_macos_app.sh | 26 ++++++++
Build/codesign_macos.sh | 21 +++++++
Build/notarize_macos.sh | 32 ++++++++++
4 files changed, 187 insertions(+), 1 deletion(-)
create mode 100755 Build/build_macos_app.sh
create mode 100644 Build/codesign_macos.sh
create mode 100644 Build/notarize_macos.sh
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 88b5eae3..d68aa480 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -171,7 +171,114 @@ jobs:
with:
name: StabilityMatrix-${{ env.platform-id }}
path: ./out/${{ env.out-name }}
-
+
+ release-macos:
+ name: Release (macos-arm64)
+ env:
+ platform-id: osx-arm64
+ app-name: "Stability Matrix.app"
+ out-name: "Stability Matrix.dmg"
+ runs-on: macos-13
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: olegtarasov/get-tag@v2.1.2
+ if: github.event_name == 'release'
+ id: tag_name
+ with:
+ tagRegex: "v(.*)"
+
+ - name: Set Version from Tag
+ if: github.event_name == 'release'
+ run: |
+ echo "Using tag ${{ env.GIT_TAG_NAME }}"
+ echo "RELEASE_VERSION=${{ env.GIT_TAG_NAME }}" >> $env:GITHUB_ENV
+
+ - name: Set Version from manual input
+ if: github.event_name == 'workflow_dispatch'
+ run: |
+ echo "Using version ${{ github.event.inputs.version }}"
+ echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $env:GITHUB_ENV
+
+ - name: Set up .NET 8
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Install dependencies
+ run: dotnet restore
+
+ - name: .NET Msbuild (App)
+ env:
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ run: >
+ dotnet msbuild StabilityMatrix.Avalonia
+ -t:BundleApp -p:UseAppHost=true -p:SelfContained=true
+ -p:Configuration=Release -p:RuntimeIdentifier=${{ env.platform-id }}
+ -p:Version=$env:RELEASE_VERSION
+ -p:PublishDir=out
+ -p:PublishReadyToRun=true
+ -p:CFBundleShortVersionString=$env:RELEASE_VERSION
+ -p:CFBundleName="Stability Matrix"
+ -p:CFBundleDisplayName="Stability Matrix"
+ -p:CFBundleVersion=$env:RELEASE_VERSION
+ -p:SentryOrg=${{ secrets.SENTRY_ORG }} -p:SentryProject=${{ secrets.SENTRY_PROJECT }}
+ -p:SentryUploadSymbols=true -p:SentryUploadSources=true
+
+ - name: Post Build (App)
+ run: >
+ mkdir -p signing
+ mv "./out/Stability Matrix.app" ./signing/${{ env.app-name }}
+
+ - name: Codesign app bundle
+ env:
+ MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
+ MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
+ MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
+ MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
+ run: ./Build/codesign_macos.sh "./signing/${{ env.app-name }}"
+
+ - name: Notarize app bundle
+ env:
+ MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
+ MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
+ MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
+ run: ./Build/notarize_macos.sh "./signing/${{ env.app-name }}"
+
+ - name: Upload Artifact (App)
+ uses: actions/upload-artifact@v2
+ with:
+ name: StabilityMatrix-${{ env.platform-id }}-app
+ path: ./signing/${{ env.app-name }}
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '20.x'
+
+ - name: Install dependencies for dmg creation
+ run: >
+ npm install --global create-dmg
+ brew install graphicsmagick imagemagick
+
+ - name: Create dmg
+ run: >
+ cd signing
+ create-dmg ${{ env.app-name }} --overwrite --identity ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
+ # Rename to remove version number
+ for f in *.dmg; do mv "$f" "${{ env.out-name }}"; done
+
+ - name: Notarize dmg
+ env:
+ MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
+ MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
+ MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
+ run: ./Build/notarize_macos.sh "./signing/${{ env.out-name }}"
+
+ - name: Upload Artifact (dmg)
+ uses: actions/upload-artifact@v2
+ with:
+ name: StabilityMatrix-${{ env.platform-id }}-dmg
+ path: ./signing/${{ env.out-name }}
publish-release:
name: Publish GitHub Release
diff --git a/Build/build_macos_app.sh b/Build/build_macos_app.sh
new file mode 100755
index 00000000..acdfcd4b
--- /dev/null
+++ b/Build/build_macos_app.sh
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+while getopts v: flag
+do
+ case "${flag}" in
+ v) version=${OPTARG};;
+ *) echo "Invalid option";;
+ esac
+done
+
+dotnet \
+msbuild \
+StabilityMatrix.Avalonia \
+-t:BundleApp \
+-p:RuntimeIdentifier=osx-arm64 \
+-p:UseAppHost=true \
+-p:Configuration=Release \
+-p:CFBundleShortVersionString="$version" \
+-p:SelfContained=true \
+-p:CFBundleName="Stability Matrix" \
+-p:CFBundleDisplayName="Stability Matrix" \
+-p:CFBundleVersion="$version" \
+-p:PublishDir="$(pwd)/out/osx-arm64/bin" \
+
+# Copy the app out of bin
+cp -r ./out/osx-arm64/bin/Stability\ Matrix.app ./out/osx-arm64/Stability\ Matrix.app
diff --git a/Build/codesign_macos.sh b/Build/codesign_macos.sh
new file mode 100644
index 00000000..91698b39
--- /dev/null
+++ b/Build/codesign_macos.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+echo "Signing file: $1"
+
+# Turn our base64-encoded certificate back to a regular .p12 file
+
+echo "$MACOS_CERTIFICATE" | base64 --decode -o certificate.p12
+
+# We need to create a new keychain, otherwise using the certificate will prompt
+# with a UI dialog asking for the certificate password, which we can't
+# use in a headless CI environment
+
+security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
+security default-keychain -s build.keychain
+security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
+security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
+security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
+
+# We finally codesign our app bundle, specifying the Hardened runtime option
+
+/usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime "$1" -v
diff --git a/Build/notarize_macos.sh b/Build/notarize_macos.sh
new file mode 100644
index 00000000..0b4d134a
--- /dev/null
+++ b/Build/notarize_macos.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+echo "Notarizing file: $1"
+
+# Store the notarization credentials so that we can prevent a UI password dialog
+# from blocking the CI
+
+echo "Create keychain profile"
+xcrun notarytool store-credentials "notarytool-profile" \
+--apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" \
+--team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" \
+--password "$PROD_MACOS_NOTARIZATION_PWD"
+
+# We can't notarize an app bundle directly, but we need to compress it as an archive.
+# Therefore, we create a zip file containing our app bundle, so that we can send it to the
+# notarization service
+
+echo "Creating temp notarization archive"
+ditto -c -k --keepParent "$1" "notarization.zip"
+
+# Here we send the notarization request to the Apple's Notarization service, waiting for the result.
+# This typically takes a few seconds inside a CI environment, but it might take more depending on the App
+# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
+# you're curious
+
+echo "Notarize app"
+xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait
+
+# Finally, we need to "attach the staple" to our executable, which will allow our app to be
+# validated by macOS even when an internet connection is not available.
+echo "Attach staple"
+xcrun stapler staple "$1"
From d70e5c922ac6d22cddf7768ee6dc5a03dc435d93 Mon Sep 17 00:00:00 2001
From: ionite34
Date: Fri, 29 Dec 2023 11:17:36 +0800
Subject: [PATCH 049/116] Add macos app icon
---
StabilityMatrix.Avalonia/Assets/AppIcon.icns | Bin 0 -> 871612 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 StabilityMatrix.Avalonia/Assets/AppIcon.icns
diff --git a/StabilityMatrix.Avalonia/Assets/AppIcon.icns b/StabilityMatrix.Avalonia/Assets/AppIcon.icns
new file mode 100644
index 0000000000000000000000000000000000000000..079c0b1f0db20f2bb4cef8db60445cfb369bf531
GIT binary patch
literal 871612
zcmb@sRZyKx5TFgg-Q9yb!QI`1yPx3hc5n#p?iSqL-Q8Uda3EN43%2?G`fGPDw)Sc-
zdU|Hwu9>O1c)RE6wl;Tgg+Nj4wl@F94grCK6sf8#gMvti2mt|sA}1@U_D_rcC-AWU
z+DdEZ-G2(wRZT`5qIQ<}^xr@dpdzLd%UEo#kUS@~u*kQw%g58+H
zVzX`)XQDV}VKX$tvzP{Y0(Og7iq>^82BwI3PAhs8Ljrjyr#O5OZc9awsJd)qIWmHM
z(rA~ZUHsT1(_^(~nt{Y~TNm|ovjPmwF{xm$lq7s{RbeiWqb
z`u)i$aa+W)jHZbw9*$*a
z^GKI3Weq(2k|+HNB~fkLWO@Odw_SH9Ql83wu+5jU3~dziA_KT<4wl~dKngWAHF=h*
zRZtc-VOOMX<5h1@J2=0gELhB;tt$y{-=)yoO#016QZPF8NW?#l~VC5>m7r0vnTR
z)`}6UIc>2fLmSUTsgYZ=wv;;daLTO)ECIr}(eKQg+$2FQdURnwP{s3>%2cjI+}%
zBKoVrj~3;{wpfi*q2D1dAD9<^&*(~zFjzvO*!Ef|$KVH;&7!+FckXA>Va9oNT8a2{7
zCU7dWi2YJ4Vn>y|uC9b$FPex@@54#n-`^9FWQFq`;UZhhK6%+G_Ff;Gv9&6HozK2@
zf$@O;U2adI(^jhFTk7SZJ|j%npLeNpeCiWZE0Iy#B+(O1@9&Mw=xoh)I^ZepMg5$K
zskOdmC<1CEJxTC{9*RBdiv_T$qkG)=h{|rB#l~0yG1X(7~i{%6G+ec`TiJn(OtS!v(?P3pc&^&0=6%B($YF=2?}=OB4(|B
zZ5DL*8PxQCBzqtA_TF!J$BcQ_%ah))=B$QY26xGi&GJM+8eQCaoOXYu@W*AR9u7?N
z-DpbPyDL28ao_uvm(rH
z{&ApS>$J)EQGitto1;a0<|e0o-brZk^k>J2+Q{Kdm&6Q}zaG8-7mFRYBuP(M
z)ZN6SKP)lS*Uwh$oF<`8joQLUtX{$*UnNk+;Nf~GUKDNOCA)>?mN^cFI2*8>U%bdr
zry|#Wl&Co!grhXQ&bC7r_oh{Q7`Ny>sFfuqVsve!7^3u(Dh7qJx5oA-zWuHn1(O+1
z%R88A4yZ1(q6zhCi~Bq8A5;ByPm|{gg`4DhLue0H>8#ZBKG;M|>rWytk%g1y9|zCl
zr2vfMOP~}Eo|g4TX=BYgD3D@Pw%j1l#iDcQC6AAfcLOp07;iCsv7JQxqD7OO=t`Rg0CrMXb{>Lo$MjBC(w9W4Xm
zHEht^Q0p=USUK?U0DxKsA>X3L&p$Ou%4Jlf@knWx9*L!@blv_}Jx{{Wl-0SOfo=~F
z7m(26-_Qxui?}kablKs<#7#2N;suxGc$dcVU#1hhe9`1^lAX<{k*ROlf@Lau;eCX}
z!L-tI3fh+9Yz&zFohqB03{y?6Lu!|sv|lKdkB1hCCd`W|+1ZZ6ZugDH7%(yt@+5j1
zul8|66bN2aX(pu!xGM1KQ3MU(wpJiCNM%V;OypG~)x9vs5|`P2h`g2$-{12g++W~T
z%dqRm$n#708Wju52xtD3r^5UOyYFUJv|qBelbSO9RxdmGZP1P4>m3I*{lGiSK*JIb
zjsB8hF!g3XR=SrB^ke~h;~a}Q)kpNxQ0D*}n@bz)q@am>!IevU
z@0RHtsFkH9*LPCU!jwS7a(BcjcGt@#1H;a-bD8ER1dobAO*;peDj80i1*>7NNS4U(
zzz2y^SstT@sg;e!k~VR^KABd#Xt|P<@Y^7+JPDoD2=;F+Ck(Wk^lbSne8ExY*rKn5
zP*#Snn40k>*)*-MY@de}oUo)6=vZDf`e~gaUw5rU{QLSL~2o
zcNsa76_R!_6GZ74z~de_upNC)g6iHbZaBuNYr@BNhfo<@QH(L?*nuvjQWmw7oA!FG
znW8W^p#pl7@rHy6Beo|o0&7CR%sT24HP4h5>qbQ%YUu8Pn*xnWUWH{uOa?SL>(K#6
z69&NKu;(nnWPZm%Q}wM?t^Kbd@Yo6jKEm!LffKa}hpFy)mby0rP0$dZapD4~2{S&t
zlr}YC@V;P~5io6?KTSwYOqtNS$eJR^&UVJ5KvL{K$N{_EUc!&bOu6qzpBZHijac?9
zAHoMVwV4Rx_L1YLx%;9GMc1sPxRpY%!?M)xoyQWnaHIP2qo1hfH{Be>NEz)4BxO#j
zJUyBfv30{QsY>HCjR)b=3@_=yOvmx{78rWdQXBG38QURvFNCIA*OC1*)hwaC!7_}=
zbu2|wguD=!N?zA4#Y{~j)XFoHlQkTul=`o;;y3Ap3NE#v93+Rkks~=dlL+nfg=kAE
z3}eGHw@3wdPAU#xv0z@f3zW|yXrgo!J{5xOvfFk)S>2gP*(3v_FJvcL>nBH*R#Z$hWL2FjG*KmLdLDh(h@0?9fpq$&U
z;2AZ2ek4i@HtJ3gNoDh-a)=XIVAVT!wNlzZ>~TP(zA>@DPST;DgHV+{?`pNyraafi
zGrOeNx2cwPmbyWBcU!`B{&sqmFWI9I)CDsZkv+hKj-A{+ED~;C!iV1+Gl;f7
zLZhMcP}*5RVAjLXekTkSyKLBq@@onYQA&y+PK1h0c+;{dD|s}_?DQaj%P0y0kB%z4
zuM4C;J{dafo3)XkS4%6mSq|o$0*rxX}2w7Cv
za5ho04P8)IK|u`bFMuN&iuZJ4{fvKG7OR1osf%AriS|*ov
z2hSuACxoZUrKFXnutpy3H})TkxvRhDt?@x2?c|a2ybF{gr34jbB+~?75>V4&S$$V@
zwgg733`~Ys!Xy*2{6YW(kRXvUv*begWd6^$Hp%u#EBd|EpUtphfFh>WZja2{cUg6a
zY*@+^rBD<-ZAv7O9UcB$jE2K6JKxWqZLBeizqLb&gyi#mu8bUYiunVrymdM%JCM
zsia%)8*hfi)KMUCy;e!?w*@5m;3|&3V-}pp`Xd3-c^H
z45c!*xMjVb(ITxh!+1cd
zUd6}rf@8}RC!u|~!S&I3>s)0orp8gH5}c%Xn4nP;My0<6d5O+sN~JWplqO}3Im*JQ
z?At3}QzH{nfcc%K`p$Hb{WZe|rBAn#bJMamCqdyv#l(6lTOe2xQGt3EMt*8G(lM>`
zn!9ON{UsCRS<>l*_^l-7+_f);#Y15u2Vc7^pBZY3Av#UhvLka<2~r!14c>(_V-tZJ
zf#P-zJlT=t_xS)I@M%IW_^_
z4W{}o=T1Y|^(A63aBV3UeQ(lw``ghB$ja{<^~OkNj+ejM$BBNi%gqiSWzyutHVY%!
zw%ZmhmgbN@CA3Kva=0s#Ofct9Ok=(BiMCIm+^;chPrFEs@1(#Y^+f^aI}R7cPW0>h
z#|#Gr$_pW{h*0OEXwRq2nMHXR$7<7YbZ)hg`PHUn%ti?|&w+}D
z!wSoN-kvjCg>k#}eg|kGfmOUG&lf)+pf}u$RT$fSAOx;QRy2+rrPj-^gy+u;I_2_fUnY<+;YUUxO|D}(6g1Wsy89?dN$xYk54zb+
z)LFO0rs;+iWJ%-{cdlfCIZw?PT6rNMo0b>WcIfCM3}Scu0kM|5KRzx?!YY&Yb9&go
ze9&dq&|3!C%dJ3L5Z(iaHq@$Vg7P`1QLAp;x})(|ZfeuBi5Z0l{1U_!Tr``(Rz+sg
zG$ELF4P+w$te^wwY>(Sc#RcYtDaWxRZciHu?bmkwvDx?E9>GbH$UEGk
zQ=uc}vU0KVOMjydTw~jxB&C&-Y3RRY(fcQpHIoB#E^6e8))+fZAE2C;{JrRjoR%A8
z@&VoBLatNHZOgE*uqk?sp3WKoT5CT_USu(^HG9m+8>t`M*{N-P`Y8(o_^VbCk}{6p
zh2IvUN)~7JjsH+;Aj|>|Y=9_rJ#iP8X$H`&V%M>%#)ag4kvy{k-{ufx1mj4}BUUq;
zm_w3}Dz2sxu4FR(@Dp;MppS<7nn^qnNNB7umAxf)ZP5g;`!Ao@T}k)PnNH3iA?!?e
zGV*hB-nx2wd-tfn(Hv_8L!Rd4*Gv@p;sN-g=`pGmv=Qs-oK*UM*Gy}AjR100M1K37
zy~Ru)dSp8KzoK7bBAmoFm>%6qWO1IY8Up2j>{QXNQ#vS4J#j0ARRY!)MRsS8lSp}Q
zDyn%HfZSVY7CfV0rvq?g_YyDuDnbv-0cP=ivCv!#NcW&~XT-7c8LOE)h;tk0k?(%T
z!y{$YG-dO|)(45OuaW0zo6ZQu-H{6XSy}a1KP{h%ViT99SuWT7DA5v%W#U~eNQ>i6zvfLjTW7tiUO*7WVBy|=|x;pvKv({_tFWZmgSeI(}K`A_@
zIC#>tJ=cRs@gc`wH*MrqysdgCHTAVzIFNpo2Iq
zGb1~MtG6gs^h&Isn(UTLbdwpBdMC5}5hcJku_t6?d@e`4A8iz9&zet~UuXPQEI0YH
zG{0)?dL71!LHD-(>Astdps+jNqCl(&wym|$S(D`b%f87e~gFd(bgrXgSO8VMRNC
zxc~ibaZwF7#|f29hj`r28iS0z=0qPdo;IoF*l;dpa2
z$BDc4z#dwGn1(2VlwVl*18q*^^G$(PC672SpN9;^p|ZQsl+JK39xo~a7Z;cRI#FZH
zbxz3hb2e814K!2&R1AYAyVyEhlbH-EnmXHFYwY|+vHf?wwYyso222Fs&*yavz1Yt&
zCwS+NJo(k)uy4`n-?duG+J(3olA3@p^Y*r3>RXUaP7%dTL^5E}Gw@YYN&WKx$>y}-
zMalU6I%_#KblH5t9wBqWN>R3Lg>`X{gbwjGFMhqjpBD7A$rJ2HmE0k1949H`D)eRb
ze)zHk3RVyzWeQExg4g6}>Rr29Mo*?1rz1i1bWC^Yb&S>XSNLWhJ+r~VE8pPVd3BRkod
zo1;#dY#bg2L0&YROeYGp4rlU_0@-39kw~Dq$vOB<`~*$VxTj2(bsCwVFq@Zs-8~<)
zUX4;SQ*~1_@B9;A08+g^x~aNn$74BBf3$fHMq_;)x7#sTjU>C%f9RV|ebL8VZ|qNl
zX=SPh1TiDo>6-lCr<~lG66Q`Q_lYT%!`$u{WXNLKNkU6-lmV%aF9{LPk!YuWguncA
zZolxR(Vt2X8kJxePgJP(#{45m64p25(0^!2fl1=l=(K1FII+mc$dba34e{<=TwLxC
zPfu5|BWAe^R?W$ofiJRDiwFjAOWGW&S}f^ZHm&0p?0DrO;F6`&yBn|V(b3V_tG&L@
zHxke7#7_`fK!%J5smFxK?Kou&(b*sJLv0R&7I|NJb=bP!O>@Fq{!2%3ew{~#Qp=`|3k=@6^U#l^+Bww6{e>^3sPTM#{P9%K0E*hgNN9|$pwKD
zL(!uV=L9n+m+2V7$Wbgz@$Du+Hp2>odOK$Da56xxV%eBj1o%d7AGrl}LZvNe=RjOj
z088|#4Fqc!jm42f3w^%bkqxVU!u6l#Ig}fOB~@9WUeXH3bYgB~jy-ApDY&`N=mN&~-N}@OfS+5z%P;GO68IS*Oyp;pNu^69O(s
zv3>LGIXCP<6Mo*;X7lb!k7B5{lw3e>j4_^#XEeSa5CjdGB&5NXDW4PM0W@kcb8o3R
zGHCL6)3o6ask7>DA08e)VX#JC=W+#}i1wXf!5hh1v5ceHfmM3;MfKXD9DmoxL_Tib
zGmiYRyp;cRUx;g@S}@?_IcGSh;K4F3M1cHr{UnfBbKe9ftB0#F{p%HVyz@ur&_?BG
z`P!KccwLUh`#yU1UV8?vfKOez^Sx%YI$QE7I{M`C>uA915U93omnoiwyQj)_Y;7N*
z-`ajc{35L5A{md?Rk4o4=EZ)-vnvH7sMQ;Q~Q#b?HXdY)cUtUmSw+yQ$9%^YemBv^)k7Q~WM+W#yn-aP)d(@DX80ygjU0cAP@37U#xVesQBrq_M&Mj^QPS$%45-Xo3N>P-NRseuO+1oBu6Kl4qf
zyWK>AV}_nP52xy7$}6t#Qui(WE7`WGr3D7i(U>H8-%m5rvoy!yiAXlg;q2bAx=EA}LhX-^joyO%IB1o>kmBSY2m6*M`>h`lS=kDmRj9%N
zx3?0isCS{18X4`)CINTr(H?Iv82Uz&h?o0ZJP+&sjsjMm-KO~p3JwGd{E`0>H{E0#
zDu)Nqk=iTD^_3LzniyVJ@t?P>pDv#f!uN%Dl?BKpZq&j$yRBuU0=6R0q&!^=5#gg5
zJs|t{$0o-eP>h`XZ5nZ^h7V;2`|nSsxs9?U^Hu{l;@d(fx?%D|k;`>c$1B|}N-3;R
zQ=dd>c7*)Tzq+qyv_{RP!ass>%jUN7h_-4fU~CpUc#U3qYberqM@eE8un$OY
z4lBM-`J6sL2iSQHf0TS~Cb2cTLw+p}{*Wg~zq&>2h=XoS!{vCand>o-)p~h~*duNX
zX?xnR-D}=Uy}q3ADvk5JY+3v8`zl6aOdK-dAWIi>L`Pf_2t_ZU@k
z^y+9Yw>h(6&`K?>LF{0okI&Y#+u7K>cwyJw-M!7^TqsT`6F%*y1jz_%
znHO_gAUrX7R0xSPd{?CSO1qhK4q-lTX@oNCde}h9xJ@u#{OqzMnY19mL}=mK$c!X!
zArN4C=4MZsS7OM^4@jS@Ue)(V6Oxjl^EEfMLc;}{At50Vhrbn$(;8#djX)yPYI+57
zglBqmK~D*UOun3+MxOZ^s}6NcG&7e?^$5#0edlkD1m9L?D>@E$xBxx)Y$I>NCZUOr
zkkN37MLixT0QzKRSPoCGyHC9=cGyxYG{8*x8$Dw3#^z@1_un!iookjf)S-7vgn3Cs
z(?CsbuGIF{+dV2w>-Z~fFDWIDbCan?z$($vzUd}sNackYcPzh&U*Z^)07=l+P
z4S~bxkp~S33EtJd*w!O_--qe}z8;d7X5F2M76Hw>tpL{BaHQkJB7?LCW%wXkFu!*<
zS{K3(F_yKjglFl@aiuY-oLtpHoXwK4t#@mJ7OtzY11cQ7o>#lJt&V%Zw;{5RdJY*b
zwJHw?_7L$z3-$@-aTITLip2&@fyY%{ViFs```Er}9!U?zxm{eRG>iiDZ7gB9;0Zh@
zx-TMl#LPLOIOO^_7r-^Op`&ID*weE_s+5P23Zk4iG0gR?se;0)Is(wH$;D!OagfH2
z8t!PwoFt6=RdWF4k+kggpm>~uH%~Bc{{C!ntn##ZX?;ALPB9qGTw7gTg(UX0j;v%@
zK7&}W-)g=5F+xz1fQ&(t=xtBpyya~;%#yRc?i?&wD}?<`(7ipce>9EBm^)6f!|xzN
zWr%K=NWZ3|XYSc$K!E7=vZ54YO*{2UVq5TXuk}wO(YZbeKiQtG5+9Qucq;#)B7)Ue
z(67U7^4-&Y5E65;mhcb%vdzvrGYm=h>}V%_d2R6UkTQv^OuZ`Jz=l|2Ez-XW`@<_1
z)NSnToc1jR7dx-#!E$%qWXMt-A4ZFjqEo)!(z9vj?_YnNO2c7uWl*9b>VA45@!1IM
z^+WVBkG_AhK=Rr6p2nK(nsoLBD^~hNGeFT*Vw>}lzgq3XX79m6T|z(7on_UI{@Q+K
zPLH_C6*-04JG56gNx9^s{U2_TXkn-aY_-gNNqFP2Hm^w=EWl%<|FG~<)4ktnz8}XwrAOtYtjjsci%ZM0OBc6|W7TBn#rORc
zwBSyd>24@M8f9!N{%K?Fcuw+ICz~u>dOz7l;4{;6Zhm!Bm|M@zhpp$9E18Jh#Oc{8
zlmk$r?Q(bTe7zD_iM15-dfH!(m6?~<`VTc!Mr>A+1iFuYa9nY8l?0NvXd1{P$p|_V
zk2?0@L48B^KKl1tk!BPLwCieJ*}W!$i&gGVh~(S~9;v3ybLqB#7dJY4Ny>~~))(Hr
znEV@%R^RV>dhy-X(Y67VpsB%x@m7vT#@_j>WsU#qn^~(J3U~HZWAEP*)7=w`sKAFi
z8Ir$y8&f};CW5WAJF_m!M7;X^KRG@v?$S>*>`OL+umntVdfHMeQP@37k}mwMp5u%8ci);0Uw57aKeZ0nY~46L74uE5FW|}Rix=c!a4||d
zq2cCdvabAQ=|Hj5KJH;o(BDQu+Ne-P^|6hOZ@gmA%1*Jy+wAyZG#oRnHO>eRx;;Ay
zvYpk{M|MtBX6i`b*`4JbyQh!G8Sb}j1&>qNNz5g$M%`(BzI*x7K4%zOG
zwmeKt0={y61wsJJ$@{})s0SO`YHAIxFQPioaOfy^5Nngzv*gt9VRvEbVuo&C2uUe_
z5>;-IDH5cyFgIj`Ly5jf;}=&fXaWJw>zL>Zi%nCOmXUV3;D%q#J$&kwf<*B_x2{%i
zb|u@+87G7Rzj=x4Q+tNFIB$H^Ome`^osr-54RV@Cbi4{no8Ahdlgs`6^N4lq
z-d>+xIk9AnIZ)<)LraWVd`XipP_U-Bm@+uZg{yuU;#PI>*=wW%`!H)-6=|`N;x~l7
z{d&>Otu_3CWiS93_--4^ZrZk{dPeSUDRw-h1pKH$(S`#uc{X3^7
z#e%vuY7b$j=J#Wc_@Q*?K^#
zh!rjaehcM|la^Uv9q&G-Sn}=dZE6K^BiA+MM*1*GTr43@aTTmttnhn3)YtSm5{W~e
zF4zYmBk5nHF$$i1RPGd{l%zT6uE7agVhluW3l_V$mP_2+T^{T47Na#i4ud|vK07sY
z>2e>$$MpF@)|;1uEF^SVps+JL>%5^mbA~|&`PBD*vyUs!7Ym#WvC6uf+#zW6`mv&f
zXRSU0_y|2DCEvbHgYfmKqPW5i8k@~?NLy#a=}$ycB
zg3r<7>mD9gwvHqR5WY^CudeXMj$Sq6Zu^XD^CzG!+38GBE>q_%MthB<;)PS9+#olv
z*thLyJy1Dau2aiF^0Kk*>yh-h#;%0Ovy%+JT$C`Oh)YxjWgL$*-)78_PJ`AlDbq4K
ztx^d^I@(Q!axU9i(zt}Z6Ue}@D0GI)TO}brEC3GWmxX0s>0l(Frh$QS|EOauau+R0
z8s(MA`_H@csdl%7nUpl;-u?o6&G7o)I+{sf799buSygvEf&&
zKrPgi?f0OB=^K_n5Pg9pMUdq4O}!gBUWuh}$TfTK;&d@bv-_j2y02+w3NH-z2QRMH
zfxQ6JYK%Lq-F`yeXj#L00-~_WL9jL1n9I=sdi>g!;x7~dc%`&VdO?5aT=L!R5fXyx
zbd``^@_a$RmpI*9QlJ%ClzX3paV5S`YAxEd@BSL$?K9ykwGF6n
zh{OC4sWXSV8_duccp9RywSx2vtBU7eXXl;{|2@YOz0|D^yEZryKu5Xm<2T`=GpZ>I{1Vf^iczLTWP
zm1GnwoJ3dCHu6L4|5yE^&tsGdRbDueLm8NIi<5v2hFKEREA{6GK^q!XPkbGD_g=tk
zyqP0@Eo4R}YYB^t2Bt-q-NtgaY^$+jCLSqJ(*K$Iz8&G02)@uEjy2ARZVw(2@fJMc
zIcCxG05#6U*SH6}^$uK;Jhxfg;F6t*8`L{Ts^`^JO}_9hvX{s5su1DhJGI+O!Y~plMk@>M{8HZ8G!&YBWX%%_AFCd0w70owr2b~Wt#sh=n
z#fO&yx9ZGGAfb5$Lk53EU)5_vCgGV}30>O&4m}#hbOlvZ`JN>JRWt|t>pc!&8?6W_
z%~TIOIXPTk{Ljl=yx_qak1F;J%slFbH{EY+P;C2B>M$`Fb1N6p&N7|URMk2$8T!cn
z$naatAw|7h2d03Epm%NQZ>LLL{ccG{EarhUPS=EZa$!>TugWG2Z-A`L~J
zR^KM8kv#d?EmDLduVN$b2>maz-PSHITvEeHXbe`76Wy{5vi=a4_1TtLqSV`jp;S+s-+Yqxomz1Ge*5q-{xyC9A>J(gYbDHJ&7CAa8Y636;e#37*NPI*gfYr4mu^;ZIT
zKE#HnnQ4;|r?05A-J{^sxkivjebU$^MD!-F`R_GD&d|zer{12Q>RI#g(I1sD5H{rB
zEcFW2awcEI+=AD&8m;efYZ)EJM!f`-|GKm~x`TuQeq$5$^)^?Hg_lA7%qI@23cOp&
zNGbfDCE)qYP_U5S5SJPOf?!R%?vMnPrqaCnD#&>5Gkv}$YVU@)Dh#b++2+lU(bYtCe{tCYOHFoR$)|;2qZq>~%M?t-7G?Ps@(1O2)jMDWk|U_QohX%W
zx@mWRyb2N*6F<)7zJ1eLBi!EPC(^?MKR4oA?w{>(I=~@K5!?^=pso>lZ+mP83^NxP
z)J`#dSdYhU&JBOg$NdPHVL1ZMnz|~i&&9`?c()aE-j2WghOQQ1Jt#q1LPDbC=1FRc
z`*5-3Pt$EE`$co9^Xn~UKNepMh(tMF1vwJYs-BCDCT<)se5Vb@C8wqf9MDccq^DCJ
zpJF_b?afo!OrL)?1xY$ge6W49>f3Fff<3D*3Qv%yzQ#b{2AW&1moBWlt{+k*qx83@
z*>7NZ3&={EudZ>}bzMN2nn^fEn5mc{`9#E)wHDs6b`|ErasSpynenN3f}^l*kCuD8
zzX#|C8g-KRG4fgG4-rJvJAF|Y@8b}5APdBL`y!oHU(h+(DxaLjA9lGEdUNAE;$0D6
zNsQJ7^D}GmH{wBtaF)te`W+~i)u*@j{X`-C(ttZ`bgY&%{CzZh@H;(swNEc`AXvxQ
z@GLXbE;R0uTR|na)o{a8R#FrFS93@r?YFJI7;mW)!jojIpP5(CG3wgfqEEoY+I9-q!WhynrzG={I$%$epT?7TB5qMUE1qauz;M6D#F
zzzo^MKCZnM_=h8aXe0y$Y%PypxpjS7))fyMbE8mdzHY|#cqL9crIut26fe2u^Nbqc0HNH*%}MQyMK8)}#fzLP`{p{@}vYsnWETyIKi-v-=1#tu6?{Yu;A>>_5|9w)?;
z&xW%2F&3EH!5lJqYzNk)x;HwSU=}oZWlV2U6PPN$tx4^0sa}d4d6Tbc0(!e{%@EJG
z=vH!Qs{7grOyQCFw&X|c5P`Vd#8#f|g9O4^`(%*vjWBy5J70>oug*nqj^B>xJ6R77wQX0!b;u%%+pgOshy>zEGN
z@L4|mD1r9MU6I2ogNh}ewcED;?mdjtBNuMa^yTbkfTPpEE^|V1gyoZXMtqqpFZ^lFXXNM5OTg;59pF5QdUdJx
zc^#d4aAp0w&EKyL4Qsz=ZO;xX9ri~4GZ)w6Lp^hk3&n%Ym{IxC8eP~$>ZuG
z$vqdp#nPj7OVam6_|swgE3oNFS|HH#7U%4Ykj>B7vB~^IL3y8Oi>KWJiT|+WqR~nM
zZkGwzgL3j&b{X{s*H8=pLL4CY#F9N_d}4K+-jKB3_;)~_Sv;{X*g}nGBbaTg9VNE!
z@5;h!cBdWLD(r`y%e-=DM9>QOUQ}FT%2>((=C__dQGb!14GqprnL$m9l%$z(rZq&AyURA9mB8KSML&Tu{
z8NlW(+mEM5TYHDwuefH2LK$xaRCU!5H~XnI=dJLB#-@uvq^T~?%~^1pzQVhA;ne=c
zB#Quvcf8WE13d_fk#{pSN;e?e7gTg~Pi6968eM?+<81!=p^B^wz1P!u_hScM8AEJJ
zZd>oVvDDpE<5Vn%b#_tOh5fb!d9T5buTsQ0=B|^$7g8fHS#
z4FhR214}R;Ve_HzOt+t5AroV+UQPoCZQS|?kt2>RTrr!n%$|e7Y)p|N$XE1$(Yx0)
zse#mw%;eJN@9$x50pN#9b=@J%xy>l4#862MlR>oSyPnfQ8(CK}9Mjga?RLtFx6T
zc+j1%K-YC*)Jo!NV)-A%@?M1o<*8BaT$S!5*F)}ro1gkd#|~o-oPtnO3p6>l^~LCw
z!o7wh-#%1-1?ShXYlG2f
zAO3I-m_%^J4g76AU7R9+azx)U=!ZD+emOy}=dEO?K`>E#PsyqWr3p@I
z!Wkan(g3~&Qv>D0C1-epjNE1<%ebxG_qwmIYCp|#`zp-4n3-eD@dx8X7rbf(NgDfdtVxV|?Lb?y~0ZCG073q$zWvU2HQ`D^sRx9~CLu-7wXt7`D-Arj>f|
zW(c!Pr-tYT{J6^0B|ph5hi=WtjZmb<5p6Pxzv(!O9{`9+8?y2V+M3FYTm;`lF#Hf}
z3+;*i6jVJ4P0C7d+^rG@0Ezbq@P!8QwVm6@Cy%9ci9I@OWq%)fcUKdfd^A>I7&k}H
zm7QG5;AgNO9(n9xirIQ&33p0tse0
z(Fy$onUjY~&bvb0?XQ63s7ve7N(-#uSKv9J4@0}HsEjUJx=E<^p)FNvdq{?_f38PL
zkPT$=8Dq=E*_eDgH^eV+@
z-c`WRbC^v9ywl*v;vNf;{J7lv)2ardaXtv{keuxACK)`H;*(D}NL7+-`8aA4^P)E7
z%E(x}8qSYLn_vUK`l-KqAWo^aBRi3>Fx)k#kvtV)_FiO}kC<1nSP4BoM3V9v9Ag4q
zVcV(am>h8#T4Ap+)&kg}1J1Z`W4{SLG8s}MU@l$A!w>k?y^|IUl*_rPGWG0#B{t91
ztFd)g;Uh)t*lUpF^_*}f8hCOzcs*1|ec!X(n5st0Kvtr^_=4o8RXX{Cp5aHoN0ai3
z*?0`kJZ|l?0qe}o!%s_YLxH_HVR6OZe_{=#y(KUoHmhlSdL7cTfe!2UHoo)n&o(aS
z;=SAT$YPG>k>`UG!3)
zF28;x$>C3H=MidIr=o}-tK$x7WR3~ZYfDFAo*^E?S-<&-DDx8rT7Gevd47ia
z=ln2~*&-6j@eiI`o#x{FpJRgu@kT>l2Ab=T?kt=}OGRV?{ib05z0XT+CYX9HQ~ogi
zj)kQVF*>03KB^NnDBZnWVpPWWh#c$h57cpluOfuBNZG&s8a7x`zBrAQ`~1wCii5Jm
zpfCs2bk{26momBf`7izd{(A4BU^X
zcnC8^S@4myyn^$>w@YK;+<0w1u~gcC1Nd|^og)P6w^NXqLQL+ct;r#`*TEQrUhp?R
z@mid^XjibRSYzfRKt4@nj2ERF`@DbF3?zjE#$qwC5BU2Q&z!^?dG%p*+hpM9P%$EX
z+-|0$bDd;Imc&*b;nWyZm#xCce`B5n;FVuv|AOs6$kd};fL;UjQ!&_@3bqs>)g#d?
zFJ7I%+o|4;f^2l`>AlIzkO}{X^xVeaxLh~J_HnD~$+grR2gNzG0h7%L91Z2OM_=$g
z(&UfcdsRWHlPdkL(y6?<6F*@R&gz_|=|S}9)UX$~rr6oK9UP5+F^P#TNc3xYcD|OO
z&QWDGlq~3=aDLGU0_N{z^sM%58HN!6o+CrF6jSg;&579OTk+`wVZd{{e(50xKWi=4
zv`YL&MKPA0I!9tdhZw1>
z(Q3e*+n4!76X6{T$~*gnw$1O)A*zub$Uu~u`a<%|UO!{n)6)|q8U8+&mN+Y#XB+T{~^}^l1Lvw=WW`+>1!_IwjByWe_Fw-k3xqU6>m4$h%%`NembM*#jZ;PL;
zGk$Ejsz>bH+zHe`IZo8Sln0lZ3IN~>c408#|MVG}BoCwiAnAk|%Y!S%UX=((3jK?HxmkNe{dU{~Q{
zjVq)wx4tZqE*;E4YcyDxw4yG=L^h=5LX>NAmb4}R=$>R6sb70FQs38l!*?-fsvTdK
zzXYN)c7fTt!O7jweZxn2UT&-XLsD{WOECxhc;ONuX^L66Vvh?l6y|drLyh0s>1!Ty
zAf?;$D*7X47-^2)o4_E*%>i7Y2`aZD?IYoxL*!5m+8+@FNlM7RJ@sWb_r1SuXN_9+
zlKo<#y|n#=A`8fp7r1B2JNM2A-ks@zWUg}wlEotm^&+j(%OFAPA>pO{y%Re&$vca1
zeDP(MS};hCa1Sh_$uwjt()^ErnC3IaTwD@5)2XN9Mz>!Yr1P2oV@qWMM4$P>&R$A~
zuMYq}Qup^qtdPgsrNaTH^hbMT$@-hf?6H%Tx>e?WcTi>~0nhaBJS>eUc{)`wJ}776
zv(Fd(mCxYSLATA`hYj_|&ZqrAcpi@^p?nGEm&JX(XL61B5+)X+J3+Dao5ma2+1~Y>
zk%75p=7D(D85VuY=N?#*+o#s>W3IYHr}cc{IbUG5t=D8J)1*a?n1>U>8)(?c;UfmX
zMiub+FTkTM=OzZ;=3I`V{qVFieg`4lmd#L)#Ya0X?AnzLI#tD>m}sg%9(^a)+O7+a
zEZbX(Ob)(P{vXtxM_7|x7^XqGG*RhYdJQ5{1EHu02tnyBbOELJKxk400qH6o1f*A~
zp@Tq>-g}LqL#Ux<^3N=0F`HS;X0rOS%6DDo`3PK@SzB|KlUP3n@c
zzJ)fh_J(4b{`A;f4X%5t_hdU)(Of(UBY=@Pc5agWm$0JX*vcwGy8vhC09%%WL+D%syz>K_5xEx5MO@kMBY$A(WT9*g(;J08F@&9&xkdU(=X
zj~;s8BL_XpziwMON(8rZXT!HCJinnI^;)Z>5bAeigT!OJB0jb9bJ-ivaY4SiL9DH<
z(=er$Q+Mw<25|$uQ$byc_?2JT*0!_^MXt(g1uPD~ZYF2N-(Ly7WzJxYjE3>GOK&1(
z6M38yM~KH?D;43E1)r<4XtZUnV=4^rCfnlzc5ZbB8%Se8zcu@u=7=cxB?D6qd}hg$
zRGXm0!UB+V{~CGp2@zSo1ZY`{~b^B-zTwCdm@LZ=(&^x6rywl
zZVHWrz0X)Ie0{%VK49N4ZQ{0_+$eaEVtIsKTCkp;Ymnia4d|Xv6Tbhj1iLWcLOjLL
zd=$4TM7iKBh}E+EeIx7rs{D`%#@!;lca5Ap`J6OF_EZKG@;s4+xAZyGS>*R$oc@zT
zN>=uVnW-r-&ThXYRBKhJK-$4q8TCVjFWe`M|LVydmGsBZIYzRbkC
z!-x!J)7Dnk?he+))|Qopgx>>&T^@2?CV*4Bn&~L1uZtq=Hk0P_V97%3(hU3?aw+|0T{u6ua
z#ozlajhANWv9Yl+XWbVUaNTx&;9pr+h?}@Q1hb^ruD19To(4)?lfZ{KHPH+y>5aT?ZJ8%~hZ#?r2Om1MfZ
zEjzBw_ItyqcX#DMgLRP3EzqK`G`}AE`yhXDIHH6;MQ@4dv$aB{Mw+O6{B@ACtE(6)
zjdREkHyh5;zJWQi#TyFYB&Sl4wu@!|rmnY4*#QOoUmXOW0J8_ouRSKtyCys&-Tn5B
zTPkfR9_8#po;i`Y#I(Gm)q8hC$fIKWUB4Rx{lfmH$m#Sns?|53yS>z*eQ|!E-$$AUrh^2mYr;gm+2eqBm<>%s*DcujoLxI!{I!ueV+~oM
z5xxRh;|iG;_Yv!oO!uM8JW@w`N8ar2p`Aolw@HVZoNFI^zq!!0CN%{`y3VZM3gp+A
zdQd+8vk11>yvOf&ogWY&IRWvF-5Yncbgp^Sd@DhvbMx57tv%!;nL97Km^=P*G9fNU
zk(I&F=gqLAr~MydYCFe+d(LWio}?n9(YM)m27Qj$rG4AVsE=q3pwIvg+`8Hc#R<~!
z>-s3>iOJKUarg9Ol%}|X84ea2;(z~z96&`jEK!%o_FJ{Ft09e
zb5N*G-H9&`ORYLAj8nhi(;xHdVdn#M`u=Z~F|tlGN7sW^qW@<;o{72NWyjhH^sZ$qC}bVv*)#HFnx=&=?G+ps_h
zvBTP)XZ+w}*yVVvXY?a=`gH+tjgi#aCdGJ??KiNoSqVG3jqOWP<-P6hotsG=@U4I^
z_Za8gQTY90ZCY2ajN(tTTx(7i+aB51EWDYa@s9vRdQZp1%Mn#Ia6==|Ffr%w1uWHa
zUg|z&hkIC{sl>Vf^wTDn-#T};*z9hPN=?dpnO?G900Up-u++lripimUR4V$0
z6id&m<&zk3|4K<#&F!3ceO8V%rLv|C-J@Yk6mXY^c3T(lQ$pC}Q0?5l5z40QkOu6m
zop7=exaINbK2Z3kAMn}E^o%ZZN9g+}1U_lRjca5tb1W(4q<&Iov*wGMfJRn)`w|
zY+a2Zc?0$IfCTpzI`yL4Tx@rwoUGr9*l--oaM78=uTs&0i`<>OV@vmMkpbTlzyUit
z)B$Vc=F*?J+&|+!6f8M~z3j(t4OzRxiYZ@R>@OI^&)Gd2&i~zh(R$d5zC8@WX6HU#
zDitq#9NY1rEXTF%S2iCUA)v{hfw?aZ=8QU-2wW_(Gletqv4+NbklxV5|3~0N)1#FF
z*J*^iJ;mZ3B=7h@mjjV^JC%31P`!IcDxag91|hAW+lFyWrpkW=0s-DgjnN2J
zh+BS@=*)5d^q2aN-XFl>t_l$FhKXJ)LmEAszH;+Vl@IqBt^Q{*^LTmExHC6?szCK=
z8BN7h+biOi$D|RQkh4a>bE{t;x?hi8#reQDI$Bn44_4;e0zN*yk)s4ZU4TE`n7SLaHYiU{i_}_@*`
z>raR@ERLTH`?Q@5@8Te0D&ONXLpkZF`^~qOXB2`c<{u7B8m-z6U1tcwlkXKguX%IX
zVc(U26D-J^&*eK^a52c%9G9UUYp493#JK@g(Cw}{C}ifS3g7u=yNa-WRmX6{yvrNV
z@YT0H=;Z}0891;a3C@oc)G?KgHbS4A!)
z#zJ|UH~!1T`MEPm>qPY|2Mq&*(SZTU>2-L2^wD*1*atil}Xr-}SmDqBb9&u;`3QE8i$B(T{b+tRii!4b4}>pV4;H10Wfy`5rWUk*fBR
zVwDur`l+s6e3T0XmZ7$%SHd=vbpr$ndg02kB~O+f(_E@Pwfx)_y8E_|fsN7aPi;8W
zgYdVFu595UR^Jw@Y@y_qjN7zI)NAfe0{;twh08lX5B@sy}!@tMK#4q4D2KX
zh_HctuCxy)`3X;i6~+LU#>G#9g?eM^Hh291J3o5MpN^dQPs&HWbtj5T?G?-J-1i`o
zTo)S=ihqYxJ94I>dK{JGxV8`yOySB)f&(|~Ak}?xf&Ct_bUpdPD?a?_mkLN}F!%GV)ZGqB(El%
zrs!uRp_v;021G537MiOi5#>S5Cd#=s0S2-57KqaXM%}wmOp8SR6Tfw3{@n2zr@U7f
zlZy)HYpX-N<*fvcT|T%u-t1KFxpv6rJhne2g(gTz!VMz^S0bd5s!@CZWYRRss3*yZ
zO$*$A@}T1BTTuJkxo)qBk+m4_V+YeqEC5?oW0@OOw00ZQKvw8g>RJ`$!4;N(2hiA9Lr1qa#
z;s1hW_gcy7e)j317@Ob$<*nRPyv5ve@8*aDa$9kjY_EMLFup*M$_|Djiw`^_~Przm%CpS14((}#|s&x!~C
zbWcjkc??9;p0;HSB0sGW3v!l@w^}EVE!hZ$zhMjcU<7FSq>t3$O^NsW!oEEo%PN#S
zHS!{}B5;bcb&O9)R7R(@ti@PFd<;@TbC4@UZQlGK{+Om3RFi+-HfHv|sNkEhQNY5d
zlIUn(HvXj4$kb`hmISAwFQgI+U&W)_{M0@eO1$97Co6ycgl3W{1&cJlLmD0tuwxX3
zU$B{;(nEYXarEs)3XbU?JIcC%7wHcaDV!DaQ&YB`a#9}*>Y_6SUOmkwF<@oO%a+Y$
z)LMF5G-Hl@>!T@!wBbX(QLP)Y(0q0N(z+z*mqyz!E$f$Nvr!DPdr{sA2J)-iJCecJ
zYSZUn87{qtji47`PVp~H86@3G`BB5vxO`w?yu6iiPhf-HwQX7&n|o!?cf-U!^%hwx
z+h3rO=5CJZqPGf#Vb&tMdwP1F+v4)Knk*pm%P_5;r;krsZW8pUlVlL*>nJmlot_>8
z=TePJ#`)c~DC4x*^qDd{%Fb?XPknAMXr6)2NN3y7F&p;>t*>08(lp(AeyRdFK5J>1
zjf(`@=wn~gvFa`4gEo08kK0V?4!js4F|ntLC)K=Ksq2?-_h{u9GDtW+d*r|~)5TFq
zb+&`4;-M^|`NZk{xGB9D14nWR!7I(mEv&W$nmw9J5A?xKi%%zWiAjCG#P@V-40tT@
zkm=^Xl77jg!FkDv^1allRdwS6f*N&l$r;+DoLfcS+&6G-6U&==RwvmONK@tE_GHSr
z?x|db?A263LBZ2ORau}MiZ2Lz5fpT~*yO>N?YC3>h(i2NJPGRxF|ljn78>57q;wNK
zS$gAA-VI0GM)9dBxYjt{@7MdLm0erv?(FC&jJ+HQy1Ua%mu!ut(u({Yck7pf_`Qb3
zKIu@j3MK5TmAsaH`s~@s-}L3vLI5Qtr3h{+n*9CggA~y11jy`oteA?l@~U%H4}>>M
z_C*0BB(DRNJ2t->Pg`DIS;0!~Zn@N!%47z7OR~4UX)0UGnDhgNmGA|Ge?gE=C);uu
ziRY2MfBZ2zITd{J?p~(u$Vi#{nf*9dn?+g970Hix#&)T28=WoN+N0#cDk9>MO|
zT8er>pQAShm7kw)KHubldge|3wq#l18zP0W|I|i`|8Z;A#PJ+7oOx%c^be>=Yh*n>
zGY5FFhbwBYrP?-F4>9!K>^L6f^IW+{idW4`0zumW4K4B5oSE>9Q`?l{qL~RDphi(z
zNcKNHAN31pyhfnN*JJfqSn8LZM_~tU1LZ^|aEn^o^sgYx(9S+5r+*uV0f+wa4(~gD
ze5U(UJa`JB|fbUTqESIO|iHyfeza@a1aZX{f3?Vy9*UVmhx6+p*uw
zJX62Xiajv&|B|^f&p#r@T|7l9D2d$qVskWuvE#
zf+MbL647ifJ+&wRFDU>*rB%5lkz
zeWe-NI5HUYXIYXmy^R%&
zZUSOPpf!;MZ;p7A|K`8WB%-11ti0X+;u5Qj?w55tx*#t}4gFPG8N42;^=YQyv*gMi
zM3?OVWH2@pTWWs0Lp@iryJFJX63fo~Nc;2kOqu_zun{7v;@ANqzE1vrKut0R<*>@V
z$1QN^<}1j>5MCMt)NIT-{R(!=zPRE4hVI*aY*MdgK$lz=DOlvECtA)Wba>{wIH+D(
z#JRgcF|xpEl=2edi+E%vzNuq&^x~N(z0=vC`Q32~C15t8p)dob{2n88@abATfL7YY
z8Ie2$nrd~Eslv@R
zHK(6>{R#ErNBmjZj>mJGMW|HLrCjcMCMPE&51LR})VN;NX>3{Dc2kOmm!L9Zu0!Yp
z9*1<<>BJ=t&)4@1{FIsU7&n!&FaF|*zec7GfhujW888kMU8NW>B8l{dRR->NB>_h#
zI#XWIKQ_ZHj;;|V5iK!*-|OnqxT_MFz<_c4
z#J#xeI8o1NOFa)NpU0VBvrex5O8ctE03w?clco$1$|@>ow9%D%I
z{*+3g&7RwxDmC!v?(S~4qOb4BSKjEI3$}r1VQQ%H=sKiLcNwwaQ^NtAoJKzUrsV@|h-5{L<$%WKwQ+{_Vx%8UgI
zk|e|Tz?Tk9ez5Jw=GuOGVBA05iib6k_Ea2F>B=Fl-j-3M_6o)kxmDAhMkD&GMv%Zv
zqEbU*GtA#;n_xbCFX*?)-0`kC_R5^c#VJ!*h?5Fam)2bIDw7+-QFik#>5`8}vO(C;
zbB`tPBsqM`;ppjHw&HOjQDxdY+y)Dfu~&C6%PSJopcWU97Xp}-2&K@hs0`qx=w`t&
z>rnoz9F!S#){fdqu@;(09u&i?0DPC4jSHIq)*)r{<&9)M@Uiy`QN
zcHaF&{t3*;?jB&lT9G`tS2#@vgMAQq;B+=}&Kj
ze_)Q@wH<#-)U-@pD*iH(54L@`w5jt0@$MCJ&xe5hwi}n&}=2d};cp|+#c?sNi^W}+G41k}J
zTK)i;<1sl=OgdCHr-Pl=!II+#wLo2ZVYdPUYqG
z+5;DgC+()ygfonPqGy`oezBoQwQj8GWlRVK=6^NJ6)FbV*X2$U+oS+Re{jHG!3LH{
z9e=WCX;p2$(8+3+&$5j{*r0(TDJRgWwOq#&*_|~(gZS+6-2wmRzKLUn_(fyKGh34o
zsgEe*9pfQBlM}yEC!-5B{|ztP8TTcH(@#ebua(KkZ(R?Sn!(n+h{-a@H+(z+5Al&G
zyVBfDZ|0+z^G`k3(r5+4)t_a=Ysjw&!i``^5r7Md*878!&pKWcfVSopjLz_76_G`v
zc85w-Yl<_9Jg!#p7-btTV_7n{&JB)agt{SH7niAn2;&f-n5w
z)RFG)KKu1--$7NTfi>zvNk7;#v0o@kaZIR0a6At`Tw{7rt-(`$8kKt~j!Qn<3Bo`$
zZvTXlmk5iMs{n0UAp^=9gTAd@M6G2r2|KB93qc_$W?ik}_=uY?tHD5Gfc%}7ns_lA
zIt23SDtD|ncyuQPygklyyZJInH2dPLltSA(xU^d%&3BwjA)rIDp=n7rD8yhD$~kPa
z#g|gbftY@}l#GuT>1f<-@O~Z7;^L0gCFptqOaCb^X)S>Jb(R){#9e@z3HA^O*xh!J
zLGnDJon=psFa|
z4h31+C-7N0T7%8sHl0OC6+H2nz4`lecGCrC^&hu#GqeKVUBdY%O)SgYTO%LG`{)eqR
z%%pYk!Ic$Aj3y(VV>MvJy4?k~!OVJI;(}W1j>NW#1OjXHU8HFUMTEAwr#7y|$xk#a
zH`h(2F{i%LK_6tln46KFcxarz%zG90$f_3J1#tb?0$)i(f;trOOSWfp*62~S(u+u$tG;USvlyp|HT1&ZP7gs
zNgarV#?xJ?lx&0d0v~CdmV(GFsKg7_V=&cX(j`j|qvO%pYDXPW3*VlhdpC<5cIK&>SY7JU=Z{+*S4L{uxX_}INgHUOHd
z|FTlVw6&=n1gvq;@g2=!)Y}O8>17eRcLb*m7v^GA+D^vcRyz!D2`CBsS`zKd7#ifEz{3
zIv=$p+iC!3`rgLrg=w56$ZwwE6%yU4WY`&4LQ(K_v&NKC6LbmoO8>Ru&3f
zOty=M?Mpan0)g1+=qo{SeUJAAhJ
z)}e;OPCc@l7s2!ZBMICu0T>jGoyT;
zc_WL$d0Sr>xw(QhdU(7>)@jU{I{W%~+x4nfXf4ZSiA}a8W(E^0VuY$?WNsybP&eXS
ziFa|HG)L_X;5m^w(QZ#RNxCWe(txqlwG<8A!O3#n9=r}~%Je&WrcBo9
zSRaZki&|fQ`J*}k5*c3f_1Hl5Uw8ZU6n*#Q#F<=hWf3EMHh~=5eSL8MViCm9+sgjH
zM)g@UcY4<=KW-B+vBE_OK=f~z`44H@!X;18V4>eLyaBp0%RtYOtfQ|(q&10qG=CF~
zgvzr-!57k3<~M(uj`|LPI0eLuQr+8S3LD<;mDME>rIc-_V}
z^u8Z-`bVk_?Qst2xEb;VSU?qaMLy6BZ%`;~TJ9X5SNGmz`Yd1S7wdN{hP_^Ms2E~Qxny04HCW&eCbzkd4!#k=j-W>yy7>AM@xMj
z8;OzLFgMAY8na7WQ;}>WlJd79TMjCok(C+iJxDdD0bTnvYLku&wqprA0svgyHnIW1
zYJklsf=*-19UM?a6(@atUG3L1+0;n}X#kEkh)utgdNF&yAG-h`^T@IAhR$E6!h)UU
zvw6F*Cdk&AyLAbQhbyb{wwW{n{z-$=Ex7t}5m%sxhLMX&GAx0-4uc0xFTW@j7W;zR>-T
ztR-CG1{wuN?U{1)UNNd>>Rlnryl5SM#<7$Ozjs;3p9G)z1q$AbUAI4c&s#eq&K-nv
zX+@)t(B^m7Rl3H@)r@AxdfxrsxYpLJW|?>ZU*4HH65dP4d~!~yEZNJ`zk4Or>BWpE
zmVw_U5WE7?-s>OrLw21SKkQiVky&n8dO{d$u$m+3Ayo#@Pg
zN@L8OpleV&BX5PUc`ow+1aR2fHxkZwckWv*UGLA_8Nj9l)_^{L$ryE(5Fm-W5DAZ8
z_j>sFbvkEI{!gy}3VSZYWraKjJ3QRgL$fB2Npn8&Z?V#tOQf9Cta6WVfu6T9DBMQ!
zebI6bm`F0O56_cwy6=CuvMN}T1;CTkt<}m=rsqWPC};7edTx_gV*u=>;yvpl|N0#J
z)Sl|9eOy)5GAC`InA^}M&UjvueChCXSm#W4+VITg<2}92$q7`~iMec2;tUVguKmo$
zcWJ>VOw{DDsm8nOGJ^=!m=A;fvsXho3y}zR5G23d8~E(H
zy~py>#n||pQ23*fxaGPlCOuEyGOVJfJF6W-EKCE+r79X;5I!y9S$I`@jEawoBp_hrmYdU%QmLXmy$_7>?Ab;y
zw}|I9ywlO@uhY)6+qd6~buVHs>^6it>tuc_DN&R}#uT|HJ~|zqRbCA^Z-!;H<$K|c
zxbU=z5(Jyw=0oW4z9VL*KrUMQ{qB-N91txS$j`6s-p6OLuG@=4tyjA*zGP+pX8&gD
zukqw==o6rO)~D9gF>dbq5!_@`P
z4+-=9o~|o@I#YmD`!j+2Gu}U78A=Pg9S=Xsm^R=Cr!n|`x3sX}2*Sx)*WQ#r8Z;W)
zocaJAAj?y={e~~RxaqGvSFKx$
zT6{hO>;M7&_BigEg5(xw*dw}Y+3Z6yhG77MvNAI$q*&S1>&QqKkKHw)J9EbqzhO4=3X=`vw9AZgd<)lMh12h~e0VbcMAE}}@a|?m
zm5D8W1*Hd?_%z91aN)d>Xlwi9rElR5Qx`0k<=-E0c<*;u|H2@AONQ&$9&z(y&wWytS^vqwKw-AWWoXCyJR?+>{%&F`wMQcHFV_iIWIRu{t=
zTyD56A6;x>UTFCXzhK`7ia&W7h<_~zXnnx`+x`WAE%=`6>z19`%w
zmqU}@Bc|ZtWkg}=>>m#8lL>^Pj`4Ys@&(j}yJgRy*9DodrWPzu6P3O_%gdSk)cYfd
zzgg6>XSOKIPjDH#;PKajJUAtLcunuQ0G>@}>&B{j7S+(nPXa?>I+c~tV>0c_bo^`L
zy+F@@ru@mW9}S2-5Z>Q%(W!YuH4AjUAH;2ac|#j859D4=T}-<0!lRQa6tRhK>V
z&Z9W2(#A#(Pxs;b2fA*his{Nak*m^XAH`C2;Ir)u=@Z-7bFbdqz<{9DdeO(*(Im*D
z?p;MZv(aE-Cyw})>I|p1qjJgA9!YPfG;g0(6+D{{Gh<;Qv>}wZXym1sScuqHI8DH(C&-a`x%|Famw!)IV5sCy}d6Y7hL8ZwJG=j
z;f5df5An~v&KJO+8X~MNwE5#RNt+5T4W;hZiX8F#cn@35@bdO=XhcCdYxxMg@a{qN
zt>xwhp)XkuOW>f|oq@c4-~Hp?6mn7W+QkX`N3i3Xts|%$s(QrGk^eDulI8tXy+yI*
zV|^*^Sk(cj5d-naYYC5<=Q0?rdW
z-#PI2ZxKG>Gum8z`wBT(rOT#pT<`E={{wj}Fw}fyly?E~q2+nMDdzLRLRb5D1-e&G
zQaSoRW5eT)xNa9zNMb8zS>2frNLh7?R5%;p91$WE#7v|%QrgnzT#*#g3VvA^tj!}D
z0Az>H9y4hqd%DE;sReBztKhh-T;)cuL&bd-3Kc>T1MiU+h)xp`Cl9z7m+sd$y9)}^)0U8QQEDn<+!Trtm(L~
z%N4>f*Nu0xZvJ*oOoJSXs#X3Cqf1Mya{QUPB&>=g5}W*?)SI2ToCF6Bo7)mBd7(ZR
zRN*0%3s$JUKbe;YM~~ziY`?d>XUe?eBk_rllUt17xQweB^)czE74k;t-~16hR=f!=
z3x6NK2m+VZ1@HIGUQ3ncLhD|A%p}S_=C+dE952@4#kXWv=P_UA
zD=>LvehL!Vj!C1^>Geiu%@TWFu{n&{wQ0fb-uE^r6yt1(l!4Sdd|S}fQ8igc&qn|D
z=kyR5g7`avNcwo@WF+$fr3`j4C|}c|kJXbLGkiWiNW}!#l9jkwRn!7*A;?wfVN?%g
zqcdA`{arOg=n^c0*n}w}uuVQ-Og}}-Jw2vU-=VHHQ_h>yqk|*Gnd7owbjM}FD+puZR)l-|yJRhF45ax*m9HJma`hMw
z0tXAFe^~+oEt06_3`a=OL^@5$&Tb1&Z9$#&J=8nv38@Y*1I>u>AosfLDR9RWWeiQO
z5}L&)M_ygwMYfN92hgPFdR^OKs~`4YaEE%lhVi^T5`*K7LAvKV*&P+T4!Gh=OC
zYiDm1S#+*~2rj&_5NzO~5RILkh(~a21QccvKV2EH1;Hrodo;yIJ7_gFM}@`HM1Vu_z)LT{Iy0XxuTuuZ>*D|*w)DkfyFwE77f
zWF~f1q@C#N)B4|UhXaW-cDJ{U#x`WC??>CD7;q$mb2mfoKNM&A@cQpP2=?En{mV8O
z5gKV6?ooL=t?JxBCC#9IE2lhHk;=WNO+aGM1(|!Tsu}?=3YE&!iQ}3co13UQDP7LI
ztT@tW^$81_YJIj>_xxE)@#9@4L$PI1|BS1RrsbnMN`GeWKw42$v?Z`XK#bu<%U?hy
z^YJllJHz9UV*=BfV^%h`>JU-WYO#q9b4tZc20s7GJ(5MPCrQ|}9fbo-CtPw?>_1%EzU!>Q=a`p#_N<#M-s825
zKos7wLN@ZN=RGf5+{H&EV9Jjt(;-LYhm8-}uHUGB%f{hdM$MIJ=l_D5cv=oK8m8_JI?Ibo{=(+i5b@!Z
zmi(raFVo2^ghx0z2
zQ1)`I`a8z9{nqsEQP&_xbEYr9RNJ;$66uE;G%wej9*KnaE7l5Ososy&gDhC8ajxf<
z=5SlzL6TtAeI5=u`^7QW_jsw|&QH<^qNE6^`clG#az@D{$g(h-trUFyI~4wo3HAs6
zeO(*ag3^Le#pNuIc66p(_io!RT}eejVr!Oo*205`Zr`dv+9W2q^Wp>xDbLG}9Ndol
z>XJpwHu$(%xgtL(95xh1@l)5hu9YS}eEYKy`mJrH*SX2Q;SUR*Hupoem6ugi?_Dx`
zgjg=Nl2BBCF3A1%29%!-97J~;LZuxAwZg2BlQYqla)CQH(DqHCuGMHt@#opGT0d%?
zu#je(kJRK-c&Z;reeUvGtmi_r1KQLbD7Ur-BBd0>AA7fZ>%gx*(o8IXH4)bBM}Bla
z@1F4eKxmNce_I)MCGYqrn>RBNAb)zsfQL9wnPBn9pA}K8VpIhByFcXH{ADk0O6BJ#
zZ-jWQB|hqjv~BiAeRNo@Pg`0s%>Ql-MJjoh3%D3uxCG1$A9a9+A(rM8td@=rVvY!K
zcpfvMP4w4WbRDJR>Csi_&c@EIL#|L0^1@mR|
z4iviOP|saD9ETMd8;n%;;b~ug4cRArT={32f~ZSHCL5u|S71PuLd(ub6!)lWz>32{
zel>0;Osvk57l@Yee|bK8OfHaYq%mLB$_Knzmh1NRr&{C}NtUzXn|+s2kJ?NTqL&;{
zNI6po-N?T|15FI#$3&4~5GSfITZ=8K5*@bmNA^T>;cPS)#GkxkAvq1*U3<=z{S
z%cE)bZ=^-k;7G0!?}#z-^Dcz)kZ^H3u{-^$vrcib9s%rK-dAD5j+PTXv8O|i$8t(f*
zifX+lmjOXtH$VbTs#LtZw=(D_>z7RbB6Y#em)nH!ul&tXH;oH2JCz#my)P7JlioC#
z26*-fon(QNRIUM`JZp*>xJn$TX&c~!v(qg#&|K8tW#dX_u!et+vpvBDaTJ24fH
z(YSP7)fIhHZiy=lEk25oM|QPRyO8=X&$5cLXH#?a{p&8^Imov6gfe
zCOfFT`uSRuhz1y5hxkVc+iM<<7%ex#+PFr7<_zKy_r;S0wIphBAX99`&m-x{uhI^y
zTVcbWZ4|c-Afy2SF>BLH1&!6+T{mC$|5>;5ZYGUM+U-|OBRDz$2hqLAhLKr9tDCrM6PnDsRnJX~)z)d2PBWB7)
zxphFKRftw^l;2}^b|1-xBo4=2F=!HvR!vcg@esgY&q$N&e|z5iB~fRCBlm4D
z0iHbRQDO92Wxr42!@Kr6#elQ)PKA%CsZzRz{pqdbEH_dcJstsm&F>p^8V4DGk^AZ}
zy`nyItHN1oLZ7U%S6A
zk#jt(HV@aIIn3vxT}6&p2g^4kE&Uh?G4ufpOfdtUW{V8rf$
z387klgGX0)kLp7_`knX6)82tvGkj+`ZrLgk|1b%R19Vd|sYSzjA2y)5F$fE3SfvlWz1n*}y^;S*EDyg2y;~x?<-r$+nXw@28_+-BagJm)l
z0{OMB9rjox-ph(zNgu^>GdyDqiXtHMmyRleku*?3y8rH(ZY{&?CO+QV62fhVl0LfM
zi$8*Q@Dr;%hV`$h4N=+O9HxDQ41bWadPH_9n%m9~cSMSA>!t_uVxLUhxyI45ul2jR$B8|N&Up32^al-2}8B&4<}a<9#bT$)Y_CP
z$bh9)*@3MTHd%s93-=Ga!-PLtSrt|7mZxbOSzY~QG0mOO$%cOUpx@Qn6>fr&){$8a
z=8BMV`O-pyQD3xDtbQ9_vFZcUK%Mw_YG7~F?3!GI@?zS&$M$H@V5=sGRZ%Lm!<(7#
z#{b@=raX7y?Fd_~P*4(iV6p(=|%?|C{cMcN<9V`;irJSptcN(C7D$`_-hRKf|?1Gx0Nogsa@;j)>FA
zC{*XBaNa+(p#g*9j{BLvhqV(@o!#GDYkb_gLGxr!^liw4bY50qB;Lt_&NtYvLRi#~
zDcMNKKr(QaYR>qTtT^Ch>A7d{`j9Av=zP~|)AuY_jSMoq9Tj>>vHhU67f{Vi5PjuM
zX3*5Ib6YYj)*N{dh%_OL-4v
z*Szk3FH`dVI}~st&|mP`a}!2SgELlt`aXh-o6ykK-}GKb*`ZcwaSfn7+S{N8l8d@T}|w)ZK0IA-^BRfeJypSXm-Tu6F%mcGZ#F6Jp3?V5~BZL
ziG{dQo|qf&jOrdSION2AOJ{kzVr-R0Eaigl?XhC!YgtgER*fr1B+B1x)XViZlJLxX
zVIfG8^zQ<>)DZ9cdu4HH!oV%S9J8m}e0#)j(B9FBrQ@cuQztl+J^LsG*z6UhmGz?R
zHI5Xf8|~CnxIf44`6Lp{g2NexA0E$Tpvl2Tj~{
zorZ`>kLs9g^@uv22U^wMd@Aw~`mz23;+D7){Yab}z{OVmj)kb8?}Q)FR^-p}-EV-{
zX1R>=zl)KK=P!#Zkp#g~iDMdSA#dB*%IF^xvGxye6v%p6dDR4GCOefVxeo_@
z1CF?hBK~l6#Hrr4p2IJ<9-U8{otTsUFc%CXKd%}6)L8%*!RX2HdOz1X&%pL1h{;Gk
z+%0@&g*!1hx8hTSDp?7!2Y0O%HDZoa;OhjG8O>1H4z5167S?ZU@-wrZeN+1#8hed;W3j{0GC3UHPe-K?o>2e7Iij8iuDqb+)4b}U
zQK+17LFnMflDrNY6^Yq}=S#0>{O*7>cIvq>wBx>}ZwK=BE4p&g8e1&g%%5^Nb@atB
zcBOLVr{U9B)nL^Lu!}j2R&hGey*u6D>3cwn6qjBa9^DEd*AWR`|Ev;hF1T`e)#eU7Pvp)O9n_eCFFcw
zxTixSpC-wAx@ak6a3kqGfc3ZqxYj~%*>&LNg!yfAT5B72a8uiBI|H4(4plM8SwU>1
z3wZ?+IdBf>G-h>XLC;q?HGI_r9eMG=xlabN#-W3!19LX79lY&zp>yS@5TvRGd+ZhO
z4fdvIMjNTgtU%0$lh*ksu3n5+ey;j-K5X>|ev((?B@a#lSHc(af}#UO1LC86m*1ij
z13ew!34#55HB3#{wc;OhLI4o@)x_o*J;%Lqp&^7WGaq&_|dw3+m*T~z1S<0(L;Wz@6a
zI_a7Lurkdk=A8uh2UVnb(G(z+7Zg9uLsw%3Cm|y}>J-rEB+#J=S4UDA-;rowJ};cB
zkUAqXfiC+++LhzqYDRq`U$tD7v8NS!G2-R&tzV@-zY^^66CpIngLfqKs&ik&JW#
zH6?}*K6>Jruwm!E&^540)kWBS9asKS#}3)F|9tHyz3Ox*)WC$T*ueoV+XYt`VHxa%
z2PCC=Xz|iy#T0oC^5bW0gN|*?1e*H+QPDd8=B-`f^2OwKz#jGGZNYEI
zIzIyp2l_cw7d894UoavMMK8l|$C!;zQ@+$vPUh
z04xF;@0GtTc=bL7qsvSQnHnm2=Y)&rp%0+sJPldV1$}wX2Ul4^%7By9Km_5b0eN{E
z936qAI#V6W^ZMyPU&($z^gb^tm@d^%vS4bw&Xnj8c=m(v>E_p6E?_3a1t^k?2*6ViV-|~fam$(@Qqx%NtYIxrg6JngrxbJ|w>$8Etc>xVt6H-*?Q
zKfy`zL$E?!mQ!OCU1d_JxKStms&43tCH~V{@t;9p+TIhG8NZI(fiM}b{Jh?)&QDya
zM^}5r+}nZ8vPTe_l4KPl@;;?8@;r~DTS>aedt7{=mnSc|ln#$1zRdv2TuhC`UI5FL
zMyVVB)ACyYa|k1I_WB~$ku_hIr<<@VBH6~LvQ;Iut1o3GBMr(o164+P(lXE)pj5Ay
zm)$@P7Qtvh$oqyx!{qTGzZ-~Y;#}Bp%O)!$ZL7m8<1=B@p7U!n4}aAMr+(Ebh*r^4
zzm!J?+~?)5%EOZ!5dA6xE_v$md~XLfQifjyUb(y&HWo{^efsk4S1efPPvv&N_WiS+
za)POhuiMuF&SOAk
z0A?p22u;B7#g6BuUs;tU1f|3VHCs2pyQ;%d+
zCw$Sxqk)rv(jy3sMoPov>Zknh#;sw`UH6)NEZWqcG22CL)_w*&wDWbg%~jb
z;nLd(cJ%az{@eE1_TN2GH~Rb_AREx(aq1_ZmAr2rmyWbdmL6yvDzA7@7F>t&(&^nF9R+*kQ6(i?Ua0JcALHERQVbZUkJW(c`nS(G(QOB
zYy;Ht(Cr~#^&kDJOTH^lz2NeLyzI>KA3jMuUru!Kqz4F1^r*n;P+oF^qI)PEg78!i
zICRTbu8&0A_Iuz*7L52Cs0!n~ak_0G#YmHS^^;nY}ws#8=cBjltqIl|0U;Ps@L-rPft@Ff}Yn1Pvtdx=3>gM`ct3!RMI#aX={$xeCupZ
zU$9$%Xe35gf5Z@f3y@XZ2R3y+<%utdzD6?jc&Qz!TqC{Wsj&ckhoo^r>#(o%_Sl~K
z>?+i3V%k5IcK&K%zB|B{BvFr&zmJ6`f5a!R1g=3MrTcOzkMf?D=JA;WyZAuL
z@sa*8vru_KM|BHg&RTB=;unYto8wQVH2|niZ})N?mBCHCdQkE}?_U-!9gEF$(wyw#;32H_Fp4dA@JP+27x8}Nv-OwBU&S8`6HSj!rg?N(oKIF=xODbXn6$^7&88)L!~AM_&<26!3kG#73HN@9CkW3U(B-$v
zDX%j1p!7&y<&g)MJ<5B&*C{zc$?LZOT2J$3T7|cK2A~s*eGqoeGp311e}y!2QVz
zl4ybw*Yt~g%9D&B{8T2*OI9%DH<~^N+r5`B_LyTT-WZxa#}2;`4n6f)!`FsWB@1?c
zJ+f#Ysk9$*ruIozcKY~;*9fXymfkFybS+xvfANeBPSeddr=|I^kehrVTs`&7ntnxC
zvO#ckX}I=NYHW~gK;g2j8JLzy^U~ovsAxdN&C?p$;?IlZ1ks`Tt7`yd^`-Tg0W{fK
zy=$xDJhSYuSpYYIT{epKojiH+MB3r&1v5*#0wNtM3*FNshYrz&10@emYLpjGI=w#1
z`h3|wl^xdbxh|_XoILhoxP1Opd}(NMa@w|~epAys3sFCN3~ub4tnDl$JH2h#sO0?-
zU1j8pr)SZm%forwQTg!MLYSOwJni>o==}Ma{q{3zTjjS{OwX{#;{Bm-xVdi;qRX}|
z30peMfBOPaYPah0{z1!%f#^W*v&upziC4*0^5By5{1i{+l$RV(Nj8$V+vq*Ejy7e#
zF_|%Z!TJ=h)n@NRA_Z%N%@5g4Dz;H7$1MP(mKh&kW{{((09RhY^AKD|y(-0*9^{~r
zS0e9|_@WCUlj5nYugBLB_1h}-TE5kX?&C-Hhmi}X<4ZpAjk*3_`|ZHfVQ%t@H@P9s
zZo^mR`)xYB(tZlbAE0p22ST51$Bpm2
zFg4KM7sgLN7iO-m;da2D3S3^aO-0=8fVWpRLjzYH=>1mS`z%?>iKl$3Q+dx9jxOnw
z9FQcsWPqv*zAXX**j+pJshDZ;pOy~*enSwml^(g`DS(`nA3g}83D%SKeBlbX@*bvv
zRh^Pi(7g^Xqw>g$9-&S9Lx*kK>;@j`%!GD=*|-0yz1z)((DOYmzI12tknwdQWBGU$ThCj7mJa|p!HT6@xLA29t~#oqv{9)bsYkEJ*Lwi)Uo}qwaMG@7ap8iWGHNAL
z&I40C&1VH9tDFzG*FkyZQ(az=wcpkcZK1(L$lb^S{&?Dyx
zwufKyoqx$Xe{R}-8EM;i(H=DT$#>=PpC1sh(dyKd=C>DPXQ9gr>@m2~f7$PS6%Ncw
zzKr-OjXViR8&qELRym;BsHaKZ)4)Xsa;WdX@Vgv4
zKPQ!|AJcywdbvfsz8+iYKK1O{o({x8pf^4phz?Z$V0)uD%U9qEKP}d2;>p8{XNuPO
zo1gZ_)qb1yGrLje$6st>&;ybOCoT*E+zw3IrXp(@1o$fPh<%z|8?AcVyLF5&1$w~(>rA2$}-E{8xy=}hCHMljh1pih^nMlqkEhQhqf
zgwW~zX-a_!DHdKnOONzRUeK3=rbBcuCtNZejXueE}OGZ%qG%q^PVi5b1#i`5;U_Dahq-be~ifns8sv?(^snBnSy2TJ$9(UJA0IhvgGaIH?Zp=D>A2>*ND31Z{+koXq*WtGS*WT!<_Uni7MWBlpFLv6y%w2X5
z(`OExKwZ56q>iUmQ!Yy;rF;FU4xjgOUPmTw)Bb!{WlJxwqb_cZg*W`zx|=IMomf{j
zUHPa-4c^zu1|#@MV`LCuYcURsV;G;93|sDeW9S}e?h8cNXCD_28+lnKjD(J*&xfA!
zpM;L`*X-Hs@-}TtXWm(Jg(n+}9XrEp-WsmHGTtoEe|%Oa#CKw$7%+?EWug9CUK9E^HuqIxbmz+@+la)r61SIy8YS71%5W~k
z6RvhByVEkF`*O&nbZB0e^1j@jJ$rUro0+y4&Tiehb^bl?dC#H^0*#$P(Hc_AX8^u#
z7BI|s^QqE-8eHuOaOc>s=FZ2w>-Ii9hif*1?{8-qK`>0*@;~3nq
zOm1n;_T4`ckEQXy4yVfB9p>jAwldAdpghmC-?r|yPjshOe*D2d#Xx!d6%Cw@&tO5q
zcl3~m0;qf=
zC_NMQZTmxEKAb2o+7D~EV+{85nta%0kM{1cMKoLDtEh?Z_OE@l|LW+)xQCyny);}k
zXcS|!8o%<`Ci~!1hCFhq%vF0tRw%w<)!TuNa5C)4{YdBuPuiPUYrkmZ7cYk0;S*t~
z_>J&v>8Ha~*=`RSq{_7aVEjZkGava4#3yDyBz1OZ@|BLSJbZj7e&Ec2RJPZ?z%+iw
zK1iA`G(8jG=>UVk=A3;?(7v%bZUf9g(|N$Ya(u<=C2cet_u8OykJY)U8vDp_e{_Bv
zsfIS<)r1^^uaw~cPm}np0pL5cD3V?MBW&fLmJa|JX&V>RY}6v4oK>*oWFuHpQbkmj
ztjd9lE*ek?eBQ1a&y?rGf3_>ZN$dET!;ul`Zg<=FOuoN36dtfgeRj=~n#RyhB-4K0
z@Y72!G+r7%kfc2J$UgDNs}sl<$}$B;Mpr?29GC26p-p!_6uSFukARFcnEMOsypNdu
z)jbI_WSS)n=Z=JLE51KGxA@P)#qxuh#g^<1zcBS|e1^}O54LLn^ge>?Pz+Fry!?j`
z&LN&+!ukm1CeIx-R?Y^2=C&N?Gi0Be&V|oTo(s?0&c#a>!>q*FW)S!hTf2ITb-GTo
zg}S}1DknM+n}Irz_k7Aohh(Hj5W13ZgRud!o@Sp1k81#}9t2uG0Prd>13+~VuuLPy
zAq6GF$w=}t3YKJnDW5zE*;HOU_{Xio|MAjTIBfUBt0kJ;L7N>MTD%kJd&{^Cc&Go_yj7nfEJ0!udSFHi0dL-qm_9Tb~6
z@P~x0-f!h~K9Lcg#zg*#PM-4UA3gPaD3=z)z?R0h0~bs4;p5|H!V_~gNUf4+QOpj0
zWn?5gV7~mMee>|OF77IhJ!$f`3I`&qyf2fb!_z3kegK+18h&l1Ug34(&9Ck>S%Eo`
zKYH|N&KhpfJ_%;_cXa&B&-~24rnq-7RR?difMo?zdb4;>mS@9nEnN*4a@XGfrSnpuYc9FPP07%5ib6$wzGzqKp6oS>1M`#N
zH?AHFM;4}Iy=(b%&TbXHVBfXaZMO!y%_pB^i)gEzJISpTe)z*5{1=|*W(Qd0s
zc46fjwLDc=tHKQVYHP~iTqlVcfPWLvtEm?#WZ>yg882TCm#l}JpvTHH;op|W!pzEj
z6}{N1_@$-!@NZ_U0hKB{8~Xy-bM%ls=eKwJdC7%_&q3XEQVu!-kVXrnjCg89g3vv#
zPNeeEi!P?w+-mf8ciPKBUyr{Xun&^iI>vjg^EbZ-zxSsZoI&7$;%~>N2<63zur&U(
z*rGR3zJBG(mGHT#sTC@GTU^0NGONkS_oP@Y%K~~WV~FW%Y3E0G%Ljn;Jpk4KWVfJq
z3Y5o+-~)0hqr3(NbtW1=a?CV1=meLKJr(+Ae#frM-|#Be1s}{E
z4LeJp4ohRt*eAN}2TaL=pJE_Cbgn#1&zF3~VC4LP9wmGy4or)|nQ`F%l@rf~iA$#>
z7Dh@7;Ww`ywiPxE$Oe&g!ZaKIkLVkb%V%cqpCT18hG(FLU^&8r-be@Wa