diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 5b4bb470..55da51a7 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -15,10 +15,10 @@ ] }, "csharpier": { - "version": "0.25.0", + "version": "0.26.4", "commands": [ "dotnet-csharpier" ] } } -} +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index ddc54b53..15cae5b1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,3 +7,7 @@ dotnet_sort_system_directives_first = true # ReSharper properties resharper_csharp_max_line_length = 120 + +# dotnet code quality +# noinspection EditorConfigKeyCorrectness +dotnet_code_quality.CA1826.exclude_ordefault_methods = true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf47cd4e..88b5eae3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -209,8 +209,6 @@ jobs: files: | StabilityMatrix-win-x64.zip StabilityMatrix-linux-x64.zip - StabilityMatrix-win-x64/StabilityMatrix.exe - StabilityMatrix-linux-x64/StabilityMatrix.AppImage fail_on_unmatched_files: true tag_name: v${{ github.event.inputs.version }} body: ${{ steps.release_notes.outputs.release_notes }} @@ -244,7 +242,7 @@ jobs: SM_CF_CACHE_PURGE_TOKEN: ${{ secrets.SM_CF_CACHE_PURGE_TOKEN }} SM_CF_ZONE_ID: ${{ secrets.SM_CF_ZONE_ID }} SM_SIGNING_PRIVATE_KEY: ${{ secrets.SM_SIGNING_PRIVATE_KEY }} - run: sm-tools updates publish-matrix-v3 -v $RELEASE_VERSION -y + run: sm-tools updates publish-matrix-v3 -v ${{ github.event.inputs.version }} -y publish-auto-update-b2: name: Publish Auto-Update Release (B2) diff --git a/Avalonia.Gif/Avalonia.Gif.csproj b/Avalonia.Gif/Avalonia.Gif.csproj new file mode 100644 index 00000000..2d72f153 --- /dev/null +++ b/Avalonia.Gif/Avalonia.Gif.csproj @@ -0,0 +1,16 @@ + + + net8.0 + latest + true + win-x64;linux-x64;osx-x64;osx-arm64 + enable + enable + true + true + + + + + + diff --git a/Avalonia.Gif/BgWorkerCommand.cs b/Avalonia.Gif/BgWorkerCommand.cs new file mode 100644 index 00000000..aebd44dd --- /dev/null +++ b/Avalonia.Gif/BgWorkerCommand.cs @@ -0,0 +1,10 @@ +namespace Avalonia.Gif +{ + internal enum BgWorkerCommand + { + Null, + Play, + Pause, + Dispose + } +} diff --git a/Avalonia.Gif/BgWorkerState.cs b/Avalonia.Gif/BgWorkerState.cs new file mode 100644 index 00000000..1b09bba1 --- /dev/null +++ b/Avalonia.Gif/BgWorkerState.cs @@ -0,0 +1,12 @@ +namespace Avalonia.Gif +{ + internal enum BgWorkerState + { + Null, + Start, + Running, + Paused, + Complete, + Dispose + } +} diff --git a/Avalonia.Gif/Decoding/BlockTypes.cs b/Avalonia.Gif/Decoding/BlockTypes.cs new file mode 100644 index 00000000..2d804d5b --- /dev/null +++ b/Avalonia.Gif/Decoding/BlockTypes.cs @@ -0,0 +1,10 @@ +namespace Avalonia.Gif.Decoding +{ + internal enum BlockTypes + { + Empty = 0, + Extension = 0x21, + ImageDescriptor = 0x2C, + Trailer = 0x3B, + } +} diff --git a/Avalonia.Gif/Decoding/ExtensionType.cs b/Avalonia.Gif/Decoding/ExtensionType.cs new file mode 100644 index 00000000..5db6d575 --- /dev/null +++ b/Avalonia.Gif/Decoding/ExtensionType.cs @@ -0,0 +1,8 @@ +namespace Avalonia.Gif.Decoding +{ + internal enum ExtensionType + { + GraphicsControl = 0xF9, + Application = 0xFF + } +} diff --git a/Avalonia.Gif/Decoding/FrameDisposal.cs b/Avalonia.Gif/Decoding/FrameDisposal.cs new file mode 100644 index 00000000..bf4f00b7 --- /dev/null +++ b/Avalonia.Gif/Decoding/FrameDisposal.cs @@ -0,0 +1,10 @@ +namespace Avalonia.Gif.Decoding +{ + public enum FrameDisposal + { + Unknown = 0, + Leave = 1, + Background = 2, + Restore = 3 + } +} diff --git a/Avalonia.Gif/Decoding/GifColor.cs b/Avalonia.Gif/Decoding/GifColor.cs new file mode 100644 index 00000000..222d7303 --- /dev/null +++ b/Avalonia.Gif/Decoding/GifColor.cs @@ -0,0 +1,36 @@ +using System.Runtime.InteropServices; + +namespace Avalonia.Gif +{ + [StructLayout(LayoutKind.Explicit)] + public readonly struct GifColor + { + [FieldOffset(3)] + public readonly byte A; + + [FieldOffset(2)] + public readonly byte R; + + [FieldOffset(1)] + public readonly byte G; + + [FieldOffset(0)] + public readonly byte B; + + /// + /// A struct that represents a ARGB color and is aligned as + /// a BGRA bytefield in memory. + /// + /// Red + /// Green + /// Blue + /// Alpha + public GifColor(byte r, byte g, byte b, byte a = byte.MaxValue) + { + A = a; + R = r; + G = g; + B = b; + } + } +} diff --git a/Avalonia.Gif/Decoding/GifDecoder.cs b/Avalonia.Gif/Decoding/GifDecoder.cs new file mode 100644 index 00000000..adc26bb0 --- /dev/null +++ b/Avalonia.Gif/Decoding/GifDecoder.cs @@ -0,0 +1,653 @@ +// This source file's Lempel-Ziv-Welch algorithm is derived from Chromium's Android GifPlayer +// as seen here (https://github.com/chromium/chromium/blob/master/third_party/gif_player/src/jp/tomorrowkey/android/gifplayer) +// Licensed under the Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0) +// Copyright (C) 2015 The Gifplayer Authors. All Rights Reserved. + +// The rest of the source file is licensed under MIT License. +// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using Avalonia; +using Avalonia.Media.Imaging; +using static Avalonia.Gif.Extensions.StreamExtensions; + +namespace Avalonia.Gif.Decoding +{ + public sealed class GifDecoder : IDisposable + { + private static readonly ReadOnlyMemory G87AMagic = "GIF87a"u8.ToArray().AsMemory(); + + private static readonly ReadOnlyMemory G89AMagic = "GIF89a"u8.ToArray().AsMemory(); + + private static readonly ReadOnlyMemory NetscapeMagic = "NETSCAPE2.0"u8.ToArray().AsMemory(); + + private static readonly TimeSpan FrameDelayThreshold = TimeSpan.FromMilliseconds(10); + private static readonly TimeSpan FrameDelayDefault = TimeSpan.FromMilliseconds(100); + private static readonly GifColor TransparentColor = new(0, 0, 0, 0); + private static readonly int MaxTempBuf = 768; + private static readonly int MaxStackSize = 4096; + private static readonly int MaxBits = 4097; + + private readonly Stream _fileStream; + private readonly CancellationToken _currentCtsToken; + private readonly bool _hasFrameBackups; + + private int _gctSize, + _bgIndex, + _prevFrame = -1, + _backupFrame = -1; + private bool _gctUsed; + + private GifRect _gifDimensions; + + // private ulong _globalColorTable; + private readonly int _backBufferBytes; + private GifColor[] _bitmapBackBuffer; + + private short[] _prefixBuf; + private byte[] _suffixBuf; + private byte[] _pixelStack; + private byte[] _indexBuf; + private byte[] _backupFrameIndexBuf; + private volatile bool _hasNewFrame; + + public GifHeader Header { get; private set; } + + public readonly List Frames = new(); + + public PixelSize Size => new PixelSize(Header.Dimensions.Width, Header.Dimensions.Height); + + public GifDecoder(Stream fileStream, CancellationToken currentCtsToken) + { + _fileStream = fileStream; + _currentCtsToken = currentCtsToken; + + ProcessHeaderData(); + ProcessFrameData(); + + Header.IterationCount = Header.Iterations switch + { + -1 => new GifRepeatBehavior { Count = 1 }, + 0 => new GifRepeatBehavior { LoopForever = true }, + > 0 => new GifRepeatBehavior { Count = Header.Iterations }, + _ => Header.IterationCount + }; + + var pixelCount = _gifDimensions.TotalPixels; + + _hasFrameBackups = Frames.Any(f => f.FrameDisposalMethod == FrameDisposal.Restore); + + _bitmapBackBuffer = new GifColor[pixelCount]; + _indexBuf = new byte[pixelCount]; + + if (_hasFrameBackups) + _backupFrameIndexBuf = new byte[pixelCount]; + + _prefixBuf = new short[MaxStackSize]; + _suffixBuf = new byte[MaxStackSize]; + _pixelStack = new byte[MaxStackSize + 1]; + + _backBufferBytes = pixelCount * Marshal.SizeOf(typeof(GifColor)); + } + + public void Dispose() + { + Frames.Clear(); + + _bitmapBackBuffer = null; + _prefixBuf = null; + _suffixBuf = null; + _pixelStack = null; + _indexBuf = null; + _backupFrameIndexBuf = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int PixCoord(int x, int y) => x + y * _gifDimensions.Width; + + static readonly (int Start, int Step)[] Pass = { (0, 8), (4, 8), (2, 4), (1, 2) }; + + private void ClearImage() + { + Array.Fill(_bitmapBackBuffer, TransparentColor); + //ClearArea(_gifDimensions); + + _prevFrame = -1; + _backupFrame = -1; + } + + public void RenderFrame(int fIndex, WriteableBitmap writeableBitmap, bool forceClear = false) + { + if (_currentCtsToken.IsCancellationRequested) + return; + + if (fIndex < 0 | fIndex >= Frames.Count) + return; + + if (_prevFrame == fIndex) + return; + + if (fIndex == 0 || forceClear || fIndex < _prevFrame) + ClearImage(); + + DisposePreviousFrame(); + + _prevFrame++; + + // render intermediate frame + for (int idx = _prevFrame; idx < fIndex; ++idx) + { + var prevFrame = Frames[idx]; + + if (prevFrame.FrameDisposalMethod == FrameDisposal.Restore) + continue; + + if (prevFrame.FrameDisposalMethod == FrameDisposal.Background) + { + ClearArea(prevFrame.Dimensions); + continue; + } + + RenderFrameAt(idx, writeableBitmap); + } + + RenderFrameAt(fIndex, writeableBitmap); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RenderFrameAt(int idx, WriteableBitmap writeableBitmap) + { + var tmpB = ArrayPool.Shared.Rent(MaxTempBuf); + + var curFrame = Frames[idx]; + DecompressFrameToIndexBuffer(curFrame, _indexBuf, tmpB); + + if (_hasFrameBackups & curFrame.ShouldBackup) + { + Buffer.BlockCopy(_indexBuf, 0, _backupFrameIndexBuf, 0, curFrame.Dimensions.TotalPixels); + _backupFrame = idx; + } + + DrawFrame(curFrame, _indexBuf); + + _prevFrame = idx; + _hasNewFrame = true; + + using var lockedBitmap = writeableBitmap.Lock(); + WriteBackBufToFb(lockedBitmap.Address); + + ArrayPool.Shared.Return(tmpB); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawFrame(GifFrame curFrame, Memory frameIndexSpan) + { + var activeColorTable = curFrame.IsLocalColorTableUsed ? curFrame.LocalColorTable : Header.GlobarColorTable; + + var cX = curFrame.Dimensions.X; + var cY = curFrame.Dimensions.Y; + var cH = curFrame.Dimensions.Height; + var cW = curFrame.Dimensions.Width; + var tC = curFrame.TransparentColorIndex; + var hT = curFrame.HasTransparency; + + if (curFrame.IsInterlaced) + { + for (var i = 0; i < 4; i++) + { + var curPass = Pass[i]; + var y = curPass.Start; + while (y < cH) + { + DrawRow(y); + y += curPass.Step; + } + } + } + else + { + for (var i = 0; i < cH; i++) + DrawRow(i); + } + + //for (var row = 0; row < cH; row++) + void DrawRow(int row) + { + // Get the starting point of the current row on frame's index stream. + var indexOffset = row * cW; + + // Get the target backbuffer offset from the frames coords. + var targetOffset = PixCoord(cX, row + cY); + var len = _bitmapBackBuffer.Length; + + for (var i = 0; i < cW; i++) + { + var indexColor = frameIndexSpan.Span[indexOffset + i]; + + if (activeColorTable == null || targetOffset >= len || indexColor > activeColorTable.Length) + return; + + if (!(hT & indexColor == tC)) + _bitmapBackBuffer[targetOffset] = activeColorTable[indexColor]; + + targetOffset++; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DisposePreviousFrame() + { + if (_prevFrame == -1) + return; + + var prevFrame = Frames[_prevFrame]; + + switch (prevFrame.FrameDisposalMethod) + { + case FrameDisposal.Background: + ClearArea(prevFrame.Dimensions); + break; + case FrameDisposal.Restore: + if (_hasFrameBackups && _backupFrame != -1) + DrawFrame(Frames[_backupFrame], _backupFrameIndexBuf); + else + ClearArea(prevFrame.Dimensions); + break; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ClearArea(GifRect area) + { + for (var y = 0; y < area.Height; y++) + { + var targetOffset = PixCoord(area.X, y + area.Y); + for (var x = 0; x < area.Width; x++) + _bitmapBackBuffer[targetOffset + x] = TransparentColor; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecompressFrameToIndexBuffer(GifFrame curFrame, Span indexSpan, byte[] tempBuf) + { + _fileStream.Position = curFrame.LzwStreamPosition; + var totalPixels = curFrame.Dimensions.TotalPixels; + + // Initialize GIF data stream decoder. + var dataSize = curFrame.LzwMinCodeSize; + var clear = 1 << dataSize; + var endOfInformation = clear + 1; + var available = clear + 2; + var oldCode = -1; + var codeSize = dataSize + 1; + var codeMask = (1 << codeSize) - 1; + + for (var code = 0; code < clear; code++) + { + _prefixBuf[code] = 0; + _suffixBuf[code] = (byte)code; + } + + // Decode GIF pixel stream. + int bits, + first, + top, + pixelIndex; + var datum = bits = first = top = pixelIndex = 0; + + while (pixelIndex < totalPixels) + { + var blockSize = _fileStream.ReadBlock(tempBuf); + + if (blockSize == 0) + break; + + var blockPos = 0; + + while (blockPos < blockSize) + { + datum += tempBuf[blockPos] << bits; + blockPos++; + + bits += 8; + + while (bits >= codeSize) + { + // Get the next code. + var code = datum & codeMask; + datum >>= codeSize; + bits -= codeSize; + + // Interpret the code + if (code == clear) + { + // Reset decoder. + codeSize = dataSize + 1; + codeMask = (1 << codeSize) - 1; + available = clear + 2; + oldCode = -1; + continue; + } + + // Check for explicit end-of-stream + if (code == endOfInformation) + return; + + if (oldCode == -1) + { + indexSpan[pixelIndex++] = _suffixBuf[code]; + oldCode = code; + first = code; + continue; + } + + var inCode = code; + if (code >= available) + { + _pixelStack[top++] = (byte)first; + code = oldCode; + + if (top == MaxBits) + ThrowException(); + } + + while (code >= clear) + { + if (code >= MaxBits || code == _prefixBuf[code]) + ThrowException(); + + _pixelStack[top++] = _suffixBuf[code]; + code = _prefixBuf[code]; + + if (top == MaxBits) + ThrowException(); + } + + first = _suffixBuf[code]; + _pixelStack[top++] = (byte)first; + + // Add new code to the dictionary + if (available < MaxStackSize) + { + _prefixBuf[available] = (short)oldCode; + _suffixBuf[available] = (byte)first; + available++; + + if ((available & codeMask) == 0 && available < MaxStackSize) + { + codeSize++; + codeMask += available; + } + } + + oldCode = inCode; + + // Drain the pixel stack. + do + { + indexSpan[pixelIndex++] = _pixelStack[--top]; + } while (top > 0); + } + } + } + + while (pixelIndex < totalPixels) + indexSpan[pixelIndex++] = 0; // clear missing pixels + + void ThrowException() => throw new LzwDecompressionException(); + } + + /// + /// Directly copies the struct array to a bitmap IntPtr. + /// + private void WriteBackBufToFb(IntPtr targetPointer) + { + if (_currentCtsToken.IsCancellationRequested) + return; + + if (!(_hasNewFrame & _bitmapBackBuffer != null)) + return; + + unsafe + { + fixed (void* src = &_bitmapBackBuffer[0]) + Buffer.MemoryCopy(src, targetPointer.ToPointer(), (uint)_backBufferBytes, (uint)_backBufferBytes); + _hasNewFrame = false; + } + } + + /// + /// Processes GIF Header. + /// + private void ProcessHeaderData() + { + var str = _fileStream; + var tmpB = ArrayPool.Shared.Rent(MaxTempBuf); + var tempBuf = tmpB.AsSpan(); + + var _ = str.Read(tmpB, 0, 6); + + if (!tempBuf[..3].SequenceEqual(G87AMagic[..3].Span)) + throw new InvalidGifStreamException("Not a GIF stream."); + + if (!(tempBuf[..6].SequenceEqual(G87AMagic.Span) | tempBuf[..6].SequenceEqual(G89AMagic.Span))) + throw new InvalidGifStreamException( + "Unsupported GIF Version: " + Encoding.ASCII.GetString(tempBuf[..6].ToArray()) + ); + + ProcessScreenDescriptor(tmpB); + + Header = new GifHeader + { + Dimensions = _gifDimensions, + HasGlobalColorTable = _gctUsed, + // GlobalColorTableCacheID = _globalColorTable, + GlobarColorTable = ProcessColorTable(ref str, tmpB, _gctSize), + GlobalColorTableSize = _gctSize, + BackgroundColorIndex = _bgIndex, + HeaderSize = _fileStream.Position + }; + + ArrayPool.Shared.Return(tmpB); + } + + /// + /// Parses colors from file stream to target color table. + /// + private static GifColor[] ProcessColorTable(ref Stream stream, byte[] rawBufSpan, int nColors) + { + var nBytes = 3 * nColors; + var target = new GifColor[nColors]; + + var n = stream.Read(rawBufSpan, 0, nBytes); + + if (n < nBytes) + throw new InvalidOperationException("Wrong color table bytes."); + + int i = 0, + j = 0; + + while (i < nColors) + { + var r = rawBufSpan[j++]; + var g = rawBufSpan[j++]; + var b = rawBufSpan[j++]; + target[i++] = new GifColor(r, g, b); + } + + return target; + } + + /// + /// Parses screen and other GIF descriptors. + /// + private void ProcessScreenDescriptor(byte[] tempBuf) + { + var width = _fileStream.ReadUShortS(tempBuf); + var height = _fileStream.ReadUShortS(tempBuf); + + var packed = _fileStream.ReadByteS(tempBuf); + + _gctUsed = (packed & 0x80) != 0; + _gctSize = 2 << (packed & 7); + _bgIndex = _fileStream.ReadByteS(tempBuf); + + _gifDimensions = new GifRect(0, 0, width, height); + _fileStream.Skip(1); + } + + /// + /// Parses all frame data. + /// + private void ProcessFrameData() + { + _fileStream.Position = Header.HeaderSize; + + var tempBuf = ArrayPool.Shared.Rent(MaxTempBuf); + + var terminate = false; + var curFrame = 0; + + Frames.Add(new GifFrame()); + + do + { + var blockType = (BlockTypes)_fileStream.ReadByteS(tempBuf); + + switch (blockType) + { + case BlockTypes.Empty: + break; + + case BlockTypes.Extension: + ProcessExtensions(ref curFrame, tempBuf); + break; + + case BlockTypes.ImageDescriptor: + ProcessImageDescriptor(ref curFrame, tempBuf); + _fileStream.SkipBlocks(tempBuf); + break; + + case BlockTypes.Trailer: + Frames.RemoveAt(Frames.Count - 1); + terminate = true; + break; + + default: + _fileStream.SkipBlocks(tempBuf); + break; + } + + // Break the loop when the stream is not valid anymore. + if (_fileStream.Position >= _fileStream.Length & terminate == false) + throw new InvalidProgramException("Reach the end of the filestream without trailer block."); + } while (!terminate); + + ArrayPool.Shared.Return(tempBuf); + } + + /// + /// Parses GIF Image Descriptor Block. + /// + private void ProcessImageDescriptor(ref int curFrame, byte[] tempBuf) + { + var str = _fileStream; + var currentFrame = Frames[curFrame]; + + // Parse frame dimensions. + var frameX = str.ReadUShortS(tempBuf); + var frameY = str.ReadUShortS(tempBuf); + var frameW = str.ReadUShortS(tempBuf); + var frameH = str.ReadUShortS(tempBuf); + + frameW = (ushort)Math.Min(frameW, _gifDimensions.Width - frameX); + frameH = (ushort)Math.Min(frameH, _gifDimensions.Height - frameY); + + currentFrame.Dimensions = new GifRect(frameX, frameY, frameW, frameH); + + // Unpack interlace and lct info. + var packed = str.ReadByteS(tempBuf); + currentFrame.IsInterlaced = (packed & 0x40) != 0; + currentFrame.IsLocalColorTableUsed = (packed & 0x80) != 0; + currentFrame.LocalColorTableSize = (int)Math.Pow(2, (packed & 0x07) + 1); + + if (currentFrame.IsLocalColorTableUsed) + currentFrame.LocalColorTable = ProcessColorTable(ref str, tempBuf, currentFrame.LocalColorTableSize); + + currentFrame.LzwMinCodeSize = str.ReadByteS(tempBuf); + currentFrame.LzwStreamPosition = str.Position; + + curFrame += 1; + Frames.Add(new GifFrame()); + } + + /// + /// Parses GIF Extension Blocks. + /// + private void ProcessExtensions(ref int curFrame, byte[] tempBuf) + { + var extType = (ExtensionType)_fileStream.ReadByteS(tempBuf); + + switch (extType) + { + case ExtensionType.GraphicsControl: + + _fileStream.ReadBlock(tempBuf); + var currentFrame = Frames[curFrame]; + var packed = tempBuf[0]; + + currentFrame.FrameDisposalMethod = (FrameDisposal)((packed & 0x1c) >> 2); + + if ( + currentFrame.FrameDisposalMethod != FrameDisposal.Restore + && currentFrame.FrameDisposalMethod != FrameDisposal.Background + ) + currentFrame.ShouldBackup = true; + + currentFrame.HasTransparency = (packed & 1) != 0; + + currentFrame.FrameDelay = TimeSpan.FromMilliseconds(SpanToShort(tempBuf.AsSpan(1)) * 10); + + if (currentFrame.FrameDelay <= FrameDelayThreshold) + currentFrame.FrameDelay = FrameDelayDefault; + + currentFrame.TransparentColorIndex = tempBuf[3]; + break; + + case ExtensionType.Application: + var blockLen = _fileStream.ReadBlock(tempBuf); + var _ = tempBuf.AsSpan(0, blockLen); + var blockHeader = tempBuf.AsSpan(0, NetscapeMagic.Length); + + if (blockHeader.SequenceEqual(NetscapeMagic.Span)) + { + var count = 1; + + while (count > 0) + count = _fileStream.ReadBlock(tempBuf); + + var iterationCount = SpanToShort(tempBuf.AsSpan(1)); + + Header.Iterations = iterationCount; + } + else + _fileStream.SkipBlocks(tempBuf); + + break; + + default: + _fileStream.SkipBlocks(tempBuf); + break; + } + } + } +} diff --git a/Avalonia.Gif/Decoding/GifFrame.cs b/Avalonia.Gif/Decoding/GifFrame.cs new file mode 100644 index 00000000..ea0e6640 --- /dev/null +++ b/Avalonia.Gif/Decoding/GifFrame.cs @@ -0,0 +1,20 @@ +using System; + +namespace Avalonia.Gif.Decoding +{ + public class GifFrame + { + public bool HasTransparency, + IsInterlaced, + IsLocalColorTableUsed; + public byte TransparentColorIndex; + public int LzwMinCodeSize, + LocalColorTableSize; + public long LzwStreamPosition; + public TimeSpan FrameDelay; + public FrameDisposal FrameDisposalMethod; + public bool ShouldBackup; + public GifRect Dimensions; + public GifColor[] LocalColorTable; + } +} diff --git a/Avalonia.Gif/Decoding/GifHeader.cs b/Avalonia.Gif/Decoding/GifHeader.cs new file mode 100644 index 00000000..16638f79 --- /dev/null +++ b/Avalonia.Gif/Decoding/GifHeader.cs @@ -0,0 +1,19 @@ +// Licensed under the MIT License. +// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. + +namespace Avalonia.Gif.Decoding +{ + public class GifHeader + { + public bool HasGlobalColorTable; + public int GlobalColorTableSize; + public ulong GlobalColorTableCacheId; + public int BackgroundColorIndex; + public long HeaderSize; + internal int Iterations = -1; + public GifRepeatBehavior IterationCount; + public GifRect Dimensions; + private GifColor[] _globarColorTable; + public GifColor[] GlobarColorTable; + } +} diff --git a/Avalonia.Gif/Decoding/GifRect.cs b/Avalonia.Gif/Decoding/GifRect.cs new file mode 100644 index 00000000..01f621de --- /dev/null +++ b/Avalonia.Gif/Decoding/GifRect.cs @@ -0,0 +1,43 @@ +namespace Avalonia.Gif.Decoding +{ + public readonly struct GifRect + { + public int X { get; } + public int Y { get; } + public int Width { get; } + public int Height { get; } + public int TotalPixels { get; } + + public GifRect(int x, int y, int width, int height) + { + X = x; + Y = y; + Width = width; + Height = height; + TotalPixels = width * height; + } + + public static bool operator ==(GifRect a, GifRect b) + { + return a.X == b.X && a.Y == b.Y && a.Width == b.Width && a.Height == b.Height; + } + + public static bool operator !=(GifRect a, GifRect b) + { + return !(a == b); + } + + public override bool Equals(object obj) + { + if (obj == null || GetType() != obj.GetType()) + return false; + + return this == (GifRect)obj; + } + + public override int GetHashCode() + { + return X.GetHashCode() ^ Y.GetHashCode() | Width.GetHashCode() ^ Height.GetHashCode(); + } + } +} diff --git a/Avalonia.Gif/Decoding/GifRepeatBehavior.cs b/Avalonia.Gif/Decoding/GifRepeatBehavior.cs new file mode 100644 index 00000000..4b27a7bb --- /dev/null +++ b/Avalonia.Gif/Decoding/GifRepeatBehavior.cs @@ -0,0 +1,8 @@ +namespace Avalonia.Gif.Decoding +{ + public class GifRepeatBehavior + { + public bool LoopForever { get; set; } + public int? Count { get; set; } + } +} diff --git a/Avalonia.Gif/Decoding/InvalidGifStreamException.cs b/Avalonia.Gif/Decoding/InvalidGifStreamException.cs new file mode 100644 index 00000000..b3554bac --- /dev/null +++ b/Avalonia.Gif/Decoding/InvalidGifStreamException.cs @@ -0,0 +1,23 @@ +// Licensed under the MIT License. +// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. + +using System; +using System.Runtime.Serialization; + +namespace Avalonia.Gif.Decoding +{ + [Serializable] + public class InvalidGifStreamException : Exception + { + public InvalidGifStreamException() { } + + public InvalidGifStreamException(string message) + : base(message) { } + + public InvalidGifStreamException(string message, Exception innerException) + : base(message, innerException) { } + + protected InvalidGifStreamException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + } +} diff --git a/Avalonia.Gif/Decoding/LzwDecompressionException.cs b/Avalonia.Gif/Decoding/LzwDecompressionException.cs new file mode 100644 index 00000000..ed25c0aa --- /dev/null +++ b/Avalonia.Gif/Decoding/LzwDecompressionException.cs @@ -0,0 +1,23 @@ +// Licensed under the MIT License. +// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. + +using System; +using System.Runtime.Serialization; + +namespace Avalonia.Gif.Decoding +{ + [Serializable] + public class LzwDecompressionException : Exception + { + public LzwDecompressionException() { } + + public LzwDecompressionException(string message) + : base(message) { } + + public LzwDecompressionException(string message, Exception innerException) + : base(message, innerException) { } + + protected LzwDecompressionException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + } +} diff --git a/Avalonia.Gif/Extensions/StreamExtensions.cs b/Avalonia.Gif/Extensions/StreamExtensions.cs new file mode 100644 index 00000000..ac08fa68 --- /dev/null +++ b/Avalonia.Gif/Extensions/StreamExtensions.cs @@ -0,0 +1,81 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; + +namespace Avalonia.Gif.Extensions +{ + [DebuggerStepThrough] + internal static class StreamExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort SpanToShort(Span b) => (ushort)(b[0] | (b[1] << 8)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Skip(this Stream stream, long count) + { + stream.Position += count; + } + + /// + /// Read a Gif block from stream while advancing the position. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReadBlock(this Stream stream, byte[] tempBuf) + { + stream.Read(tempBuf, 0, 1); + + var blockLength = (int)tempBuf[0]; + + if (blockLength > 0) + stream.Read(tempBuf, 0, blockLength); + + // Guard against infinite loop. + if (stream.Position >= stream.Length) + throw new InvalidGifStreamException("Reach the end of the filestream without trailer block."); + + return blockLength; + } + + /// + /// Skips GIF blocks until it encounters an empty block. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SkipBlocks(this Stream stream, byte[] tempBuf) + { + int blockLength; + do + { + stream.Read(tempBuf, 0, 1); + + blockLength = tempBuf[0]; + stream.Position += blockLength; + + // Guard against infinite loop. + if (stream.Position >= stream.Length) + throw new InvalidGifStreamException("Reach the end of the filestream without trailer block."); + } while (blockLength > 0); + } + + /// + /// Read a from stream by providing a temporary buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadUShortS(this Stream stream, byte[] tempBuf) + { + stream.Read(tempBuf, 0, 2); + return SpanToShort(tempBuf); + } + + /// + /// Read a from stream by providing a temporary buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadByteS(this Stream stream, byte[] tempBuf) + { + stream.Read(tempBuf, 0, 1); + var finalVal = tempBuf[0]; + return finalVal; + } + } +} diff --git a/Avalonia.Gif/GifImage.cs b/Avalonia.Gif/GifImage.cs new file mode 100644 index 00000000..d53cbd19 --- /dev/null +++ b/Avalonia.Gif/GifImage.cs @@ -0,0 +1,296 @@ +using System; +using System.IO; +using System.Numerics; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Logging; +using Avalonia.Media; +using Avalonia.Rendering.Composition; +using Avalonia.VisualTree; + +namespace Avalonia.Gif +{ + public class GifImage : Control + { + public static readonly StyledProperty SourceUriRawProperty = AvaloniaProperty.Register< + GifImage, + string + >("SourceUriRaw"); + + public static readonly StyledProperty SourceUriProperty = AvaloniaProperty.Register( + "SourceUri" + ); + + public static readonly StyledProperty SourceStreamProperty = AvaloniaProperty.Register< + GifImage, + Stream + >("SourceStream"); + + public static readonly StyledProperty IterationCountProperty = AvaloniaProperty.Register< + GifImage, + IterationCount + >("IterationCount", IterationCount.Infinite); + + private GifInstance? _gifInstance; + + public static readonly StyledProperty StretchDirectionProperty = AvaloniaProperty.Register< + GifImage, + StretchDirection + >("StretchDirection"); + + public static readonly StyledProperty StretchProperty = AvaloniaProperty.Register( + "Stretch" + ); + + private CompositionCustomVisual? _customVisual; + + private object? _initialSource = null; + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + switch (change.Property.Name) + { + case nameof(SourceUriRaw): + case nameof(SourceUri): + case nameof(SourceStream): + SourceChanged(change); + break; + case nameof(Stretch): + case nameof(StretchDirection): + InvalidateArrange(); + InvalidateMeasure(); + Update(); + break; + case nameof(IterationCount): + IterationCountChanged(change); + break; + case nameof(Bounds): + Update(); + break; + } + + base.OnPropertyChanged(change); + } + + public string SourceUriRaw + { + get => GetValue(SourceUriRawProperty); + set => SetValue(SourceUriRawProperty, value); + } + + public Uri SourceUri + { + get => GetValue(SourceUriProperty); + set => SetValue(SourceUriProperty, value); + } + + public Stream SourceStream + { + get => GetValue(SourceStreamProperty); + set => SetValue(SourceStreamProperty, value); + } + + public IterationCount IterationCount + { + get => GetValue(IterationCountProperty); + set => SetValue(IterationCountProperty, value); + } + + public StretchDirection StretchDirection + { + get => GetValue(StretchDirectionProperty); + set => SetValue(StretchDirectionProperty, value); + } + + public Stretch Stretch + { + get => GetValue(StretchProperty); + set => SetValue(StretchProperty, value); + } + + private static void IterationCountChanged(AvaloniaPropertyChangedEventArgs e) + { + var image = e.Sender as GifImage; + if (image is null || e.NewValue is not IterationCount iterationCount) + return; + + image.IterationCount = iterationCount; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + var compositor = ElementComposition.GetElementVisual(this)?.Compositor; + if (compositor == null || _customVisual?.Compositor == compositor) + return; + _customVisual = compositor.CreateCustomVisual(new CustomVisualHandler()); + ElementComposition.SetElementChildVisual(this, _customVisual); + _customVisual.SendHandlerMessage(CustomVisualHandler.StartMessage); + + if (_initialSource is not null) + { + UpdateGifInstance(_initialSource); + _initialSource = null; + } + + Update(); + base.OnAttachedToVisualTree(e); + } + + private void Update() + { + if (_customVisual is null || _gifInstance is null) + return; + + var dpi = this.GetVisualRoot()?.RenderScaling ?? 1.0; + var sourceSize = _gifInstance.GifPixelSize.ToSize(dpi); + var viewPort = new Rect(Bounds.Size); + + var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); + var scaledSize = sourceSize * scale; + var destRect = viewPort.CenterRect(new Rect(scaledSize)).Intersect(viewPort); + + if (Stretch == Stretch.None) + { + _customVisual.Size = new Vector2((float)sourceSize.Width, (float)sourceSize.Height); + } + else + { + _customVisual.Size = new Vector2((float)destRect.Size.Width, (float)destRect.Size.Height); + } + + _customVisual.Offset = new Vector3((float)destRect.Position.X, (float)destRect.Position.Y, 0); + } + + private class CustomVisualHandler : CompositionCustomVisualHandler + { + private TimeSpan _animationElapsed; + private TimeSpan? _lastServerTime; + private GifInstance? _currentInstance; + private bool _running; + + public static readonly object StopMessage = new(), + StartMessage = new(); + + public override void OnMessage(object message) + { + if (message == StartMessage) + { + _running = true; + _lastServerTime = null; + RegisterForNextAnimationFrameUpdate(); + } + else if (message == StopMessage) + { + _running = false; + } + else if (message is GifInstance instance) + { + _currentInstance?.Dispose(); + _currentInstance = instance; + } + } + + public override void OnAnimationFrameUpdate() + { + if (!_running) + return; + Invalidate(); + RegisterForNextAnimationFrameUpdate(); + } + + public override void OnRender(ImmediateDrawingContext drawingContext) + { + if (_running) + { + if (_lastServerTime.HasValue) + _animationElapsed += (CompositionNow - _lastServerTime.Value); + _lastServerTime = CompositionNow; + } + + try + { + if (_currentInstance is null || _currentInstance.IsDisposed) + return; + + var bitmap = _currentInstance.ProcessFrameTime(_animationElapsed); + if (bitmap is not null) + { + drawingContext.DrawBitmap( + bitmap, + new Rect(_currentInstance.GifPixelSize.ToSize(1)), + GetRenderBounds() + ); + } + } + catch (Exception e) + { + Logger.Sink?.Log(LogEventLevel.Error, "GifImage Renderer ", this, e.ToString()); + } + } + } + + /// + /// Measures the control. + /// + /// The available size. + /// The desired size of the control. + protected override Size MeasureOverride(Size availableSize) + { + var result = new Size(); + var scaling = this.GetVisualRoot()?.RenderScaling ?? 1.0; + if (_gifInstance != null) + { + result = Stretch.CalculateSize( + availableSize, + _gifInstance.GifPixelSize.ToSize(scaling), + StretchDirection + ); + } + + return result; + } + + /// + protected override Size ArrangeOverride(Size finalSize) + { + if (_gifInstance is null) + return new Size(); + var scaling = this.GetVisualRoot()?.RenderScaling ?? 1.0; + var sourceSize = _gifInstance.GifPixelSize.ToSize(scaling); + var result = Stretch.CalculateSize(finalSize, sourceSize); + return result; + } + + private void SourceChanged(AvaloniaPropertyChangedEventArgs e) + { + if ( + e.NewValue is null + || (e.NewValue is string value && !Uri.IsWellFormedUriString(value, UriKind.Absolute)) + ) + { + return; + } + + if (_customVisual is null) + { + _initialSource = e.NewValue; + return; + } + + UpdateGifInstance(e.NewValue); + + InvalidateArrange(); + InvalidateMeasure(); + Update(); + } + + private void UpdateGifInstance(object source) + { + _gifInstance?.Dispose(); + _gifInstance = new GifInstance(source); + _gifInstance.IterationCount = IterationCount; + _customVisual?.SendHandlerMessage(_gifInstance); + } + } +} diff --git a/Avalonia.Gif/GifInstance.cs b/Avalonia.Gif/GifInstance.cs new file mode 100644 index 00000000..30e002d1 --- /dev/null +++ b/Avalonia.Gif/GifInstance.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Gif.Decoding; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace Avalonia.Gif +{ + public class GifInstance : IDisposable + { + public IterationCount IterationCount { get; set; } + public bool AutoStart { get; private set; } = true; + private readonly GifDecoder _gifDecoder; + private readonly WriteableBitmap? _targetBitmap; + private TimeSpan _totalTime; + private readonly List _frameTimes; + private uint _iterationCount; + private int _currentFrameIndex; + private readonly List _colorTableIdList; + + public CancellationTokenSource CurrentCts { get; } + + internal GifInstance(object newValue) + : this( + newValue switch + { + Stream s => s, + Uri u => GetStreamFromUri(u), + string str => GetStreamFromString(str), + _ => throw new InvalidDataException("Unsupported source object") + } + ) { } + + public GifInstance(string uri) + : this(GetStreamFromString(uri)) { } + + public GifInstance(Uri uri) + : this(GetStreamFromUri(uri)) { } + + public GifInstance(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(); + + _gifDecoder = new GifDecoder(currentStream, CurrentCts.Token); + var pixSize = new PixelSize(_gifDecoder.Header.Dimensions.Width, _gifDecoder.Header.Dimensions.Height); + + _targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque); + GifPixelSize = pixSize; + + _totalTime = TimeSpan.Zero; + + _frameTimes = _gifDecoder + .Frames + .Select(frame => + { + _totalTime = _totalTime.Add(frame.FrameDelay); + return _totalTime; + }) + .ToList(); + + _gifDecoder.RenderFrame(0, _targetBitmap); + } + + 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(); + } + + 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) + { + _gifDecoder.RenderFrame(frameIndex, _targetBitmap); + _currentFrameIndex = frameIndex; + + return _targetBitmap; + } + } +} diff --git a/Avalonia.Gif/InvalidGifStreamException.cs b/Avalonia.Gif/InvalidGifStreamException.cs new file mode 100644 index 00000000..9771d9cb --- /dev/null +++ b/Avalonia.Gif/InvalidGifStreamException.cs @@ -0,0 +1,20 @@ +using System; +using System.Runtime.Serialization; + +namespace Avalonia.Gif +{ + [Serializable] + internal class InvalidGifStreamException : Exception + { + public InvalidGifStreamException() { } + + public InvalidGifStreamException(string message) + : base(message) { } + + public InvalidGifStreamException(string message, Exception innerException) + : base(message, innerException) { } + + protected InvalidGifStreamException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index a2f6870b..cd5995d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,142 @@ 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.2 +### Changed +- Changed Symlink shared folder link targets for Automatic1111 and ComfyUI. From `ControlNet -> models/controlnet` to `ControlNet -> models/controlnet/ControlNet` and `T2IAdapter -> models/controlnet/T2IAdapter`. +- Changed FreeU defaults to match recommended SD1.5 defaults +- Changed default denoise strength from 1.0 to 0.7 +### Fixed +- Fixed ControlNet / T2IAdapter shared folder links for Automatic1111 conflicting with each other +- Fixed URIScheme registration errors on Linux +- Fixed RuinedFooocus missing output folder on startup +- Fixed incorrect Fooocus VRAM launch arguments + +## v2.7.1 +### Added +- Added Turkish UI language option, thanks to Progresor for the translation +### Fixed +- Fixed Inference Image to Image projects missing denoise strength setting + +## v2.7.0 +### Added +#### General +- New package: [RuinedFooocus](https://github.com/runew0lf/RuinedFooocus) +- Added an X button to all search fields to instantly clear them (Esc key also works) +- Added System Information section to Settings +#### Inference +- Added Image to Image project type +- Added Modular custom steps + - Use the plus button to add new steps (Hires Fix, Upscaler, and Save Image are currently available), and the edit button to enable removing or dragging steps to reorder them. This enables multi-pass Hires Fix, mixing different upscalers, and saving intermediate images at any point in the pipeline. +- Added Sampler addons + - Addons usually affect guidance like ControlNet, T2I, FreeU, and other addons to come. They apply to the individual sampler, so you can mix and match different ControlNets for Base and Hires Fix, or use the current output from a previous sampler as ControlNet guidance image for HighRes passes. +- Added SD Turbo Scheduler +- Added display names for new samplers ("Heun++ 2", "DDPM", "LCM") +- Added Ctrl+Enter as a shortcut for the Generate Image button +#### Accounts Settings Subpage +- Lykos Account sign-up and login - currently for Patreon OAuth connections but GitHub requests caching and settings sync are planned +- Supporters can now connect your Patreon accounts, then head to the Updates page to choose to receive auto-updates from the Dev or Preview channels +- CivitAI Account login with API key - enables downloading models from the Browser page that require CivitAI logins, more integrations like liking and commenting are also planned +#### Updates Settings Subpage +- Toggle auto-update notifications and manually check for updates +- Choose between Stable, Preview, and Dev update channels +#### Inference Settings Subpage +- Moved Inference settings to subpage +- Updated with more localized labels +#### Outputs Page +- Added Refresh button to update gallery from file system changes +#### Checkpoints Page +- Added the ability to drag & drop checkpoints between different folders +- Added "Copy Trigger Words" option to the three-dots menu on the Checkpoints page (when data is available) +- Added trigger words on checkpoint card and tooltip +- Added "Find Connected Metadata" options for root-level and file-level scans +- Added "Update Existing Metadata" button +#### Model Browser +- Added Hugging Face tab to the Model Browser +- Added additional base model filter options for CivitAI ("SD 1.5 LCM", "SDXL 1.0 LCM", "SDXL Turbo", "Other") +- Added the ability to type in a specific page number in the CivitAI Model Browser +- Right clicking anywhere on the model card will open the same menu as the three-dots button +- New model downloads will save trigger words in metadata, if available +- Model author username and avatar display, with clickable link to their profile +### Changed +#### General +- Model Browser page has been redesigned, featuring more information like rating and download counts +- Model Browser navigation has improved animations on hover and compact number formatting +- Updated Outputs Page button and menu layout +- Rearranged Add Package dialog slightly to accommodate longer package list +- Folder-level "Find Connected Metadata" now scans the selected folder and its subfolders +- Model Browser now split into "CivitAI" and "Hugging Face" tabs +#### Inference +- Selected images (i.e. Image2Image, Upscale, ControlNet) will now save their source paths saved and restored on load. If the image is moved or deleted, the selection will show as missing and can be reselected +- Project files (.smproj) have been updated to v3, existing projects will be upgraded on load and will no longer be compatible with older versions of Stability Matrix +### Fixed +- Fixed Outputs page reverting back to Shared Output Folder every time the page is reloaded +- Potentially fixed updates sometimes clearing settings or launching in the wrong directory +- Improved startup time and window load time after exiting dialogs +- Fixed control character decoding that caused some progress bars to show as `\u2588` +- Fixed Python `rich` package's progress bars not showing in console +- Optimized ProgressRing animation bindings to reduce CPU usage +- Improved safety checks in custom control rendering to reduce potential graphical artifacts +- Improved console rendering safety with cursor line increment clamping, as potential fix for [#111](https://github.com/LykosAI/StabilityMatrix/issues/111) +- Fixed [#290](https://github.com/LykosAI/StabilityMatrix/issues/290) - Model browser crash due to text trimming certain unicode characters +- Fixed crash when loading an empty settings file +- Improve Settings save and load performance with .NET 8 Source Generating Serialization +- Fixed ApplicationException during database shutdown +- InvokeAI model links for T2I/IpAdapters now point to the correct folders +- Added extra checks to help prevent settings resetting in certain scenarios +- Fixed Refiner model enabled state not saving to Inference project files +- Fixed NullReference error labels when clearing the Inference batch size settings, now shows improved message with minimum and maximum value constraints + +## v2.7.0-pre.4 +### Added +#### Inference +- Added Image to Image project type +- Added Modular custom steps + - Use the plus button to add new steps (Hires Fix, Upscaler, and Save Image are currently available), and the edit button to enable removing or dragging steps to reorder them. This enables multi-pass Hires Fix, mixing different upscalers, and saving intermediate images at any point in the pipeline. +- Added Sampler addons + - Addons usually affect guidance like ControlNet, T2I, FreeU, and other addons to come. They apply to the individual sampler, so you can mix and match different ControlNets for Base and Hires Fix, or use the current output from a previous sampler as ControlNet guidance image for HighRes passes. +- Added SD Turbo Scheduler +- Added display names for new samplers ("Heun++ 2", "DDPM", "LCM") +#### Model Browser +- Added additional base model filter options ("SD 1.5 LCM", "SDXL 1.0 LCM", "SDXL Turbo", "Other") +### Changed +#### Inference +- Selected images (i.e. Image2Image, Upscale, ControlNet) will now save their source paths saved and restored on load. If the image is moved or deleted, the selection will show as missing and can be reselected +- Project files (.smproj) have been updated to v3, existing projects will be upgraded on load and will no longer be compatible with older versions of Stability Matrix +### Fixed +- Fixed Refiner model enabled state not saving to Inference project files + +## v2.7.0-pre.3 +### Added +- Added "Find Connected Metadata" options for root-level and file-level scans to the Checkpoints page +- Added "Update Existing Metadata" button to the Checkpoints page +- Added Hugging Face tab to the Model Browser +- Added the ability to type in a specific page number in the CivitAI Model Browser +### Changed +- Folder-level "Find Connected Metadata" now scans the selected folder and its subfolders +- Model Browser now split into "CivitAI" and "Hugging Face" tabs +### Fixed +- InvokeAI model links for T2I/IpAdapters now point to the correct folders +- Added extra checks to help prevent settings resetting in certain scenarios + +## v2.7.0-pre.2 +### Added +- Added System Information section to Settings +### Changed +- Moved Inference Settings to subpage +### Fixed +- Fixed crash when loading an empty settings file +- Improve Settings save and load performance with .NET 8 Source Generating Serialization +- Fixed ApplicationException during database shutdown + +## v2.7.0-pre.1 +### Fixed +- Fixed control character decoding that caused some progress bars to show as `\u2588` +- Fixed Python `rich` package's progress bars not showing in console +- Optimized ProgressRing animation bindings to reduce CPU usage +- Improved safety checks in custom control rendering to reduce potential graphical artifacts +- Improved console rendering safety with cursor line increment clamping, as potential fix for [#111](https://github.com/LykosAI/StabilityMatrix/issues/111) + ## v2.7.0-dev.4 ### Fixed - Fixed [#290](https://github.com/LykosAI/StabilityMatrix/issues/290) - Model browser crash due to text trimming certain unicode characters @@ -36,7 +172,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Added Refresh button to update gallery from file system changes #### Checkpoints Page - Added the ability to drag & drop checkpoints between different folders -## Changed +### Changed #### Outputs Page - Updated button and menu layout #### Packages Page @@ -55,9 +191,14 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2 - Updates Settings Subpage - Toggle auto-update notifications and manually check for updates - Choose between Stable, Preview, and Dev update channels -## Changed +### Changed - Model Browser page has been redesigned, featuring more information like rating and download counts +## v2.6.7 +### Fixed +- Fixed prerequisite install not unpacking due to improperly formatted 7z argument (Caused the "python310._pth FileNotFoundException") +- Fixed [#301](https://github.com/LykosAI/StabilityMatrix/issues/301) - Package updates failing silently because of a PortableGit error + ## v2.6.6 ### Fixed - Fixed [#297](https://github.com/LykosAI/StabilityMatrix/issues/297) - Model browser LiteAsyncException occuring when fetching entries with unrecognized values from enum name changes diff --git a/README.md b/README.md index d94a82d2..daadd8f5 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,8 @@ Stability Matrix is now available in the following languages, thanks to our comm - Lautaroturina - 🇷🇺 Русский - aolko +- 🇹🇷 Türkçe + - Progresor If you would like to contribute a translation, please create an issue or contact us on Discord. Include an email where we'll send an invite to our [POEditor](https://poeditor.com/) project. 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 4e02cb07..7439ffa5 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"> @@ -21,18 +22,16 @@ - + 700 - 32 - avares://StabilityMatrix.Avalonia/Assets/Fonts/NotoSansJP#Noto Sans JP @@ -53,29 +52,30 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs index d5e91657..742b10a3 100644 --- a/StabilityMatrix.Avalonia/App.axaml.cs +++ b/StabilityMatrix.Avalonia/App.axaml.cs @@ -1,7 +1,3 @@ -#if DEBUG -using StabilityMatrix.Avalonia.Diagnostics.LogViewer; -using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions; -#endif using System; using System.Collections.Generic; using System.Diagnostics; @@ -14,6 +10,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using AsyncAwaitBestPractices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -64,11 +61,17 @@ using StabilityMatrix.Core.Services; using Application = Avalonia.Application; using DrawingColor = System.Drawing.Color; using LogLevel = Microsoft.Extensions.Logging.LogLevel; +#if DEBUG +using StabilityMatrix.Avalonia.Diagnostics.LogViewer; +using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions; +#endif namespace StabilityMatrix.Avalonia; public sealed class App : Application { + private static bool isAsyncDisposeComplete; + [NotNull] public static IServiceProvider? Services { get; private set; } @@ -77,8 +80,7 @@ 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; } @@ -114,7 +116,8 @@ public sealed class App : Application public override void OnFrameworkInitializationCompleted() { // Remove DataAnnotations validation plugin since we're using INotifyDataErrorInfo from MvvmToolkit - var dataValidationPluginsToRemove = BindingPlugins.DataValidators + var dataValidationPluginsToRemove = BindingPlugins + .DataValidators .OfType() .ToArray(); @@ -158,19 +161,22 @@ public sealed class App : Application DesktopLifetime.MainWindow = setupWindow; - setupWindow.ShowAsyncCts.Token.Register(() => - { - if (setupWindow.Result == ContentDialogResult.Primary) + setupWindow + .ShowAsyncCts + .Token + .Register(() => { - settingsManager.SetEulaAccepted(); - ShowMainWindow(); - DesktopLifetime.MainWindow.Show(); - } - else - { - Shutdown(); - } - }); + if (setupWindow.Result == ContentDialogResult.Primary) + { + settingsManager.SetEulaAccepted(); + ShowMainWindow(); + DesktopLifetime.MainWindow.Show(); + } + else + { + Shutdown(); + } + }); } else { @@ -217,25 +223,7 @@ public sealed class App : Application mainWindow.WindowStartupLocation = WindowStartupLocation.CenterScreen; } - mainWindow.Closing += (_, _) => - { - var validWindowPosition = mainWindow.Screens.All.Any( - screen => screen.Bounds.Contains(mainWindow.Position) - ); - - settingsManager.Transaction( - s => - { - s.WindowSettings = new WindowSettings( - mainWindow.Width, - mainWindow.Height, - validWindowPosition ? mainWindow.Position.X : 0, - validWindowPosition ? mainWindow.Position.Y : 0 - ); - }, - ignoreMissingLibraryDir: true - ); - }; + mainWindow.Closing += OnMainWindowClosing; mainWindow.Closed += (_, _) => Shutdown(); mainWindow.SetDefaultFonts(); @@ -301,10 +289,7 @@ public sealed class App : Application services.AddSingleton(p => p.GetRequiredService()); } - internal static void ConfigureDialogViewModels( - IServiceCollection services, - Type[] exportedTypes - ) + internal static void ConfigureDialogViewModels(IServiceCollection services, Type[] exportedTypes) { // Dialog factory services.AddSingleton>(provider => @@ -312,17 +297,7 @@ 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(); @@ -346,21 +321,18 @@ public sealed class App : Application services.AddMessagePipe(); services.AddMessagePipeNamedPipeInterprocess("StabilityMatrix"); - var exportedTypes = AppDomain.CurrentDomain + var exportedTypes = AppDomain + .CurrentDomain .GetAssemblies() .Where(a => a.FullName?.StartsWith("StabilityMatrix") == true) .SelectMany(a => a.GetExportedTypes()) .ToArray(); var transientTypes = exportedTypes - .Select( - t => - new { t, attributes = t.GetCustomAttributes(typeof(TransientAttribute), false) } - ) + .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] }); @@ -377,23 +349,12 @@ public sealed class App : Application } var singletonTypes = exportedTypes - .Select( - t => - new { t, attributes = t.GetCustomAttributes(typeof(SingletonAttribute), false) } - ) + .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) { @@ -425,9 +386,7 @@ 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()) @@ -473,9 +432,7 @@ public sealed class App : Application jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter()); jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter()); jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter()); - jsonSerializerOptions.Converters.Add( - new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) - ); + jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); jsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; var defaultRefitSettings = new RefitSettings @@ -484,8 +441,7 @@ public sealed class App : Application }; // Refit settings for IApiFactory - var defaultSystemTextJsonSettings = - SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions(); + var defaultSystemTextJsonSettings = SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions(); defaultSystemTextJsonSettings.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; var apiFactoryRefitSettings = new RefitSettings { @@ -559,27 +515,19 @@ public sealed class App : Application c.BaseAddress = new Uri("https://stableauthentication.azurewebsites.net"); c.Timeout = TimeSpan.FromSeconds(15); }) - .ConfigurePrimaryHttpMessageHandler( - () => new HttpClientHandler { AllowAutoRedirect = false } - ) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false }) .AddPolicyHandler(retryPolicy) .AddHttpMessageHandler( serviceProvider => - new TokenAuthHeaderHandler( - serviceProvider.GetRequiredService() - ) + new TokenAuthHeaderHandler(serviceProvider.GetRequiredService()) ); // Add Refit client managers - services - .AddHttpClient("A3Client") - .AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy)); + services.AddHttpClient("A3Client").AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy)); services .AddHttpClient("DontFollowRedirects") - .ConfigurePrimaryHttpMessageHandler( - () => new HttpClientHandler { AllowAutoRedirect = false } - ) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false }) .AddPolicyHandler(retryPolicy); /*services.AddHttpClient("IComfyApi") @@ -609,11 +557,7 @@ 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()); @@ -639,6 +583,87 @@ public sealed class App : Application } } + /// + /// Handle shutdown requests (happens before ) + /// + private static void OnMainWindowClosing(object? sender, WindowClosingEventArgs e) + { + if (e.Cancel) + return; + + var mainWindow = (MainWindow)sender!; + + // Show confirmation if package running + var launchPageViewModel = Services.GetRequiredService(); + launchPageViewModel.OnMainWindowClosing(e); + + if (e.Cancel) + return; + + // Check if we need to dispose IAsyncDisposables + if ( + !isAsyncDisposeComplete + && Services.GetServices().ToList() is { Count: > 0 } asyncDisposables + ) + { + // Cancel shutdown for now + e.Cancel = true; + isAsyncDisposeComplete = true; + + Debug.WriteLine("OnShutdownRequested Canceled: Disposing IAsyncDisposables"); + + Task.Run(async () => + { + foreach (var disposable in asyncDisposables) + { + Debug.WriteLine($"Disposing IAsyncDisposable ({disposable.GetType().Name})"); + try + { + await disposable.DisposeAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + Debug.Fail(ex.ToString()); + } + } + }) + .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 => + { + s.WindowSettings = new WindowSettings( + sender.Width, + sender.Height, + validWindowPosition ? sender.Position.X : 0, + validWindowPosition ? sender.Position.Y : 0 + ); + }, + ignoreMissingLibraryDir: true + ); + } + private static void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs args) { Debug.WriteLine("Start OnExit"); @@ -646,16 +671,15 @@ public sealed class App : Application var settingsManager = Services.GetRequiredService(); // If RemoveFolderLinksOnShutdown is set, delete all package junctions - if ( - settingsManager is { IsLibraryDirSet: true, Settings.RemoveFolderLinksOnShutdown: true } - ) + if (settingsManager is { IsLibraryDirSet: true, Settings.RemoveFolderLinksOnShutdown: true }) { var sharedFolders = Services.GetRequiredService(); sharedFolders.RemoveLinksForAllPackages(); } Debug.WriteLine("Start OnExit: Disposing services"); - // Dispose all services + + // Dispose IDisposable services foreach (var disposable in Services.GetServices()) { Debug.WriteLine($"Disposing {disposable.GetType().Name}"); @@ -690,13 +714,10 @@ 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", + FileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log", + ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log", ArchiveNumbering = ArchiveNumberingMode.Rolling, MaxArchiveFiles = 2 } @@ -709,8 +730,11 @@ 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); + + // Disable LoadableViewModelBase trace logging by default builder - .ForLogger("StabilityMatrix.Avalonia.ViewModels.ConsoleViewModel") + .ForLogger("StabilityMatrix.Avalonia.ViewModels.Base.LoadableViewModelBase") .WriteToNil(NLog.LogLevel.Debug); builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(debugTarget); @@ -727,18 +751,20 @@ 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(); @@ -777,36 +803,34 @@ public sealed class App : Application results.Add(ms); } - Dispatcher.UIThread.InvokeAsync(async () => - { - var dest = await StorageProvider.SaveFilePickerAsync( - new FilePickerSaveOptions() - { - SuggestedFileName = "screenshot.png", - ShowOverwritePrompt = true - } - ); - - if (dest?.TryGetLocalPath() is { } localPath) + Dispatcher + .UIThread + .InvokeAsync(async () => { - var localFile = new FilePath(localPath); - foreach (var (i, stream) in results.Enumerate()) + var dest = await StorageProvider.SaveFilePickerAsync( + new FilePickerSaveOptions() { SuggestedFileName = "screenshot.png", ShowOverwritePrompt = true } + ); + + if (dest?.TryGetLocalPath() is { } localPath) { - var name = localFile.NameWithoutExtension; - if (results.Count > 1) + var localFile = new FilePath(localPath); + foreach (var (i, stream) in results.Enumerate()) { - name += $"_{i + 1}"; - } + var name = localFile.NameWithoutExtension; + if (results.Count > 1) + { + name += $"_{i + 1}"; + } - localFile = localFile.Directory!.JoinFile(name + ".png"); - localFile.Create(); + localFile = localFile.Directory!.JoinFile(name + ".png"); + localFile.Create(); - await using var fileStream = localFile.Info.OpenWrite(); - stream.Seek(0, SeekOrigin.Begin); - await stream.CopyToAsync(fileStream); + await using var fileStream = localFile.Info.OpenWrite(); + stream.Seek(0, SeekOrigin.Begin); + await stream.CopyToAsync(fileStream); + } } - } - }); + }); } [Conditional("DEBUG")] @@ -822,8 +846,7 @@ public sealed class App : Application { #if DEBUG setupBuilder.SetupExtensions( - extensionBuilder => - extensionBuilder.RegisterTarget("DataStoreLogger") + extensionBuilder => extensionBuilder.RegisterTarget("DataStoreLogger") ); #endif } diff --git a/StabilityMatrix.Avalonia/Assets.cs b/StabilityMatrix.Avalonia/Assets.cs index 58867158..68f76884 100644 --- a/StabilityMatrix.Avalonia/Assets.cs +++ b/StabilityMatrix.Avalonia/Assets.cs @@ -11,20 +11,16 @@ namespace StabilityMatrix.Avalonia; internal static class Assets { - public static AvaloniaResource AppIcon { get; } = - new("avares://StabilityMatrix.Avalonia/Assets/Icon.ico"); + public static AvaloniaResource AppIcon { get; } = new("avares://StabilityMatrix.Avalonia/Assets/Icon.ico"); - public static AvaloniaResource AppIconPng { get; } = - new("avares://StabilityMatrix.Avalonia/Assets/Icon.png"); + public static AvaloniaResource AppIconPng { get; } = new("avares://StabilityMatrix.Avalonia/Assets/Icon.png"); /// /// Fixed image for models with no images. /// - public static Uri NoImage { get; } = - new("avares://StabilityMatrix.Avalonia/Assets/noimage.png"); + public static Uri NoImage { get; } = new("avares://StabilityMatrix.Avalonia/Assets/noimage.png"); - public static AvaloniaResource LicensesJson => - new("avares://StabilityMatrix.Avalonia/Assets/licenses.json"); + public static AvaloniaResource LicensesJson => new("avares://StabilityMatrix.Avalonia/Assets/licenses.json"); public static AvaloniaResource ImagePromptLanguageJson => new("avares://StabilityMatrix.Avalonia/Assets/ImagePrompt.tmLanguage.json"); @@ -32,6 +28,8 @@ internal static class Assets public static AvaloniaResource ThemeMatrixDarkJson => new("avares://StabilityMatrix.Avalonia/Assets/ThemeMatrixDark.json"); + public static AvaloniaResource HfPackagesJson => new("avares://StabilityMatrix.Avalonia/Assets/hf-packages.json"); + private const UnixFileMode Unix755 = UnixFileMode.UserRead | UnixFileMode.UserWrite @@ -46,23 +44,14 @@ internal static class Assets [SupportedOSPlatform("macos")] public static AvaloniaResource SevenZipExecutable => Compat.Switch( - ( - PlatformKind.Windows, - new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe") - ), + (PlatformKind.Windows, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe")), ( PlatformKind.Linux | PlatformKind.X64, - new AvaloniaResource( - "avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs", - Unix755 - ) + new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs", Unix755) ), ( PlatformKind.MacOS | PlatformKind.Arm, - new AvaloniaResource( - "avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz", - Unix755 - ) + new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz", Unix755) ) ); @@ -73,21 +62,15 @@ internal static class Assets Compat.Switch( ( PlatformKind.Windows, - new AvaloniaResource( - "avares://StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt" - ) + new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt") ), ( PlatformKind.Linux | PlatformKind.X64, - new AvaloniaResource( - "avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt" - ) + new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt") ), ( PlatformKind.MacOS | PlatformKind.Arm, - new AvaloniaResource( - "avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt" - ) + new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt") ) ); @@ -111,9 +94,7 @@ internal static class Assets PlatformKind.Windows | PlatformKind.X64, new RemoteResource { - Url = new Uri( - "https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip" - ), + Url = new Uri("https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"), HashSha256 = "608619f8619075629c9c69f361352a0da6ed7e62f83a0e19c63e0ea32eb7629d" } ), @@ -167,9 +148,7 @@ internal static class Assets /// /// Yield AvaloniaResources given a relative directory path within the 'Assets' folder. /// - public static IEnumerable<(AvaloniaResource resource, string relativePath)> FindAssets( - string relativeAssetPath - ) + public static IEnumerable<(AvaloniaResource resource, string relativePath)> FindAssets(string relativeAssetPath) { var baseUri = new Uri("avares://StabilityMatrix.Avalonia/Assets/"); var targetUri = new Uri(baseUri, relativeAssetPath); diff --git a/StabilityMatrix.Avalonia/Assets/hf-packages.json b/StabilityMatrix.Avalonia/Assets/hf-packages.json new file mode 100644 index 00000000..e103d80e --- /dev/null +++ b/StabilityMatrix.Avalonia/Assets/hf-packages.json @@ -0,0 +1,544 @@ +[ + { + "ModelCategory": "BaseModel", + "ModelName": "Stable Diffusion 1.5", + "RepositoryPath": "runwayml/stable-diffusion-v1-5", + "Files": [ + "v1-5-pruned-emaonly.safetensors" + ], + "LicenseType": "CreativeML Open RAIL-M" + }, + { + "ModelCategory": "BaseModel", + "ModelName": "Stable Diffusion 2.1", + "RepositoryPath": "stabilityai/stable-diffusion-2-1", + "Files": [ + "v2-1_768-ema-pruned.safetensors" + ], + "LicenseType": "Open RAIL++" + }, + { + "ModelCategory": "BaseModel", + "ModelName": "Stable Diffusion XL (Base)", + "RepositoryPath": "stabilityai/stable-diffusion-xl-base-1.0", + "Files": [ + "sd_xl_base_1.0_0.9vae.safetensors", + "sd_xl_offset_example-lora_1.0.safetensors" + ], + "LicenseType": "Open RAIL++", + "LicensePath": "LICENSE.md" + }, + { + "ModelCategory": "BaseModel", + "ModelName": "Stable Diffusion XL (Refiner)", + "RepositoryPath": "stabilityai/stable-diffusion-xl-refiner-1.0", + "Files": [ + "sd_xl_refiner_1.0_0.9vae.safetensors" + ], + "LicenseType": "Open RAIL++", + "LicensePath": "LICENSE.md" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "Canny", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11p_sd15_canny.pth", + "control_v11p_sd15_canny.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "Depth", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11f1p_sd15_depth.pth", + "control_v11f1p_sd15_depth.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "MLSD", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11p_sd15_mlsd.pth", + "control_v11p_sd15_mlsd.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "Inpaint", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11p_sd15_inpaint.pth", + "control_v11p_sd15_inpaint.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "IP2P", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11e_sd15_ip2p.pth", + "control_v11e_sd15_ip2p.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "Tile", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11f1e_sd15_tile.pth", + "control_v11f1e_sd15_tile.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "OpenPose", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11p_sd15_openpose.pth", + "control_v11p_sd15_openpose.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "LineArt", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11p_sd15_lineart.pth", + "control_v11p_sd15_lineart.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "LineArt Anime", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11p_sd15s2_lineart_anime.pth", + "control_v11p_sd15s2_lineart_anime.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "NormalBae", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11p_sd15_normalbae.pth", + "control_v11p_sd15_normalbae.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "Seg", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11p_sd15_seg.pth", + "control_v11p_sd15_seg.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "Soft Edge", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11p_sd15_softedge.pth", + "control_v11p_sd15_softedge.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "Scribble", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11p_sd15_scribble.pth", + "control_v11p_sd15_scribble.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "ControlNet", + "ModelName": "Shuffle", + "RepositoryPath": "lllyasviel/ControlNet-v1-1", + "Files": [ + "control_v11e_sd15_shuffle.pth", + "control_v11e_sd15_shuffle.yaml" + ], + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "Canny", + "RepositoryPath": "lllyasviel/control_v11p_sd15_canny", + "Files": [ + "diffusion_pytorch_model.safetensors", + "config.json" + ], + "Subfolder": "canny", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "Depth", + "RepositoryPath": "lllyasviel/control_v11f1p_sd15_depth", + "Files": [ + "diffusion_pytorch_model.safetensors", + "config.json" + ], + "Subfolder": "depth", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "MLSD", + "RepositoryPath": "lllyasviel/control_v11p_sd15_mlsd", + "Files": [ + "diffusion_pytorch_model.safetensors", + "config.json" + ], + "Subfolder": "MLSD", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "Inpaint", + "RepositoryPath": "lllyasviel/control_v11p_sd15_inpaint", + "Files": [ + "diffusion_pytorch_model.safetensors", + "config.json" + ], + "Subfolder": "inpaint", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "IP2P", + "RepositoryPath": "lllyasviel/control_v11e_sd15_ip2p", + "Files": [ + "diffusion_pytorch_model.safetensors", + "config.json" + ], + "Subfolder": "ip2p", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "Tile", + "RepositoryPath": "lllyasviel/control_v11f1e_sd15_tile", + "Files": [ + "diffusion_pytorch_model.bin", + "config.json" + ], + "Subfolder": "tile", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "OpenPose", + "RepositoryPath": "lllyasviel/control_v11p_sd15_openpose", + "Files": [ + "diffusion_pytorch_model.safetensors", + "config.json" + ], + "Subfolder": "openpose", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "LineArt", + "RepositoryPath": "lllyasviel/control_v11p_sd15_lineart", + "Files": [ + "diffusion_pytorch_model.safetensors", + "config.json" + ], + "Subfolder": "lineart", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "LineArt Anime", + "RepositoryPath": "lllyasviel/control_v11p_sd15s2_lineart_anime", + "Files": [ + "diffusion_pytorch_model.safetensors", + "config.json" + ], + "Subfolder": "lineart_anime", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "NormalBae", + "RepositoryPath": "lllyasviel/control_v11p_sd15_normalbae", + "Files": [ + "diffusion_pytorch_model.safetensors", + "config.json" + ], + "Subfolder": "normalbae", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "Seg", + "RepositoryPath": "lllyasviel/control_v11p_sd15_seg", + "Files": [ + "diffusion_pytorch_model.safetensors", + "config.json" + ], + "Subfolder": "seg", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "Soft Edge", + "RepositoryPath": "lllyasviel/control_v11p_sd15_softedge", + "Files": [ + "diffusion_pytorch_model.safetensors", + "config.json" + ], + "Subfolder": "softedge", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "Scribble", + "RepositoryPath": "lllyasviel/control_v11p_sd15_scribble", + "Files": [ + "diffusion_pytorch_model.safetensors", + "config.json" + ], + "Subfolder": "scribble", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersControlNet", + "ModelName": "Shuffle", + "RepositoryPath": "lllyasviel/control_v11e_sd15_shuffle", + "Files": [ + "diffusion_pytorch_model.safetensors", + "config.json" + ], + "Subfolder": "shuffle", + "LicenseType": "Open RAIL" + }, + { + "ModelCategory": "DiffusersClipVision", + "ModelName": "IP Adapter Encoder", + "RepositoryPath": "InvokeAI/ip_adapter_sd_image_encoder", + "Files": [ + "config.json", + "model.safetensors" + ], + "Subfolder": "ip_adapter_sd_image_encoder", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersClipVision", + "ModelName": "IP Adapter Encoder (SDXL)", + "RepositoryPath": "InvokeAI/ip_adapter_sdxl_image_encoder", + "Files": [ + "config.json", + "model.safetensors" + ], + "Subfolder": "ip_adapter_sd_image_encoder", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersIpAdapter", + "ModelName": "SD 1.5 Adapter", + "RepositoryPath": "InvokeAI/ip_adapter_sd15", + "Files": [ + "image_encoder.txt", + "ip_adapter.bin" + ], + "Subfolder": "ip_adapter_sd15", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersIpAdapter", + "ModelName": "SD 1.5 Light Adapter", + "RepositoryPath": "InvokeAI/ip_adapter_sd15_light", + "Files": [ + "image_encoder.txt", + "ip_adapter.bin" + ], + "Subfolder": "ip_adapter_sd15_light", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersIpAdapter", + "ModelName": "SD 1.5 Plus Adapter", + "RepositoryPath": "InvokeAI/ip_adapter_plus_sd15", + "Files": [ + "image_encoder.txt", + "ip_adapter.bin" + ], + "Subfolder": "ip_adapter_plus_sd15", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersIpAdapter", + "ModelName": "SD 1.5 Face Plus Adapter", + "RepositoryPath": "InvokeAI/ip_adapter_plus_face_sd15", + "Files": [ + "image_encoder.txt", + "ip_adapter.bin" + ], + "Subfolder": "ip_adapter_plus_face_sd15", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersIpAdapterXl", + "ModelName": "SDXL Adapter", + "RepositoryPath": "InvokeAI/ip_adapter_sdxl", + "Files": [ + "image_encoder.txt", + "ip_adapter.bin" + ], + "Subfolder": "ip_adapter_sdxl", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersIpAdapterXl", + "ModelName": "SDXL Plus Adapter", + "RepositoryPath": "InvokeAI/ip-adapter-plus_sdxl_vit-h", + "Files": [ + "image_encoder.txt", + "ip_adapter.bin" + ], + "Subfolder": "ip_adapter_plus_sdxl_vit-h", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersIpAdapterXl", + "ModelName": "SDXL Face Plus Adapter", + "RepositoryPath": "InvokeAI/ip-adapter-plus-face_sdxl_vit-h", + "Files": [ + "image_encoder.txt", + "ip_adapter.bin" + ], + "Subfolder": "ip_adapter_plus_face_sdxl_vit-h", + "LicenseType": "CreativeML Open RAIL-M" + }, + { + "ModelCategory": "DiffusersT2IAdapter", + "ModelName": "Sketch", + "RepositoryPath": "TencentARC/t2iadapter_sketch_sd15v2", + "Files": [ + "config.json", + "diffusion_pytorch_model.bin" + ], + "Subfolder": "t2iadapter_sketch_sd15v2", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersT2IAdapter", + "ModelName": "Depth", + "RepositoryPath": "TencentARC/t2iadapter_depth_sd15v2", + "Files": [ + "config.json", + "diffusion_pytorch_model.bin" + ], + "Subfolder": "t2iadapter_depth_sd15v2", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersT2IAdapter", + "ModelName": "Canny", + "RepositoryPath": "TencentARC/t2iadapter_canny_sd15v2", + "Files": [ + "config.json", + "diffusion_pytorch_model.bin" + ], + "Subfolder": "t2iadapter_canny_sd15v2", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersT2IAdapter", + "ModelName": "Depth-Zoe", + "RepositoryPath": "TencentARC/t2iadapter_zoedepth_sd15v1", + "Files": [ + "config.json", + "diffusion_pytorch_model.bin" + ], + "Subfolder": "t2iadapter_zoedepth_sd15v1", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersT2IAdapter", + "ModelName": "Sketch (SDXL)", + "RepositoryPath": "TencentARC/t2i-adapter-sketch-sdxl-1.0", + "Files": [ + "config.json", + "diffusion_pytorch_model.safetensors" + ], + "Subfolder": "t2i-adapter-sketch-sdxl-1.0", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersT2IAdapter", + "ModelName": "Depth-Zoe (SDXL)", + "RepositoryPath": "TencentARC/t2i-adapter-depth-zoe-sdxl-1.0", + "Files": [ + "config.json", + "diffusion_pytorch_model.safetensors" + ], + "Subfolder": "t2i-adapter-depth-zoe-sdxl-1.0", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersT2IAdapter", + "ModelName": "OpenPose (SDXL)", + "RepositoryPath": "TencentARC/t2i-adapter-openpose-sdxl-1.0", + "Files": [ + "config.json", + "diffusion_pytorch_model.safetensors" + ], + "Subfolder": "t2i-adapter-openpose-sdxl-1.0", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersT2IAdapter", + "ModelName": "Depth-MiDaS (SDXL)", + "RepositoryPath": "TencentARC/t2i-adapter-depth-midas-sdxl-1.0", + "Files": [ + "config.json", + "diffusion_pytorch_model.safetensors" + ], + "Subfolder": "t2i-adapter-depth-midas-sdxl-1.0", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersT2IAdapter", + "ModelName": "LineArt (SDXL)", + "RepositoryPath": "TencentARC/t2i-adapter-lineart-sdxl-1.0", + "Files": [ + "config.json", + "diffusion_pytorch_model.safetensors" + ], + "Subfolder": "t2i-adapter-lineart-sdxl-1.0", + "LicenseType": "Apache 2.0" + }, + { + "ModelCategory": "DiffusersT2IAdapter", + "ModelName": "Canny (SDXL)", + "RepositoryPath": "TencentARC/t2i-adapter-canny-sdxl-1.0", + "Files": [ + "config.json", + "diffusion_pytorch_model.safetensors" + ], + "Subfolder": "t2i-adapter-canny-sdxl-1.0", + "LicenseType": "Apache 2.0" + } +] diff --git a/StabilityMatrix.Avalonia/Assets/santahat.png b/StabilityMatrix.Avalonia/Assets/santahat.png new file mode 100644 index 00000000..40815d07 Binary files /dev/null and b/StabilityMatrix.Avalonia/Assets/santahat.png differ diff --git a/StabilityMatrix.Avalonia/Assets/sitecustomize.py b/StabilityMatrix.Avalonia/Assets/sitecustomize.py index 8c2718f4..b6ebb4c1 100644 --- a/StabilityMatrix.Avalonia/Assets/sitecustomize.py +++ b/StabilityMatrix.Avalonia/Assets/sitecustomize.py @@ -46,7 +46,41 @@ def audit(event: str, *args): # Reconfigure stdout to UTF-8 # noinspection PyUnresolvedReferences +sys.stdin.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8") +sys.stderr.reconfigure(encoding="utf-8") # Install the audit hook sys.addaudithook(audit) + +# Patch Rich terminal detection +def _patch_rich_console(): + try: + from rich import console + + class _Console(console.Console): + @property + def is_terminal(self) -> bool: + return True + + console.Console = _Console + except ImportError: + pass + except Exception as e: + print("[sitecustomize error]:", e) + +_patch_rich_console() + +# Patch tqdm to use stdout instead of stderr +def _patch_tqdm(): + try: + import sys + from tqdm import std + + sys.stderr = sys.stdout + except ImportError: + pass + except Exception as e: + print("[sitecustomize error]:", e) + +_patch_tqdm() diff --git a/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs b/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs index 80704547..76e08f3c 100644 --- a/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs +++ b/StabilityMatrix.Avalonia/Controls/AdvancedImageBox.axaml.cs @@ -54,11 +54,7 @@ public class AdvancedImageBox : TemplatedControl remove => _propertyChanged -= value; } - protected bool RaiseAndSetIfChanged( - ref T field, - T value, - [CallerMemberName] string? propertyName = null - ) + protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) return false; @@ -335,9 +331,7 @@ public class AdvancedImageBox : TemplatedControl if (index < Count - 1) index++; - return constrainZoomLevel > 0 && this[index] >= constrainZoomLevel - ? constrainZoomLevel - : this[index]; + return constrainZoomLevel > 0 && this[index] >= constrainZoomLevel ? constrainZoomLevel : this[index]; } /// @@ -352,9 +346,7 @@ public class AdvancedImageBox : TemplatedControl if (index > 0) index--; - return constrainZoomLevel > 0 && this[index] <= constrainZoomLevel - ? constrainZoomLevel - : this[index]; + return constrainZoomLevel > 0 && this[index] <= constrainZoomLevel ? constrainZoomLevel : this[index]; } /// @@ -525,11 +517,10 @@ public class AdvancedImageBox : TemplatedControl #endregion #region Properties - public static readonly DirectProperty CanRenderProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanRender), - o => o.CanRender - ); + public static readonly DirectProperty CanRenderProperty = AvaloniaProperty.RegisterDirect< + AdvancedImageBox, + bool + >(nameof(CanRender), o => o.CanRender); /// /// Gets or sets if control can render the image @@ -560,11 +551,10 @@ public class AdvancedImageBox : TemplatedControl set => SetValue(GridCellSizeProperty, value); } - public static readonly StyledProperty GridColorProperty = - AvaloniaProperty.Register( - nameof(GridColor), - SolidColorBrush.Parse("#181818") - ); + public static readonly StyledProperty GridColorProperty = AvaloniaProperty.Register< + AdvancedImageBox, + ISolidColorBrush + >(nameof(GridColor), SolidColorBrush.Parse("#181818")); /// /// Gets or sets the color used to create the checkerboard style background @@ -575,11 +565,10 @@ public class AdvancedImageBox : TemplatedControl set => SetValue(GridColorProperty, value); } - public static readonly StyledProperty GridColorAlternateProperty = - AvaloniaProperty.Register( - nameof(GridColorAlternate), - SolidColorBrush.Parse("#252525") - ); + public static readonly StyledProperty GridColorAlternateProperty = AvaloniaProperty.Register< + AdvancedImageBox, + ISolidColorBrush + >(nameof(GridColorAlternate), SolidColorBrush.Parse("#252525")); /// /// Gets or sets the color used to create the checkerboard style background @@ -606,7 +595,8 @@ public class AdvancedImageBox : TemplatedControl { var loader = ImageLoader.AsyncImageLoader; - Dispatcher.UIThread + Dispatcher + .UIThread .InvokeAsync(async () => { Image = await loader.ProvideImageAsync(value); @@ -616,10 +606,9 @@ public class AdvancedImageBox : TemplatedControl } } - public static readonly StyledProperty ImageProperty = AvaloniaProperty.Register< - AdvancedImageBox, - Bitmap? - >(nameof(Image)); + public static readonly StyledProperty ImageProperty = AvaloniaProperty.Register( + nameof(Image) + ); /// /// Gets or sets the image to be displayed @@ -656,8 +645,7 @@ public class AdvancedImageBox : TemplatedControl { offsetBackup = Offset; - var zoomFactorScale = - (float)GetZoomLevelToFit(newImage) / GetZoomLevelToFit(oldImage); + var zoomFactorScale = (float)GetZoomLevelToFit(newImage) / GetZoomLevelToFit(oldImage); var imageScale = newImage.Size / oldImage.Size; Debug.WriteLine($"Image scale: {imageScale}"); @@ -731,8 +719,10 @@ public class AdvancedImageBox : TemplatedControl public bool HaveTrackerImage => _trackerImage is not null; - public static readonly StyledProperty TrackerImageAutoZoomProperty = - AvaloniaProperty.Register(nameof(TrackerImageAutoZoom), true); + public static readonly StyledProperty TrackerImageAutoZoomProperty = AvaloniaProperty.Register< + AdvancedImageBox, + bool + >(nameof(TrackerImageAutoZoom), true); /// /// Gets or sets if the tracker image will be scaled to the current zoom @@ -743,8 +733,10 @@ public class AdvancedImageBox : TemplatedControl set => SetValue(TrackerImageAutoZoomProperty, value); } - public static readonly StyledProperty IsTrackerImageEnabledProperty = - AvaloniaProperty.Register("IsTrackerImageEnabled"); + public static readonly StyledProperty IsTrackerImageEnabledProperty = AvaloniaProperty.Register< + AdvancedImageBox, + bool + >("IsTrackerImageEnabled"); public bool IsTrackerImageEnabled { @@ -776,10 +768,10 @@ public class AdvancedImageBox : TemplatedControl } } - public static readonly StyledProperty ShowGridProperty = AvaloniaProperty.Register< - AdvancedImageBox, - bool - >(nameof(ShowGrid), true); + public static readonly StyledProperty ShowGridProperty = AvaloniaProperty.Register( + nameof(ShowGrid), + true + ); /// /// Gets or sets the grid visibility when reach high zoom levels @@ -791,10 +783,7 @@ public class AdvancedImageBox : TemplatedControl } public static readonly DirectProperty PointerPositionProperty = - AvaloniaProperty.RegisterDirect( - nameof(PointerPosition), - o => o.PointerPosition - ); + AvaloniaProperty.RegisterDirect(nameof(PointerPosition), o => o.PointerPosition); /// /// Gets the current pointer position @@ -805,11 +794,10 @@ public class AdvancedImageBox : TemplatedControl private set => SetAndRaise(PointerPositionProperty, ref _pointerPosition, value); } - public static readonly DirectProperty IsPanningProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsPanning), - o => o.IsPanning - ); + public static readonly DirectProperty IsPanningProperty = AvaloniaProperty.RegisterDirect< + AdvancedImageBox, + bool + >(nameof(IsPanning), o => o.IsPanning); /// /// Gets if control is currently panning @@ -836,11 +824,10 @@ public class AdvancedImageBox : TemplatedControl } } - public static readonly DirectProperty IsSelectingProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsSelecting), - o => o.IsSelecting - ); + public static readonly DirectProperty IsSelectingProperty = AvaloniaProperty.RegisterDirect< + AdvancedImageBox, + bool + >(nameof(IsSelecting), o => o.IsSelecting); /// /// Gets if control is currently selecting a ROI @@ -863,10 +850,10 @@ public class AdvancedImageBox : TemplatedControl } } - public static readonly StyledProperty AutoPanProperty = AvaloniaProperty.Register< - AdvancedImageBox, - bool - >(nameof(AutoPan), true); + public static readonly StyledProperty AutoPanProperty = AvaloniaProperty.Register( + nameof(AutoPan), + true + ); /// /// Gets or sets if the control can pan with the mouse @@ -877,11 +864,10 @@ public class AdvancedImageBox : TemplatedControl set => SetValue(AutoPanProperty, value); } - public static readonly StyledProperty PanWithMouseButtonsProperty = - AvaloniaProperty.Register( - nameof(PanWithMouseButtons), - MouseButtons.LeftButton | MouseButtons.MiddleButton - ); + public static readonly StyledProperty PanWithMouseButtonsProperty = AvaloniaProperty.Register< + AdvancedImageBox, + MouseButtons + >(nameof(PanWithMouseButtons), MouseButtons.LeftButton | MouseButtons.MiddleButton); /// /// Gets or sets the mouse buttons to pan the image @@ -906,11 +892,10 @@ public class AdvancedImageBox : TemplatedControl set => SetValue(PanWithArrowsProperty, value); } - public static readonly StyledProperty SelectWithMouseButtonsProperty = - AvaloniaProperty.Register( - nameof(SelectWithMouseButtons), - MouseButtons.LeftButton - ); + public static readonly StyledProperty SelectWithMouseButtonsProperty = AvaloniaProperty.Register< + AdvancedImageBox, + MouseButtons + >(nameof(SelectWithMouseButtons), MouseButtons.LeftButton); /// /// Gets or sets the mouse buttons to select a region on image @@ -935,10 +920,10 @@ public class AdvancedImageBox : TemplatedControl set => SetValue(InvertMousePanProperty, value); } - public static readonly StyledProperty AutoCenterProperty = AvaloniaProperty.Register< - AdvancedImageBox, - bool - >(nameof(AutoCenter), true); + public static readonly StyledProperty AutoCenterProperty = AvaloniaProperty.Register( + nameof(AutoCenter), + true + ); /// /// Gets or sets if image is auto centered @@ -993,10 +978,10 @@ public class AdvancedImageBox : TemplatedControl } } - public static readonly StyledProperty AllowZoomProperty = AvaloniaProperty.Register< - AdvancedImageBox, - bool - >(nameof(AllowZoom), true); + public static readonly StyledProperty AllowZoomProperty = AvaloniaProperty.Register( + nameof(AllowZoom), + true + ); /// /// Gets or sets if zoom is allowed @@ -1007,14 +992,12 @@ public class AdvancedImageBox : TemplatedControl set => SetValue(AllowZoomProperty, value); } - public static readonly DirectProperty< - AdvancedImageBox, - ZoomLevelCollection - > ZoomLevelsProperty = AvaloniaProperty.RegisterDirect( - nameof(ZoomLevels), - o => o.ZoomLevels, - (o, v) => o.ZoomLevels = v - ); + public static readonly DirectProperty ZoomLevelsProperty = + AvaloniaProperty.RegisterDirect( + nameof(ZoomLevels), + o => o.ZoomLevels, + (o, v) => o.ZoomLevels = v + ); ZoomLevelCollection _zoomLevels = ZoomLevelCollection.Default; @@ -1028,10 +1011,10 @@ public class AdvancedImageBox : TemplatedControl set => SetAndRaise(ZoomLevelsProperty, ref _zoomLevels, value); } - public static readonly StyledProperty MinZoomProperty = AvaloniaProperty.Register< - AdvancedImageBox, - int - >(nameof(MinZoom), 10); + public static readonly StyledProperty MinZoomProperty = AvaloniaProperty.Register( + nameof(MinZoom), + 10 + ); /// /// Gets or sets the minimum possible zoom. @@ -1043,10 +1026,10 @@ public class AdvancedImageBox : TemplatedControl set => SetValue(MinZoomProperty, value); } - public static readonly StyledProperty MaxZoomProperty = AvaloniaProperty.Register< - AdvancedImageBox, - int - >(nameof(MaxZoom), 6400); + public static readonly StyledProperty MaxZoomProperty = AvaloniaProperty.Register( + nameof(MaxZoom), + 6400 + ); /// /// Gets or sets the maximum possible zoom. @@ -1058,8 +1041,10 @@ public class AdvancedImageBox : TemplatedControl set => SetValue(MaxZoomProperty, value); } - public static readonly StyledProperty ConstrainZoomOutToFitLevelProperty = - AvaloniaProperty.Register(nameof(ConstrainZoomOutToFitLevel), true); + public static readonly StyledProperty ConstrainZoomOutToFitLevelProperty = AvaloniaProperty.Register< + AdvancedImageBox, + bool + >(nameof(ConstrainZoomOutToFitLevel), true); /// /// Gets or sets if the zoom out should constrain to fit image as the lowest zoom level. @@ -1070,8 +1055,10 @@ public class AdvancedImageBox : TemplatedControl set => SetValue(ConstrainZoomOutToFitLevelProperty, value); } - public static readonly DirectProperty OldZoomProperty = - AvaloniaProperty.RegisterDirect(nameof(OldZoom), o => o.OldZoom); + public static readonly DirectProperty OldZoomProperty = AvaloniaProperty.RegisterDirect< + AdvancedImageBox, + int + >(nameof(OldZoom), o => o.OldZoom); private int _oldZoom = 100; @@ -1085,10 +1072,10 @@ public class AdvancedImageBox : TemplatedControl private set => SetAndRaise(OldZoomProperty, ref _oldZoom, value); } - public static readonly StyledProperty ZoomProperty = AvaloniaProperty.Register< - AdvancedImageBox, - int - >(nameof(Zoom), 100); + public static readonly StyledProperty ZoomProperty = AvaloniaProperty.Register( + nameof(Zoom), + 100 + ); /// /// Gets or sets the zoom. @@ -1178,11 +1165,10 @@ public class AdvancedImageBox : TemplatedControl /// The height of the scaled image. public double ScaledImageHeight => Image?.Size.Height * ZoomFactor ?? 0; - public static readonly StyledProperty PixelGridColorProperty = - AvaloniaProperty.Register( - nameof(PixelGridColor), - Brushes.DimGray - ); + public static readonly StyledProperty PixelGridColorProperty = AvaloniaProperty.Register< + AdvancedImageBox, + ISolidColorBrush + >(nameof(PixelGridColor), Brushes.DimGray); /// /// Gets or sets the color of the pixel grid. @@ -1194,8 +1180,10 @@ public class AdvancedImageBox : TemplatedControl set => SetValue(PixelGridColorProperty, value); } - public static readonly StyledProperty PixelGridZoomThresholdProperty = - AvaloniaProperty.Register(nameof(PixelGridZoomThreshold), 13); + public static readonly StyledProperty PixelGridZoomThresholdProperty = AvaloniaProperty.Register< + AdvancedImageBox, + int + >(nameof(PixelGridZoomThreshold), 13); /// /// Minimum size of zoomed pixel's before the pixel grid will be drawn @@ -1206,11 +1194,15 @@ public class AdvancedImageBox : TemplatedControl set => SetValue(PixelGridZoomThresholdProperty, value); } - public static readonly StyledProperty SelectionModeProperty = - AvaloniaProperty.Register(nameof(SelectionMode)); + public static readonly StyledProperty SelectionModeProperty = AvaloniaProperty.Register< + AdvancedImageBox, + SelectionModes + >(nameof(SelectionMode)); - public static readonly StyledProperty IsPixelGridEnabledProperty = - AvaloniaProperty.Register("IsPixelGridEnabled", true); + public static readonly StyledProperty IsPixelGridEnabledProperty = AvaloniaProperty.Register< + AdvancedImageBox, + bool + >("IsPixelGridEnabled", true); /// /// Whether or not to draw the pixel grid at the @@ -1227,11 +1219,10 @@ public class AdvancedImageBox : TemplatedControl set => SetValue(SelectionModeProperty, value); } - public static readonly StyledProperty SelectionColorProperty = - AvaloniaProperty.Register( - nameof(SelectionColor), - new SolidColorBrush(new Color(127, 0, 128, 255)) - ); + public static readonly StyledProperty SelectionColorProperty = AvaloniaProperty.Register< + AdvancedImageBox, + ISolidColorBrush + >(nameof(SelectionColor), new SolidColorBrush(new Color(127, 0, 128, 255))); public ISolidColorBrush SelectionColor { @@ -1313,14 +1304,9 @@ public class AdvancedImageBox : TemplatedControl AffectsRender(ShowGridProperty); HorizontalScrollBar = - e.NameScope.Find("PART_HorizontalScrollBar") - ?? throw new NullReferenceException(); - VerticalScrollBar = - e.NameScope.Find("PART_VerticalScrollBar") - ?? throw new NullReferenceException(); - ViewPort = - e.NameScope.Find("PART_ViewPort") - ?? throw new NullReferenceException(); + e.NameScope.Find("PART_HorizontalScrollBar") ?? throw new NullReferenceException(); + VerticalScrollBar = e.NameScope.Find("PART_VerticalScrollBar") ?? throw new NullReferenceException(); + ViewPort = e.NameScope.Find("PART_ViewPort") ?? throw new NullReferenceException(); SizeModeChanged(); @@ -1347,13 +1333,12 @@ public class AdvancedImageBox : TemplatedControl // If we're in high zoom, switch off bitmap interpolation mode // Otherwise use high quality - BitmapInterpolationMode = isHighZoom - ? BitmapInterpolationMode.None - : BitmapInterpolationMode.HighQuality; + BitmapInterpolationMode = isHighZoom ? BitmapInterpolationMode.None : BitmapInterpolationMode.HighQuality; InvalidateVisual(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RenderBackgroundGrid(DrawingContext context) { var size = GridCellSize; @@ -1387,8 +1372,6 @@ public class AdvancedImageBox : TemplatedControl public override void Render(DrawingContext context) { - base.Render(context); - var gridCellSize = GridCellSize; if (ShowGrid & gridCellSize > 0 && (!IsHorizontalBarVisible || !IsVerticalBarVisible)) @@ -1399,9 +1382,7 @@ public class AdvancedImageBox : TemplatedControl var zoomFactor = ZoomFactor; var shouldDrawPixelGrid = - IsPixelGridEnabled - && SizeMode == SizeModes.Normal - && zoomFactor > PixelGridZoomThreshold; + IsPixelGridEnabled && SizeMode == SizeModes.Normal && zoomFactor > PixelGridZoomThreshold; // Draw Grid /*var viewPortSize = ViewPortSize; @@ -1441,16 +1422,10 @@ public class AdvancedImageBox : TemplatedControl if (HaveTrackerImage && _pointerPosition is { X: >= 0, Y: >= 0 }) { var destSize = TrackerImageAutoZoom - ? new Size( - _trackerImage!.Size.Width * zoomFactor, - _trackerImage.Size.Height * zoomFactor - ) + ? new Size(_trackerImage!.Size.Width * zoomFactor, _trackerImage.Size.Height * zoomFactor) : image.Size; - var destPos = new Point( - _pointerPosition.X - destSize.Width / 2, - _pointerPosition.Y - destSize.Height / 2 - ); + var destPos = new Point(_pointerPosition.X - destSize.Width / 2, _pointerPosition.Y - destSize.Height / 2); context.DrawImage(_trackerImage!, new Rect(destPos, destSize)); } @@ -1462,30 +1437,14 @@ public class AdvancedImageBox : TemplatedControl var offsetY = Offset.Y % zoomFactor; Pen pen = new(PixelGridColor); - for ( - var x = imageViewPort.X + zoomFactor - offsetX; - x < imageViewPort.Right; - x += zoomFactor - ) + for (var x = imageViewPort.X + zoomFactor - offsetX; x < imageViewPort.Right; x += zoomFactor) { - context.DrawLine( - pen, - new Point(x, imageViewPort.X), - new Point(x, imageViewPort.Bottom) - ); + context.DrawLine(pen, new Point(x, imageViewPort.X), new Point(x, imageViewPort.Bottom)); } - for ( - var y = imageViewPort.Y + zoomFactor - offsetY; - y < imageViewPort.Bottom; - y += zoomFactor - ) + for (var y = imageViewPort.Y + zoomFactor - offsetY; y < imageViewPort.Bottom; y += zoomFactor) { - context.DrawLine( - pen, - new Point(imageViewPort.Y, y), - new Point(imageViewPort.Right, y) - ); + context.DrawLine(pen, new Point(imageViewPort.Y, y), new Point(imageViewPort.Right, y)); } context.DrawRectangle(pen, imageViewPort); @@ -1496,12 +1455,7 @@ public class AdvancedImageBox : TemplatedControl var rect = GetOffsetRectangle(SelectionRegion); var selectionColor = SelectionColor; context.FillRectangle(selectionColor, rect); - var color = Color.FromArgb( - 255, - selectionColor.Color.R, - selectionColor.Color.G, - selectionColor.Color.B - ); + var color = Color.FromArgb(255, selectionColor.Color.R, selectionColor.Color.G, selectionColor.Color.B); context.DrawRectangle(new Pen(color.ToUInt32()), rect); } } @@ -1609,8 +1563,7 @@ public class AdvancedImageBox : TemplatedControl { if ( !( - pointer.Properties.IsLeftButtonPressed - && (SelectWithMouseButtons & MouseButtons.LeftButton) != 0 + pointer.Properties.IsLeftButtonPressed && (SelectWithMouseButtons & MouseButtons.LeftButton) != 0 || pointer.Properties.IsMiddleButtonPressed && (SelectWithMouseButtons & MouseButtons.MiddleButton) != 0 || pointer.Properties.IsRightButtonPressed @@ -1624,12 +1577,10 @@ public class AdvancedImageBox : TemplatedControl { if ( !( - pointer.Properties.IsLeftButtonPressed - && (PanWithMouseButtons & MouseButtons.LeftButton) != 0 + pointer.Properties.IsLeftButtonPressed && (PanWithMouseButtons & MouseButtons.LeftButton) != 0 || pointer.Properties.IsMiddleButtonPressed && (PanWithMouseButtons & MouseButtons.MiddleButton) != 0 - || pointer.Properties.IsRightButtonPressed - && (PanWithMouseButtons & MouseButtons.RightButton) != 0 + || pointer.Properties.IsRightButtonPressed && (PanWithMouseButtons & MouseButtons.RightButton) != 0 ) || !AutoPan || SizeMode != SizeModes.Normal @@ -2669,12 +2620,8 @@ public class AdvancedImageBox : TemplatedControl case SizeModes.Normal: if (AutoCenter) { - xOffset = ( - !IsHorizontalBarVisible ? (viewPortSize.Width - ScaledImageWidth) / 2 : 0 - ); - yOffset = ( - !IsVerticalBarVisible ? (viewPortSize.Height - ScaledImageHeight) / 2 : 0 - ); + xOffset = (!IsHorizontalBarVisible ? (viewPortSize.Width - ScaledImageWidth) / 2 : 0); + yOffset = (!IsVerticalBarVisible ? (viewPortSize.Height - ScaledImageHeight) / 2 : 0); } width = Math.Min(ScaledImageWidth - Math.Abs(Offset.X), viewPortSize.Width); @@ -2729,12 +2676,7 @@ public class AdvancedImageBox : TemplatedControl var pixelSize = SelectionPixelSize; using var frameBuffer = image.Lock(); - var newBitmap = new WriteableBitmap( - pixelSize, - image.Dpi, - frameBuffer.Format, - AlphaFormat.Unpremul - ); + var newBitmap = new WriteableBitmap(pixelSize, image.Dpi, frameBuffer.Format, AlphaFormat.Unpremul); using var newFrameBuffer = newBitmap.Lock(); var i = 0; diff --git a/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml b/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml index d9032f21..eb681d53 100644 --- a/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml +++ b/StabilityMatrix.Avalonia/Controls/AdvancedImageBoxView.axaml @@ -1,30 +1,70 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs b/StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs index 8f5f4502..0af2ea7a 100644 --- a/StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs +++ b/StabilityMatrix.Avalonia/Controls/BetterAdvancedImage.cs @@ -15,24 +15,28 @@ public class BetterAdvancedImage : AdvancedImage #region Reflection Shenanigans to access private parent fields [NotNull] private static readonly FieldInfo? IsCornerRadiusUsedField = typeof(AdvancedImage).GetField( - "_isCornerRadiusUsed",BindingFlags.Instance | BindingFlags.NonPublic); - + "_isCornerRadiusUsed", + BindingFlags.Instance | BindingFlags.NonPublic + ); + [NotNull] private static readonly FieldInfo? CornerRadiusClipField = typeof(AdvancedImage).GetField( - "_cornerRadiusClip",BindingFlags.Instance | BindingFlags.NonPublic); - + "_cornerRadiusClip", + BindingFlags.Instance | BindingFlags.NonPublic + ); + private bool IsCornerRadiusUsed { get => IsCornerRadiusUsedField.GetValue(this) as bool? ?? false; set => IsCornerRadiusUsedField.SetValue(this, value); } - + private RoundedRect CornerRadiusClip { - get => (RoundedRect) CornerRadiusClipField.GetValue(this)!; + get => (RoundedRect)CornerRadiusClipField.GetValue(this)!; set => CornerRadiusClipField.SetValue(this, value); } - + static BetterAdvancedImage() { if (IsCornerRadiusUsedField is null) @@ -45,16 +49,14 @@ public class BetterAdvancedImage : AdvancedImage } } #endregion - + protected override Type StyleKeyOverride { get; } = typeof(AdvancedImage); - - public BetterAdvancedImage(Uri? baseUri) : base(baseUri) - { - } - public BetterAdvancedImage(IServiceProvider serviceProvider) : base(serviceProvider) - { - } + public BetterAdvancedImage(Uri? baseUri) + : base(baseUri) { } + + public BetterAdvancedImage(IServiceProvider serviceProvider) + : base(serviceProvider) { } /// /// @@ -69,61 +71,66 @@ public class BetterAdvancedImage : AdvancedImage var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); var scaledSize = sourceSize * scale; - + // Calculate starting points for dest var destX = HorizontalContentAlignment switch { HorizontalAlignment.Left => 0, - HorizontalAlignment.Center => (int) (viewPort.Width - scaledSize.Width) / 2, - HorizontalAlignment.Right => (int) (viewPort.Width - scaledSize.Width), + HorizontalAlignment.Center => (int)(viewPort.Width - scaledSize.Width) / 2, + HorizontalAlignment.Right => (int)(viewPort.Width - scaledSize.Width), // Stretch is default, use center - HorizontalAlignment.Stretch => (int) (viewPort.Width - scaledSize.Width) / 2, + HorizontalAlignment.Stretch + => (int)(viewPort.Width - scaledSize.Width) / 2, _ => throw new ArgumentException(nameof(HorizontalContentAlignment)) }; var destY = VerticalContentAlignment switch { VerticalAlignment.Top => 0, - VerticalAlignment.Center => (int) (viewPort.Height - scaledSize.Height) / 2, - VerticalAlignment.Bottom => (int) (viewPort.Height - scaledSize.Height), + VerticalAlignment.Center => (int)(viewPort.Height - scaledSize.Height) / 2, + VerticalAlignment.Bottom => (int)(viewPort.Height - scaledSize.Height), VerticalAlignment.Stretch => 0, // Stretch is default, use top _ => throw new ArgumentException(nameof(VerticalContentAlignment)) }; - - var destRect = viewPort - .CenterRect(new Rect(scaledSize)) - .WithX(destX) - .WithY(destY) - .Intersect(viewPort); + + var destRect = viewPort.CenterRect(new Rect(scaledSize)).WithX(destX).WithY(destY).Intersect(viewPort); var destRectUnscaledSize = destRect.Size / scale; - + // Calculate starting points for source var sourceX = HorizontalContentAlignment switch { HorizontalAlignment.Left => 0, - HorizontalAlignment.Center => (int) (sourceSize - destRectUnscaledSize).Width / 2, - HorizontalAlignment.Right => (int) (sourceSize - destRectUnscaledSize).Width, + HorizontalAlignment.Center => (int)(sourceSize - destRectUnscaledSize).Width / 2, + HorizontalAlignment.Right => (int)(sourceSize - destRectUnscaledSize).Width, // Stretch is default, use center - HorizontalAlignment.Stretch => (int) (sourceSize - destRectUnscaledSize).Width / 2, + HorizontalAlignment.Stretch + => (int)(sourceSize - destRectUnscaledSize).Width / 2, _ => throw new ArgumentException(nameof(HorizontalContentAlignment)) }; var sourceY = VerticalContentAlignment switch { VerticalAlignment.Top => 0, - VerticalAlignment.Center => (int) (sourceSize - destRectUnscaledSize).Height / 2, - VerticalAlignment.Bottom => (int) (sourceSize - destRectUnscaledSize).Height, + VerticalAlignment.Center => (int)(sourceSize - destRectUnscaledSize).Height / 2, + VerticalAlignment.Bottom => (int)(sourceSize - destRectUnscaledSize).Height, VerticalAlignment.Stretch => 0, // Stretch is default, use top _ => throw new ArgumentException(nameof(VerticalContentAlignment)) }; - + var sourceRect = new Rect(sourceSize) .CenterRect(new Rect(destRect.Size / scale)) .WithX(sourceX) .WithY(sourceY); - - DrawingContext.PushedState? pushedState = - IsCornerRadiusUsed ? context.PushClip(CornerRadiusClip) : null; - context.DrawImage(source, sourceRect, destRect); - pushedState?.Dispose(); + + if (IsCornerRadiusUsed) + { + using (context.PushClip(CornerRadiusClip)) + { + context.DrawImage(source, sourceRect, destRect); + } + } + else + { + context.DrawImage(source, sourceRect, destRect); + } } else { diff --git a/StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/BatchSizeCard.axaml similarity index 90% rename from StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml rename to StabilityMatrix.Avalonia/Controls/Inference/BatchSizeCard.axaml index 47ab4045..a18717d9 100644 --- a/StabilityMatrix.Avalonia/Controls/BatchSizeCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/Inference/BatchSizeCard.axaml @@ -6,6 +6,7 @@ xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" + xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters" x:DataType="vmInference:BatchSizeCardViewModel"> @@ -19,7 +20,7 @@ + diff --git a/StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml.cs new file mode 100644 index 00000000..8a1b364d --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/Inference/SelectImageCard.axaml.cs @@ -0,0 +1,39 @@ +using System; +using System.Drawing; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using DynamicData.Binding; +using StabilityMatrix.Avalonia.ViewModels.Inference; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.Controls; + +[Transient] +public class SelectImageCard : DropTargetTemplatedControlBase +{ + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + if (DataContext is not SelectImageCardViewModel vm) + return; + + if (e.NameScope.Find("PART_BetterAdvancedImage") is not { } image) + return; + + image + .WhenPropertyChanged(x => x.CurrentImage) + .Subscribe(propertyValue => + { + if (propertyValue.Value?.Size is { } size) + { + vm.CurrentBitmapSize = new Size(Convert.ToInt32(size.Width), Convert.ToInt32(size.Height)); + } + else + { + vm.CurrentBitmapSize = Size.Empty; + } + }); + } +} diff --git a/StabilityMatrix.Avalonia/Controls/SharpenCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/SharpenCard.axaml similarity index 100% rename from StabilityMatrix.Avalonia/Controls/SharpenCard.axaml rename to StabilityMatrix.Avalonia/Controls/Inference/SharpenCard.axaml diff --git a/StabilityMatrix.Avalonia/Controls/SharpenCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/Inference/SharpenCard.axaml.cs similarity index 100% rename from StabilityMatrix.Avalonia/Controls/SharpenCard.axaml.cs rename to StabilityMatrix.Avalonia/Controls/Inference/SharpenCard.axaml.cs diff --git a/StabilityMatrix.Avalonia/Controls/StackCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml similarity index 97% rename from StabilityMatrix.Avalonia/Controls/StackCard.axaml rename to StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml index 03b3c1d9..f2d3f7af 100644 --- a/StabilityMatrix.Avalonia/Controls/StackCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml @@ -20,7 +20,7 @@ - + diff --git a/StabilityMatrix.Avalonia/Controls/StackCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml.cs similarity index 85% rename from StabilityMatrix.Avalonia/Controls/StackCard.axaml.cs rename to StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml.cs index 2341a96d..ec325ceb 100644 --- a/StabilityMatrix.Avalonia/Controls/StackCard.axaml.cs +++ b/StabilityMatrix.Avalonia/Controls/Inference/StackCard.axaml.cs @@ -9,10 +9,10 @@ namespace StabilityMatrix.Avalonia.Controls; [Transient] public class StackCard : TemplatedControl { - public static readonly StyledProperty SpacingProperty = AvaloniaProperty.Register< - StackCard, - int - >("Spacing", 8); + public static readonly StyledProperty SpacingProperty = AvaloniaProperty.Register( + "Spacing", + 4 + ); public int Spacing { diff --git a/StabilityMatrix.Avalonia/Controls/StackEditableCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml similarity index 83% rename from StabilityMatrix.Avalonia/Controls/StackEditableCard.axaml rename to StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml index f89576d8..95f96b76 100644 --- a/StabilityMatrix.Avalonia/Controls/StackEditableCard.axaml +++ b/StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml @@ -11,7 +11,7 @@ x:DataType="vmInference:StackEditableCardViewModel"> - + @@ -58,23 +58,15 @@ Grid.Row="1" Theme="{StaticResource DraggableListBoxTheme}" ItemsSource="{Binding Cards}"> - - - - + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/StackEditableCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml.cs similarity index 100% rename from StabilityMatrix.Avalonia/Controls/StackEditableCard.axaml.cs rename to StabilityMatrix.Avalonia/Controls/Inference/StackEditableCard.axaml.cs diff --git a/StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml b/StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml new file mode 100644 index 00000000..19af6987 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml.cs b/StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml.cs new file mode 100644 index 00000000..0f01c83c --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/Inference/StackExpander.axaml.cs @@ -0,0 +1,41 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.Controls; + +[Transient] +public class StackExpander : TemplatedControl +{ + public static readonly StyledProperty IsExpandedProperty = Expander + .IsExpandedProperty + .AddOwner(); + + public static readonly StyledProperty ExpandDirectionProperty = Expander + .ExpandDirectionProperty + .AddOwner(); + + public static readonly StyledProperty SpacingProperty = AvaloniaProperty.Register( + "Spacing", + 8 + ); + + public ExpandDirection ExpandDirection + { + get => GetValue(ExpandDirectionProperty); + set => SetValue(ExpandDirectionProperty, value); + } + + public bool IsExpanded + { + get => GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); + } + + public int Spacing + { + get => GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } +} diff --git a/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml b/StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml similarity index 100% rename from StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml rename to StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml diff --git a/StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml.cs similarity index 100% rename from StabilityMatrix.Avalonia/Controls/UpscalerCard.axaml.cs rename to StabilityMatrix.Avalonia/Controls/Inference/UpscalerCard.axaml.cs diff --git a/StabilityMatrix.Avalonia/Controls/LineDashFrame.cs b/StabilityMatrix.Avalonia/Controls/LineDashFrame.cs index efb410da..667b22a5 100644 --- a/StabilityMatrix.Avalonia/Controls/LineDashFrame.cs +++ b/StabilityMatrix.Avalonia/Controls/LineDashFrame.cs @@ -12,8 +12,10 @@ public class LineDashFrame : Frame { protected override Type StyleKeyOverride { get; } = typeof(Frame); - public static readonly StyledProperty StrokeProperty = - AvaloniaProperty.Register("Stroke"); + public static readonly StyledProperty StrokeProperty = AvaloniaProperty.Register< + LineDashFrame, + ISolidColorBrush + >("Stroke"); public ISolidColorBrush Stroke { @@ -21,8 +23,10 @@ public class LineDashFrame : Frame set => SetValue(StrokeProperty, value); } - public static readonly StyledProperty StrokeThicknessProperty = - AvaloniaProperty.Register("StrokeThickness"); + public static readonly StyledProperty StrokeThicknessProperty = AvaloniaProperty.Register< + LineDashFrame, + double + >("StrokeThickness"); public double StrokeThickness { @@ -30,8 +34,10 @@ public class LineDashFrame : Frame set => SetValue(StrokeThicknessProperty, value); } - public static readonly StyledProperty StrokeDashLineProperty = - AvaloniaProperty.Register("StrokeDashLine"); + public static readonly StyledProperty StrokeDashLineProperty = AvaloniaProperty.Register< + LineDashFrame, + double + >("StrokeDashLine"); public double StrokeDashLine { @@ -39,8 +45,10 @@ public class LineDashFrame : Frame set => SetValue(StrokeDashLineProperty, value); } - public static readonly StyledProperty StrokeDashSpaceProperty = - AvaloniaProperty.Register("StrokeDashSpace"); + public static readonly StyledProperty StrokeDashSpaceProperty = AvaloniaProperty.Register< + LineDashFrame, + double + >("StrokeDashSpace"); public double StrokeDashSpace { @@ -48,8 +56,10 @@ public class LineDashFrame : Frame set => SetValue(StrokeDashSpaceProperty, value); } - public static readonly StyledProperty FillProperty = - AvaloniaProperty.Register("Fill"); + public static readonly StyledProperty FillProperty = AvaloniaProperty.Register< + LineDashFrame, + ISolidColorBrush + >("Fill"); public ISolidColorBrush Fill { @@ -82,17 +92,12 @@ public class LineDashFrame : Frame /// public override void Render(DrawingContext context) { - base.Render(context); - var width = Bounds.Width; var height = Bounds.Height; context.DrawRectangle(Fill, null, new Rect(0, 0, width, height)); - var dashPen = new Pen(Stroke, StrokeThickness) - { - DashStyle = new DashStyle(GetDashArray(width), 0) - }; + var dashPen = new Pen(Stroke, StrokeThickness) { DashStyle = new DashStyle(GetDashArray(width), 0) }; context.DrawLine(dashPen, new Point(0, 0), new Point(width, 0)); context.DrawLine(dashPen, new Point(0, height), new Point(width, height)); diff --git a/StabilityMatrix.Avalonia/Controls/ProgressRing.cs b/StabilityMatrix.Avalonia/Controls/ProgressRing.cs index f1a5b56c..cb09bc31 100644 --- a/StabilityMatrix.Avalonia/Controls/ProgressRing.cs +++ b/StabilityMatrix.Avalonia/Controls/ProgressRing.cs @@ -1,8 +1,10 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Diagnostics.CodeAnalysis; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; namespace StabilityMatrix.Avalonia.Controls; @@ -13,34 +15,11 @@ namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class ProgressRing : RangeBase { - public static readonly StyledProperty IsIndeterminateProperty = - ProgressBar.IsIndeterminateProperty.AddOwner(); + private Arc? fillArc; - public static readonly StyledProperty PreserveAspectProperty = - AvaloniaProperty.Register(nameof(PreserveAspect), true); - - public static readonly StyledProperty ValueAngleProperty = - AvaloniaProperty.Register(nameof(ValueAngle)); - - public static readonly StyledProperty StartAngleProperty = - AvaloniaProperty.Register(nameof(StartAngle)); - - public static readonly StyledProperty EndAngleProperty = - AvaloniaProperty.Register(nameof(EndAngle), 360); - - static ProgressRing() - { - MinimumProperty.Changed.AddClassHandler(OnMinimumPropertyChanged); - MaximumProperty.Changed.AddClassHandler(OnMaximumPropertyChanged); - ValueProperty.Changed.AddClassHandler(OnValuePropertyChanged); - MaximumProperty.Changed.AddClassHandler(OnStartAnglePropertyChanged); - MaximumProperty.Changed.AddClassHandler(OnEndAnglePropertyChanged); - } - - public ProgressRing() - { - UpdatePseudoClasses(IsIndeterminate, PreserveAspect); - } + public static readonly StyledProperty IsIndeterminateProperty = ProgressBar + .IsIndeterminateProperty + .AddOwner(); public bool IsIndeterminate { @@ -48,35 +27,90 @@ public class ProgressRing : RangeBase set => SetValue(IsIndeterminateProperty, value); } + public static readonly StyledProperty PreserveAspectProperty = AvaloniaProperty.Register( + nameof(PreserveAspect), + true + ); + public bool PreserveAspect { get => GetValue(PreserveAspectProperty); set => SetValue(PreserveAspectProperty, value); } - public double ValueAngle + public static readonly StyledProperty StrokeThicknessProperty = Shape + .StrokeThicknessProperty + .AddOwner(); + + public double StrokeThickness { - get => GetValue(ValueAngleProperty); - private set => SetValue(ValueAngleProperty, value); + get => GetValue(StrokeThicknessProperty); + set => SetValue(StrokeThicknessProperty, value); } + public static readonly StyledProperty StartAngleProperty = AvaloniaProperty.Register( + nameof(StartAngle) + ); + public double StartAngle { get => GetValue(StartAngleProperty); set => SetValue(StartAngleProperty, value); } + public static readonly StyledProperty SweepAngleProperty = AvaloniaProperty.Register( + nameof(SweepAngle) + ); + + public double SweepAngle + { + get => GetValue(SweepAngleProperty); + set => SetValue(SweepAngleProperty, value); + } + + public static readonly StyledProperty EndAngleProperty = AvaloniaProperty.Register( + nameof(EndAngle), + 360 + ); + public double EndAngle { get => GetValue(EndAngleProperty); set => SetValue(EndAngleProperty, value); } + static ProgressRing() + { + AffectsRender(SweepAngleProperty, StartAngleProperty, EndAngleProperty); + + ValueProperty.Changed.AddClassHandler(OnValuePropertyChanged); + SweepAngleProperty.Changed.AddClassHandler(OnSweepAnglePropertyChanged); + } + + public ProgressRing() + { + UpdatePseudoClasses(IsIndeterminate, PreserveAspect); + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + fillArc = e.NameScope.Find("PART_Fill"); + if (fillArc is not null) + { + fillArc.StartAngle = StartAngle; + fillArc.SweepAngle = SweepAngle; + } + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); var e = change as AvaloniaPropertyChangedEventArgs; - if (e is null) return; + if (e is null) + return; if (e.Property == IsIndeterminateProperty) { @@ -88,9 +122,7 @@ public class ProgressRing : RangeBase } } - private void UpdatePseudoClasses( - bool? isIndeterminate, - bool? preserveAspect) + private void UpdatePseudoClasses(bool? isIndeterminate, bool? preserveAspect) { if (isIndeterminate.HasValue) { @@ -103,28 +135,19 @@ public class ProgressRing : RangeBase } } - private static void OnMinimumPropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) - { - sender.Minimum = (double) e.NewValue!; - } - - private static void OnMaximumPropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) - { - sender.Maximum = (double) e.NewValue!; - } - private static void OnValuePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) { - sender.ValueAngle = ((double) e.NewValue! - sender.Minimum) * (sender.EndAngle - sender.StartAngle) / (sender.Maximum - sender.Minimum); - } - - private static void OnStartAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) - { - sender.StartAngle = (double) e.NewValue!; + sender.SweepAngle = + ((double)e.NewValue! - sender.Minimum) + * (sender.EndAngle - sender.StartAngle) + / (sender.Maximum - sender.Minimum); } - private static void OnEndAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) + private static void OnSweepAnglePropertyChanged(ProgressRing sender, AvaloniaPropertyChangedEventArgs e) { - sender.EndAngle = (double) e.NewValue!; + if (sender.fillArc is { } arc) + { + arc.SweepAngle = Math.Round(e.GetNewValue()); + } } } diff --git a/StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs b/StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs new file mode 100644 index 00000000..a03243fb --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/PropertyGrid/BetterPropertyGrid.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.PropertyGrid.Services; +using JetBrains.Annotations; +using PropertyModels.ComponentModel; +using StabilityMatrix.Core.Extensions; + +namespace StabilityMatrix.Avalonia.Controls; + +/// +[PublicAPI] +public class BetterPropertyGrid : global::Avalonia.PropertyGrid.Controls.PropertyGrid +{ + protected override Type StyleKeyOverride => typeof(global::Avalonia.PropertyGrid.Controls.PropertyGrid); + + public static readonly StyledProperty> ExcludedCategoriesProperty = AvaloniaProperty.Register< + BetterPropertyGrid, + IEnumerable + >("ExcludedCategories"); + + public IEnumerable ExcludedCategories + { + get => GetValue(ExcludedCategoriesProperty); + set => SetValue(ExcludedCategoriesProperty, value); + } + + public static readonly StyledProperty> IncludedCategoriesProperty = AvaloniaProperty.Register< + BetterPropertyGrid, + IEnumerable + >("IncludedCategories"); + + public IEnumerable IncludedCategories + { + get => GetValue(IncludedCategoriesProperty); + set => SetValue(IncludedCategoriesProperty, value); + } + + static BetterPropertyGrid() + { + // Register factories + CellEditFactoryService.Default.AddFactory(new ToggleSwitchCellEditFactory()); + + // Initialize localization and name resolver + LocalizationService.Default.AddExtraService(new PropertyGridLocalizationService()); + + ExcludedCategoriesProperty + .Changed + .AddClassHandler( + (grid, args) => + { + if (args.NewValue is IEnumerable excludedCategories) + { + grid.FilterExcludeCategories(excludedCategories); + } + } + ); + + IncludedCategoriesProperty + .Changed + .AddClassHandler( + (grid, args) => + { + if (args.NewValue is IEnumerable includedCategories) + { + grid.FilterIncludeCategories(includedCategories); + } + } + ); + } + + public void FilterExcludeCategories(IEnumerable excludedCategories) + { + // Get internal property `ViewModel` of internal type `PropertyGridViewModel` + var gridVm = this.GetProtectedProperty("ViewModel")!; + // Get public property `CategoryFilter` + var categoryFilter = gridVm.GetProtectedProperty("CategoryFilter")!; + + categoryFilter.BeginUpdate(); + + // Uncheck All, then check all except All + categoryFilter.UnCheck(categoryFilter.All); + + foreach (var mask in categoryFilter.Masks.Where(m => m != categoryFilter.All)) + { + categoryFilter.Check(mask); + } + + // Uncheck excluded categories + foreach (var mask in excludedCategories) + { + categoryFilter.UnCheck(mask); + } + + categoryFilter.EndUpdate(); + } + + public void FilterIncludeCategories(IEnumerable includeCategories) + { + // Get internal property `ViewModel` of internal type `PropertyGridViewModel` + var gridVm = this.GetProtectedProperty("ViewModel")!; + // Get public property `CategoryFilter` + var categoryFilter = gridVm.GetProtectedProperty("CategoryFilter")!; + + categoryFilter.BeginUpdate(); + + // Uncheck non-included categories + foreach (var mask in categoryFilter.Masks.Where(m => !includeCategories.Contains(m))) + { + categoryFilter.UnCheck(mask); + } + + categoryFilter.UnCheck(categoryFilter.All); + + // Check included categories + foreach (var mask in includeCategories) + { + categoryFilter.Check(mask); + } + + categoryFilter.EndUpdate(); + } +} diff --git a/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs b/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs new file mode 100644 index 00000000..d074dd06 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridCultureData.cs @@ -0,0 +1,35 @@ +using System; +using System.Globalization; +using PropertyModels.Localilzation; +using StabilityMatrix.Avalonia.Languages; + +namespace StabilityMatrix.Avalonia.Controls; + +internal class PropertyGridCultureData : ICultureData +{ + /// + public bool Reload() => false; + + /// + public CultureInfo Culture => Cultures.Current ?? Cultures.Default; + + /// + public Uri Path => new(""); + + /// + public string this[string key] + { + get + { + if (Resources.ResourceManager.GetString(key) is { } result) + { + return result; + } + + return key; + } + } + + /// + public bool IsLoaded => true; +} diff --git a/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs b/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs new file mode 100644 index 00000000..dfc27ade --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/PropertyGrid/PropertyGridLocalizationService.cs @@ -0,0 +1,36 @@ +using System; +using PropertyModels.ComponentModel; +using PropertyModels.Localilzation; +using StabilityMatrix.Avalonia.Languages; + +namespace StabilityMatrix.Avalonia.Controls; + +/// +/// Implements using static . +/// +internal class PropertyGridLocalizationService : MiniReactiveObject, ILocalizationService +{ + /// + public ICultureData CultureData { get; } = new PropertyGridCultureData(); + + /// + public string this[string key] => CultureData[key]; + + /// + public event EventHandler? OnCultureChanged; + + /// + public ILocalizationService[] GetExtraServices() => Array.Empty(); + + /// + public void AddExtraService(ILocalizationService service) { } + + /// + public void RemoveExtraService(ILocalizationService service) { } + + /// + public ICultureData[] GetCultures() => new[] { CultureData }; + + /// + public void SelectCulture(string cultureName) { } +} diff --git a/StabilityMatrix.Avalonia/Controls/PropertyGrid/ToggleSwitchCellEditFactory.cs b/StabilityMatrix.Avalonia/Controls/PropertyGrid/ToggleSwitchCellEditFactory.cs new file mode 100644 index 00000000..28232635 --- /dev/null +++ b/StabilityMatrix.Avalonia/Controls/PropertyGrid/ToggleSwitchCellEditFactory.cs @@ -0,0 +1,60 @@ +using Avalonia.Controls; +using Avalonia.PropertyGrid.Controls; +using Avalonia.PropertyGrid.Controls.Factories; +using Avalonia.PropertyGrid.Localization; + +namespace StabilityMatrix.Avalonia.Controls; + +internal class ToggleSwitchCellEditFactory : AbstractCellEditFactory +{ + // make this extend factor only effect on TestExtendPropertyGrid + public override bool Accept(object accessToken) + { + return accessToken is BetterPropertyGrid; + } + + public override Control? HandleNewProperty(PropertyCellContext context) + { + var propertyDescriptor = context.Property; + var target = context.Target; + + if (propertyDescriptor.PropertyType != typeof(bool)) + { + return null; + } + + var control = new ToggleSwitch(); + control.SetLocalizeBinding(ToggleSwitch.OnContentProperty, "On"); + control.SetLocalizeBinding(ToggleSwitch.OffContentProperty, "Off"); + + control.IsCheckedChanged += (s, e) => + { + SetAndRaise(context, control, control.IsChecked); + }; + + return control; + } + + public override bool HandlePropertyChanged(PropertyCellContext context) + { + var propertyDescriptor = context.Property; + var target = context.Target; + var control = context.CellEdit; + + if (propertyDescriptor.PropertyType != typeof(bool)) + { + return false; + } + + ValidateProperty(control, propertyDescriptor, target); + + if (control is ToggleSwitch ts) + { + ts.IsChecked = (bool)(propertyDescriptor.GetValue(target) ?? false); + + return true; + } + + return false; + } +} diff --git a/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml b/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml deleted file mode 100644 index b6dd0a2b..00000000 --- a/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - diff --git a/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml.cs b/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml.cs deleted file mode 100644 index 726cf414..00000000 --- a/StabilityMatrix.Avalonia/Controls/SelectImageCard.axaml.cs +++ /dev/null @@ -1,6 +0,0 @@ -using StabilityMatrix.Core.Attributes; - -namespace StabilityMatrix.Avalonia.Controls; - -[Transient] -public class SelectImageCard : DropTargetTemplatedControlBase { } diff --git a/StabilityMatrix.Avalonia/Controls/StackExpander.axaml b/StabilityMatrix.Avalonia/Controls/StackExpander.axaml deleted file mode 100644 index 9e7af392..00000000 --- a/StabilityMatrix.Avalonia/Controls/StackExpander.axaml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs b/StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs deleted file mode 100644 index 38208891..00000000 --- a/StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Avalonia; -using Avalonia.Controls.Primitives; -using StabilityMatrix.Core.Attributes; - -namespace StabilityMatrix.Avalonia.Controls; - -[Transient] -public class StackExpander : TemplatedControl -{ - public static readonly StyledProperty SpacingProperty = AvaloniaProperty.Register< - StackCard, - int - >("Spacing", 8); - - public int Spacing - { - get => GetValue(SpacingProperty); - set => SetValue(SpacingProperty, value); - } -} diff --git a/StabilityMatrix.Avalonia/Converters/CustomStringFormatConverter.cs b/StabilityMatrix.Avalonia/Converters/CustomStringFormatConverter.cs new file mode 100644 index 00000000..3eaf7090 --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/CustomStringFormatConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace StabilityMatrix.Avalonia.Converters; + +public class CustomStringFormatConverter([StringSyntax("CompositeFormat")] string format) : IValueConverter + where T : IFormatProvider, new() +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is null ? null : string.Format(new T(), format, value); + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is null ? null : throw new NotImplementedException(); + } +} diff --git a/StabilityMatrix.Avalonia/Converters/IndexPlusOneConverter.cs b/StabilityMatrix.Avalonia/Converters/IndexPlusOneConverter.cs new file mode 100644 index 00000000..086b1133 --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/IndexPlusOneConverter.cs @@ -0,0 +1,31 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace StabilityMatrix.Avalonia.Converters; + +/// +/// Converts an index to index + 1 +/// +public class IndexPlusOneConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is int i) + { + return i + 1; + } + + return value; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is int i) + { + return i - 1; + } + + return value; + } +} diff --git a/StabilityMatrix.Avalonia/Converters/MemoryBytesFormatter.cs b/StabilityMatrix.Avalonia/Converters/MemoryBytesFormatter.cs new file mode 100644 index 00000000..18dba05f --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/MemoryBytesFormatter.cs @@ -0,0 +1,41 @@ +using System; +using Size = StabilityMatrix.Core.Helper.Size; + +namespace StabilityMatrix.Avalonia.Converters; + +public class MemoryBytesFormatter : ICustomFormatter, IFormatProvider +{ + /// + public object? GetFormat(Type? formatType) + { + return formatType == typeof(ICustomFormatter) ? this : null; + } + + /// + public string Format(string? format, object? arg, IFormatProvider? formatProvider) + { + if (format == null || !format.Trim().StartsWith('M')) + { + if (arg is IFormattable formatArg) + { + return formatArg.ToString(format, formatProvider); + } + + return arg?.ToString() ?? string.Empty; + } + + var value = Convert.ToUInt64(arg); + + var result = format.Trim().EndsWith("10", StringComparison.OrdinalIgnoreCase) + ? Size.FormatBase10Bytes(value) + : Size.FormatBytes(value); + + // Strip i if not Mi + if (!format.Trim().Contains('I', StringComparison.OrdinalIgnoreCase)) + { + result = result.Replace("i", string.Empty, StringComparison.OrdinalIgnoreCase); + } + + return result; + } +} diff --git a/StabilityMatrix.Avalonia/Converters/NullableDefaultNumericConverter.cs b/StabilityMatrix.Avalonia/Converters/NullableDefaultNumericConverter.cs new file mode 100644 index 00000000..43379900 --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/NullableDefaultNumericConverter.cs @@ -0,0 +1,84 @@ +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using Avalonia.Data.Converters; + +namespace StabilityMatrix.Avalonia.Converters; + +/// +/// Converts a possibly boxed nullable value type to its default value +/// +public class NullableDefaultNumericConverter : IValueConverter + where TSource : unmanaged, INumber + where TTarget : unmanaged, INumber +{ + public ReturnBehavior NanHandling { get; set; } = ReturnBehavior.DefaultValue; + + /// + /// Unboxes a nullable value type + /// + private TSource Unbox(TTarget? value) + { + if (!value.HasValue) + { + return default; + } + + if (TTarget.IsNaN(value.Value)) + { + return NanHandling switch + { + ReturnBehavior.DefaultValue => default, + ReturnBehavior.Throw => throw new InvalidCastException("Cannot convert NaN to a numeric type"), + _ + => throw new InvalidEnumArgumentException( + nameof(NanHandling), + (int)NanHandling, + typeof(ReturnBehavior) + ) + }; + } + + return (TSource)System.Convert.ChangeType(value.Value, typeof(TSource)); + } + + /// + /// Convert a value type to a nullable value type + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (targetType != typeof(TTarget?) && !targetType.IsAssignableTo(typeof(TTarget))) + { + // ReSharper disable once LocalizableElement + throw new ArgumentException( + $"Convert Target type {targetType.Name} must be assignable to {typeof(TTarget).Name}" + ); + } + + return (TTarget?)System.Convert.ChangeType(value, typeof(TTarget)); + } + + /// + /// Convert a nullable value type to a value type + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (!targetType.IsAssignableTo(typeof(TSource))) + { + // ReSharper disable once LocalizableElement + throw new ArgumentException( + $"ConvertBack Target type {targetType.Name} must be assignable to {typeof(TSource).Name}" + ); + } + + return Unbox((TTarget?)value); + } + + public enum ReturnBehavior + { + DefaultValue, + Throw + } +} diff --git a/StabilityMatrix.Avalonia/Converters/NullableDefaultNumericConverters.cs b/StabilityMatrix.Avalonia/Converters/NullableDefaultNumericConverters.cs new file mode 100644 index 00000000..662b2601 --- /dev/null +++ b/StabilityMatrix.Avalonia/Converters/NullableDefaultNumericConverters.cs @@ -0,0 +1,6 @@ +namespace StabilityMatrix.Avalonia.Converters; + +public static class NullableDefaultNumericConverters +{ + public static readonly NullableDefaultNumericConverter IntToDecimal = new(); +} diff --git a/StabilityMatrix.Avalonia/Converters/StringFormatConverters.cs b/StabilityMatrix.Avalonia/Converters/StringFormatConverters.cs index e4ed2455..6402747f 100644 --- a/StabilityMatrix.Avalonia/Converters/StringFormatConverters.cs +++ b/StabilityMatrix.Avalonia/Converters/StringFormatConverters.cs @@ -1,4 +1,5 @@ -using Avalonia.Data.Converters; +using System; +using Avalonia.Data.Converters; namespace StabilityMatrix.Avalonia.Converters; @@ -7,4 +8,9 @@ public static class StringFormatConverters private static StringFormatValueConverter? _decimalConverter; public static StringFormatValueConverter Decimal => _decimalConverter ??= new StringFormatValueConverter("{0:D}", null); + + private static readonly Lazy MemoryBytesConverterLazy = + new(() => new CustomStringFormatConverter("{0:M}")); + + public static IValueConverter MemoryBytes => MemoryBytesConverterLazy.Value; } diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs index 87de1f7c..469ff676 100644 --- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs +++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Http; using System.Text; using AvaloniaEdit.Utils; +using CommunityToolkit.Mvvm.ComponentModel; using DynamicData; using DynamicData.Binding; using Microsoft.Extensions.DependencyInjection; @@ -21,11 +23,11 @@ using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser; using StabilityMatrix.Avalonia.ViewModels.CheckpointManager; using StabilityMatrix.Avalonia.ViewModels.Dialogs; -using StabilityMatrix.Avalonia.ViewModels.Progress; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.ViewModels.Inference.Modules; using StabilityMatrix.Avalonia.ViewModels.Inference.Video; using StabilityMatrix.Avalonia.ViewModels.OutputsPage; +using StabilityMatrix.Avalonia.ViewModels.Progress; using StabilityMatrix.Avalonia.ViewModels.Settings; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Database; @@ -43,6 +45,8 @@ using StabilityMatrix.Core.Models.Update; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Updater; +using CivitAiBrowserViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.CivitAiBrowserViewModel; +using HuggingFacePageViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.HuggingFacePageViewModel; namespace StabilityMatrix.Avalonia.DesignData; @@ -77,10 +81,7 @@ public static class DesignData Id = activePackageId, DisplayName = "My Installed Package", PackageName = "stable-diffusion-webui", - Version = new InstalledPackageVersion - { - InstalledReleaseVersion = "v1.0.0" - }, + Version = new InstalledPackageVersion { InstalledReleaseVersion = "v1.0.0" }, LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LastUpdateCheck = DateTimeOffset.Now }, @@ -92,8 +93,7 @@ public static class DesignData Version = new InstalledPackageVersion { InstalledBranch = "master", - InstalledCommitSha = - "abc12uwu345568972abaedf7g7e679a98879e879f87ga8" + InstalledCommitSha = "abc12uwu345568972abaedf7g7e679a98879e879f87ga8" }, LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LastUpdateCheck = DateTimeOffset.Now @@ -126,7 +126,8 @@ public static class DesignData .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); // Placeholder services that nobody should need during design time services @@ -149,6 +150,7 @@ public static class DesignData var modelFinder = Services.GetRequiredService(); var packageFactory = Services.GetRequiredService(); var notificationService = Services.GetRequiredService(); + var modelImportService = Services.GetRequiredService(); LaunchOptionsViewModel = Services.GetRequiredService(); LaunchOptionsViewModel.Cards = new[] @@ -185,7 +187,7 @@ public static class DesignData CheckpointsPageViewModel.CheckpointFoldersCache, new CheckpointFolder[] { - new(settingsManager, downloadService, modelFinder, notificationService) + new(settingsManager, downloadService, modelFinder, notificationService, modelImportService) { DirectoryPath = "Models/StableDiffusion", DisplayedCheckpointFiles = new ObservableCollectionExtended() @@ -212,14 +214,10 @@ public static class DesignData TrainedWords = ["aurora", "lightning"] } }, - new() - { - FilePath = "~/Models/Lora/model.safetensors", - Title = "Some model" - }, + new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" }, }, }, - new(settingsManager, downloadService, modelFinder, notificationService) + new(settingsManager, downloadService, modelFinder, notificationService, modelImportService) { Title = "Lora", DirectoryPath = "Packages/Lora", @@ -300,56 +298,55 @@ public static class DesignData ); }*/ - CheckpointBrowserViewModel.ModelCards = - new ObservableCollection + CivitAiBrowserViewModel.ModelCards = new ObservableCollection + { + dialogFactory.Get(vm => { - dialogFactory.Get(vm => + vm.CivitModel = new CivitModel { - vm.CivitModel = new CivitModel + Name = "BB95 Furry Mix", + Description = "A furry mix of BB95", + Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 }, + ModelVersions = [new() { Name = "v1.2.2-Inpainting" }], + Creator = new CivitCreator { - Name = "BB95 Furry Mix", - Description = "A furry mix of BB95", - Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 }, - ModelVersions = [ - new() { Name = "v1.2.2-Inpainting" } - ], - Creator = new CivitCreator - { - Image = "https://gravatar.com/avatar/fe74084ae8a081dc2283f5bde4736756ad?f=y&d=retro", - Username = "creator-1" - } - }; - }), - dialogFactory.Get(vm => + Image = "https://gravatar.com/avatar/fe74084ae8a081dc2283f5bde4736756ad?f=y&d=retro", + Username = "creator-1" + } + }; + }), + dialogFactory.Get(vm => + { + vm.CivitModel = new CivitModel { - vm.CivitModel = new CivitModel - { - Name = "Another Model", - Description = "A mix of example", - Stats = new CivitModelStats { Rating = 5, RatingCount = 3500 }, - ModelVersions = [ - new() + Name = "Another Model", + Description = "A mix of example", + Stats = new CivitModelStats { Rating = 5, RatingCount = 3500 }, + ModelVersions = + [ + new() + { + Name = "v1.2.2-Inpainting", + Images = new List { - Name = "v1.2.2-Inpainting", - Images = new List + new() { - new() - { - Nsfw = "None", - Url = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" - + "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg" - } + Nsfw = "None", + Url = + "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/" + + "78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg" } - } - ], - Creator = new CivitCreator - { - Image = "https://gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro", - Username = "creator-2" + } } - }; - }) - }; + ], + Creator = new CivitCreator + { + Image = "https://gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?f=y&d=retro", + Username = "creator-2" + } + }; + }) + }; NewCheckpointsPageViewModel.AllCheckpoints = new ObservableCollection { @@ -377,33 +374,30 @@ public static class DesignData new() { FilePath = "~/Models/Lora/model.safetensors", Title = "Some model" } }; - ProgressManagerViewModel.ProgressItems.AddRange( - new ProgressItemViewModelBase[] - { - new ProgressItemViewModel( - new ProgressItem( - Guid.NewGuid(), - "Test File.exe", - new ProgressReport(0.5f, "Downloading...") + ProgressManagerViewModel + .ProgressItems + .AddRange( + new ProgressItemViewModelBase[] + { + new ProgressItemViewModel( + new ProgressItem(Guid.NewGuid(), "Test File.exe", new ProgressReport(0.5f, "Downloading...")) + ), + new MockDownloadProgressItemViewModel( + "Very Long Test File Name Need Even More Longness Thanks That's pRobably good 2.exe" + ), + new PackageInstallProgressItemViewModel( + new PackageModificationRunner + { + CurrentProgress = new ProgressReport(0.5f, "Installing package...") + } ) - ), - new MockDownloadProgressItemViewModel( - "Very Long Test File Name Need Even More Longness Thanks That's pRobably good 2.exe" - ), - new PackageInstallProgressItemViewModel( - new PackageModificationRunner - { - CurrentProgress = new ProgressReport(0.5f, "Installing package...") - } - ) - } - ); + } + ); UpdateViewModel = Services.GetRequiredService(); UpdateViewModel.CurrentVersionText = "v2.0.0"; UpdateViewModel.NewVersionText = "v2.0.1"; - UpdateViewModel.ReleaseNotes = - "## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature"; + UpdateViewModel.ReleaseNotes = "## v2.0.1\n- Fixed a bug\n- Added a feature\n- Removed a feature"; isInitialized = true; } @@ -420,14 +414,15 @@ public static class DesignData public static ServiceManager DialogFactory => Services.GetRequiredService>(); - public static MainWindowViewModel MainWindowViewModel => - Services.GetRequiredService(); + public static MainWindowViewModel MainWindowViewModel => Services.GetRequiredService(); public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel => Services.GetRequiredService(); - public static LaunchPageViewModel LaunchPageViewModel => - Services.GetRequiredService(); + public static LaunchPageViewModel LaunchPageViewModel => Services.GetRequiredService(); + + public static HuggingFacePageViewModel HuggingFacePageViewModel => + Services.GetRequiredService(); public static OutputsPageViewModel OutputsPageViewModel { @@ -458,10 +453,7 @@ public static class DesignData vm.SetPackages(settings.Settings.InstalledPackages); vm.SetUnknownPackages( - new InstalledPackage[] - { - UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"), - } + new InstalledPackage[] { UnknownInstalledPackage.FromDirectoryName("sd-unknown-with-long-name"), } ); vm.PackageCards[0].IsUpdateAvailable = true; @@ -476,14 +468,12 @@ public static class DesignData public static NewCheckpointsPageViewModel NewCheckpointsPageViewModel => Services.GetRequiredService(); - public static SettingsViewModel SettingsViewModel => - Services.GetRequiredService(); + public static SettingsViewModel SettingsViewModel => Services.GetRequiredService(); public static InferenceSettingsViewModel InferenceSettingsViewModel => Services.GetRequiredService(); - public static MainSettingsViewModel MainSettingsViewModel => - Services.GetRequiredService(); + public static MainSettingsViewModel MainSettingsViewModel => Services.GetRequiredService(); public static AccountSettingsViewModel AccountSettingsViewModel => Services.GetRequiredService(); @@ -503,7 +493,7 @@ public static class DesignData HashBlake3 = "", Signature = "", }; - + vm.UpdateStatus = new UpdateStatusChangedEventArgs { LatestUpdate = update, @@ -519,6 +509,9 @@ public static class DesignData } } + public static CivitAiBrowserViewModel CivitAiBrowserViewModel => + Services.GetRequiredService(); + public static CheckpointBrowserViewModel CheckpointBrowserViewModel => Services.GetRequiredService(); @@ -564,10 +557,7 @@ The gallery images are often inpainted, but you will get something very similar } } }; - var sampleViewModel = new ModelVersionViewModel( - new HashSet { "ABCD" }, - sampleCivitVersions[0] - ); + var sampleViewModel = new ModelVersionViewModel(new HashSet { "ABCD" }, sampleCivitVersions[0]); // Sample data for dialogs vm.Versions = new List { sampleViewModel }; @@ -579,8 +569,7 @@ The gallery images are often inpainted, but you will get something very similar public static OneClickInstallViewModel OneClickInstallViewModel => Services.GetRequiredService(); - public static InferenceViewModel InferenceViewModel => - Services.GetRequiredService(); + public static InferenceViewModel InferenceViewModel => Services.GetRequiredService(); public static SelectDataDirectoryViewModel SelectDataDirectoryViewModel => Services.GetRequiredService(); @@ -618,14 +607,10 @@ The gallery images are often inpainted, but you will get something very similar public static PythonPackagesViewModel PythonPackagesViewModel => DialogFactory.Get(vm => { - vm.AddPackages( - new PipPackageInfo("pip", "1.0.0"), - new PipPackageInfo("torch", "2.1.0+cu121") - ); + vm.AddPackages(new PipPackageInfo("pip", "1.0.0"), new PipPackageInfo("torch", "2.1.0+cu121")); }); - public static LykosLoginViewModel LykosLoginViewModel => - DialogFactory.Get(); + public static LykosLoginViewModel LykosLoginViewModel => DialogFactory.Get(); public static OAuthConnectViewModel OAuthConnectViewModel => DialogFactory.Get(vm => @@ -645,7 +630,7 @@ The gallery images are often inpainted, but you will get something very similar vm.OutputProgress.Maximum = 30; vm.OutputProgress.Text = "Sampler 10/30"; }); - + public static InferenceImageToVideoViewModel InferenceImageToVideoViewModel => DialogFactory.Get(vm => { @@ -654,14 +639,26 @@ The gallery images are often inpainted, but you will get something very similar vm.OutputProgress.Text = "Sampler 10/30"; }); + public static InferenceImageToImageViewModel InferenceImageToImageViewModel => + DialogFactory.Get(); + public static InferenceImageUpscaleViewModel InferenceImageUpscaleViewModel => DialogFactory.Get(); - public static PackageImportViewModel PackageImportViewModel => - DialogFactory.Get(); + public static PackageImportViewModel PackageImportViewModel => DialogFactory.Get(); - public static RefreshBadgeViewModel RefreshBadgeViewModel => - new() { State = ProgressState.Success }; + public static RefreshBadgeViewModel RefreshBadgeViewModel => new() { State = ProgressState.Success }; + + public static PropertyGridViewModel PropertyGridViewModel => + DialogFactory.Get(vm => + { + vm.SelectedObject = new INotifyPropertyChanged[] + { + new MockPropertyGridObject(), + new MockPropertyGridObjectAlt() + }; + vm.ExcludeCategories = ["Excluded Category"]; + }); public static SeedCardViewModel SeedCardViewModel => new(); public static SvdImgToVidConditioningViewModel SvdImgToVidConditioningViewModel => new(); @@ -723,8 +720,7 @@ The gallery images are often inpainted, but you will get something very similar ); }); - public static ImageFolderCardViewModel ImageFolderCardViewModel => - DialogFactory.Get(); + public static ImageFolderCardViewModel ImageFolderCardViewModel => DialogFactory.Get(); public static FreeUCardViewModel FreeUCardViewModel => DialogFactory.Get(); @@ -755,7 +751,7 @@ The gallery images are often inpainted, but you will get something very similar public static StackEditableCardViewModel StackEditableCardViewModel => DialogFactory.Get(vm => { - vm.AddCards(StackExpanderViewModel, StackExpanderViewModel); + vm.AddCards(StackExpanderViewModel, StackExpanderViewModel2); }); public static StackExpanderViewModel StackExpanderViewModel => @@ -766,11 +762,18 @@ The gallery images are often inpainted, but you will get something very similar vm.OnContainerIndexChanged(0); }); - public static UpscalerCardViewModel UpscalerCardViewModel => - DialogFactory.Get(); + public static StackExpanderViewModel StackExpanderViewModel2 => + DialogFactory.Get(vm => + { + vm.Title = "Hires Fix"; + vm.IsSettingsEnabled = true; + vm.AddCards(UpscalerCardViewModel, SamplerCardViewModel); + vm.OnContainerIndexChanged(1); + }); + + public static UpscalerCardViewModel UpscalerCardViewModel => DialogFactory.Get(); - public static BatchSizeCardViewModel BatchSizeCardViewModel => - DialogFactory.Get(); + public static BatchSizeCardViewModel BatchSizeCardViewModel => DialogFactory.Get(); public static BatchSizeCardViewModel BatchSizeCardViewModelWithIndexOption => DialogFactory.Get(vm => @@ -822,14 +825,12 @@ The gallery images are often inpainted, but you will get something very similar vm.Resource = ComfyUpscaler.DefaultDownloadableModels[0].DownloadableResource!.Value; }); - public static SharpenCardViewModel SharpenCardViewModel => - DialogFactory.Get(); + public static SharpenCardViewModel SharpenCardViewModel => DialogFactory.Get(); public static InferenceConnectionHelpViewModel InferenceConnectionHelpViewModel => DialogFactory.Get(); - public static SelectImageCardViewModel SelectImageCardViewModel => - DialogFactory.Get(); + public static SelectImageCardViewModel SelectImageCardViewModel => DialogFactory.Get(); public static SelectImageCardViewModel SelectImageCardViewModel_WithImage => DialogFactory.Get(vm => @@ -842,15 +843,13 @@ The gallery images are often inpainted, but you will get something very similar }); public static ImageSource SampleImageSource => - new( - new Uri( - "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500" - ) - ); + new(new Uri("https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/a318ac1f-3ad0-48ac-98cc-79126febcc17/width=1500")) + { + Label = "Test Image" + }; + + public static ControlNetCardViewModel ControlNetCardViewModel => DialogFactory.Get(); - public static ControlNetCardViewModel ControlNetCardViewModel => - DialogFactory.Get(); - public static Indexer Types { get; } = new(); public class Indexer @@ -859,9 +858,7 @@ The gallery images are often inpainted, but you will get something very similar { get { - var type = - Type.GetType(typeName) - ?? throw new ArgumentException($"Type {typeName} not found"); + var type = Type.GetType(typeName) ?? throw new ArgumentException($"Type {typeName} not found"); try { return Services.GetService(type); diff --git a/StabilityMatrix.Avalonia/DesignData/MockMetadataImportService.cs b/StabilityMatrix.Avalonia/DesignData/MockMetadataImportService.cs new file mode 100644 index 00000000..d0172d34 --- /dev/null +++ b/StabilityMatrix.Avalonia/DesignData/MockMetadataImportService.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.Progress; +using StabilityMatrix.Core.Services; + +namespace StabilityMatrix.Avalonia.DesignData; + +public class MockMetadataImportService : IMetadataImportService +{ + public Task ScanDirectoryForMissingInfo(DirectoryPath directory, IProgress? progress = null) + { + return Task.CompletedTask; + } + + public Task GetMetadataForFile( + FilePath filePath, + IProgress? progress = null, + bool forceReimport = false + ) + { + return null; + } + + public Task UpdateExistingMetadata(DirectoryPath directory, IProgress? progress = null) + { + return Task.CompletedTask; + } +} diff --git a/StabilityMatrix.Avalonia/DesignData/MockPropertyGridObject.cs b/StabilityMatrix.Avalonia/DesignData/MockPropertyGridObject.cs new file mode 100644 index 00000000..0facd0c2 --- /dev/null +++ b/StabilityMatrix.Avalonia/DesignData/MockPropertyGridObject.cs @@ -0,0 +1,54 @@ +using System.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; +using PropertyModels.ComponentModel; +using StabilityMatrix.Avalonia.Languages; + +#pragma warning disable CS0657 // Not a valid attribute location for this declaration + +namespace StabilityMatrix.Avalonia.DesignData; + +public partial class MockPropertyGridObject : ObservableObject +{ + [ObservableProperty] + private string? stringProperty; + + [ObservableProperty] + private int intProperty; + + [ObservableProperty] + [property: Trackable(0, 50, Increment = 1, FormatString = "{0:0}")] + private int intRange = 10; + + [ObservableProperty] + [property: Trackable(0d, 1d, Increment = 0.01, FormatString = "{0:P0}")] + private double floatPercentRange = 0.25; + + [ObservableProperty] + [property: DisplayName("Int Custom Name")] + private int intCustomNameProperty = 42; + + [ObservableProperty] + [property: DisplayName(nameof(Resources.Label_Language))] + private int? intLocalizedNameProperty; + + [ObservableProperty] + private bool boolProperty; + + [ObservableProperty] + [property: Category("Included Category")] + private string? stringIncludedCategoryProperty; + + [ObservableProperty] + [property: Category("Excluded Category")] + private string? stringExcludedCategoryProperty; +} + +public partial class MockPropertyGridObjectAlt : ObservableObject +{ + [ObservableProperty] + private int altIntProperty = 10; + + [ObservableProperty] + [property: Category("Settings")] + private string? altStringProperty; +} diff --git a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs index 8a67ad00..16097f80 100644 --- a/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs +++ b/StabilityMatrix.Avalonia/Extensions/ComfyNodeBuilderExtensions.cs @@ -1,6 +1,8 @@ using System; using System.ComponentModel.DataAnnotations; using System.Drawing; +using System.IO; +using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.Inference; using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Core.Models.Api.Comfy.Nodes; @@ -18,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); @@ -34,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", @@ -48,38 +53,38 @@ public static class ComfyNodeBuilderExtensions } } - public static void SetupImageLatentSource( + /// + /// Setup an image as the connection + /// + public static void SetupImagePrimarySource( this ComfyNodeBuilder builder, - BatchSizeCardViewModel batchSizeCardViewModel, - SamplerCardViewModel samplerCardViewModel + ImageSource image, + Size imageSize, + int? batchIndex = null ) { - var emptyLatent = builder.Nodes.AddTypedNode( - new ComfyNodeBuilder.EmptyLatentImage - { - Name = "EmptyLatentImage", - BatchSize = batchSizeCardViewModel.BatchSize, - Height = samplerCardViewModel.Height, - Width = samplerCardViewModel.Width - } - ); + // Get source image + var sourceImageRelativePath = Path.Combine("Inference", image.GetHashGuidFileNameCached()); - builder.Connections.Primary = emptyLatent.Output; - builder.Connections.PrimarySize = new Size( - samplerCardViewModel.Width, - samplerCardViewModel.Height - ); + // Load source + var loadImage = builder + .Nodes + .AddTypedNode(new ComfyNodeBuilder.LoadImage { Name = "LoadImage", Image = sourceImageRelativePath }); + + builder.Connections.Primary = loadImage.Output1; + builder.Connections.PrimarySize = imageSize; // If batch index is selected, add a LatentFromBatch - if (batchSizeCardViewModel.IsBatchIndexEnabled) + if (batchIndex is not null) { - builder.Connections.Primary = builder.Nodes + builder.Connections.Primary = builder + .Nodes .AddNamedNode( ComfyNodeBuilder.LatentFromBatch( "LatentFromBatch", builder.GetPrimaryAsLatent(), // remote expects a 0-based index, vm is 1-based - batchSizeCardViewModel.BatchIndex - 1, + batchIndex.Value - 1, 1 ) ) @@ -92,20 +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.RefinerVAE + ?? builder.Connections.BaseVAE + ?? throw new ArgumentException("No Primary, Refiner, or Base VAE") + ), + image => image + ); - var previewImage = builder.Nodes.AddTypedNode( - new ComfyNodeBuilder.PreviewImage { Name = "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/Extensions/DataObjectExtensions.cs b/StabilityMatrix.Avalonia/Extensions/DataObjectExtensions.cs new file mode 100644 index 00000000..ef6a47f0 --- /dev/null +++ b/StabilityMatrix.Avalonia/Extensions/DataObjectExtensions.cs @@ -0,0 +1,19 @@ +using Avalonia.Input; + +namespace StabilityMatrix.Avalonia.Extensions; + +public static class DataObjectExtensions +{ + /// + /// Get Context from IDataObject, set by Xaml Behaviors + /// + public static T? GetContext(this IDataObject dataObject) + { + if (dataObject.Get("Context") is T context) + { + return context; + } + + return default; + } +} diff --git a/StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs b/StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs index 83a744eb..fe2f7e94 100644 --- a/StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs +++ b/StabilityMatrix.Avalonia/Helpers/TextEditorConfigs.cs @@ -84,6 +84,8 @@ public static class TextEditorConfigs textMate.SetGrammar(scope); textMate.SetTheme(registryOptions.LoadTheme(ThemeName.DarkPlus)); + + editor.Options.ShowBoxForControlCharacters = false; } private static IRawTheme GetThemeFromStream(Stream stream) diff --git a/StabilityMatrix.Avalonia/Helpers/UriHandler.cs b/StabilityMatrix.Avalonia/Helpers/UriHandler.cs index e500a1e4..a7b9dc1c 100644 --- a/StabilityMatrix.Avalonia/Helpers/UriHandler.cs +++ b/StabilityMatrix.Avalonia/Helpers/UriHandler.cs @@ -56,17 +56,25 @@ public class UriHandler Environment.Exit(0); } - public void Callback() { } - public void RegisterUriScheme() { if (Compat.IsWindows) { RegisterUriSchemeWin(); } - else + else if (Compat.IsLinux) { - RegisterUriSchemeUnix(); + // Try to register on unix but ignore errors + // Library does not support some distros + try + { + RegisterUriSchemeUnix(); + } + catch (Exception e) + { + Debug.WriteLine(e); + Console.WriteLine(e); + } } } @@ -92,11 +100,7 @@ public class UriHandler private void RegisterUriSchemeUnix() { - var service = URISchemeServiceFactory.GetURISchemeSerivce( - Scheme, - Description, - Compat.AppCurrentPath.FullPath - ); + var service = URISchemeServiceFactory.GetURISchemeSerivce(Scheme, Description, Compat.AppCurrentPath.FullPath); service.Set(); } } diff --git a/StabilityMatrix.Avalonia/Helpers/ViewModelSerializer.cs b/StabilityMatrix.Avalonia/Helpers/ViewModelSerializer.cs index 5039d082..238c67d7 100644 --- a/StabilityMatrix.Avalonia/Helpers/ViewModelSerializer.cs +++ b/StabilityMatrix.Avalonia/Helpers/ViewModelSerializer.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Text.Json.Serialization; @@ -9,16 +8,12 @@ namespace StabilityMatrix.Avalonia.Helpers; public static class ViewModelSerializer { - public static IImmutableDictionary GetDerivedTypes(Type baseType) + public static Dictionary GetDerivedTypes(Type baseType) { - return GetJsonDerivedTypeAttributes(baseType) - .ToImmutableDictionary(x => x.typeDiscriminator, x => x.subType); + return GetJsonDerivedTypeAttributes(baseType).ToDictionary(x => x.typeDiscriminator, x => x.subType); } - public static IEnumerable<( - Type subType, - string typeDiscriminator - )> GetJsonDerivedTypeAttributes(Type type) + public static IEnumerable<(Type subType, string typeDiscriminator)> GetJsonDerivedTypeAttributes(Type type) { return type.GetCustomAttributes() .Select(x => (x.DerivedType, x.TypeDiscriminator as string ?? x.DerivedType.Name)); diff --git a/StabilityMatrix.Avalonia/Languages/Cultures.cs b/StabilityMatrix.Avalonia/Languages/Cultures.cs index 77fb993a..019aee8d 100644 --- a/StabilityMatrix.Avalonia/Languages/Cultures.cs +++ b/StabilityMatrix.Avalonia/Languages/Cultures.cs @@ -13,10 +13,7 @@ public static class Cultures public static CultureInfo? Current => Resources.Culture; - public static readonly Dictionary SupportedCulturesByCode = new Dictionary< - string, - CultureInfo - > + public static readonly Dictionary SupportedCulturesByCode = new Dictionary { ["en-US"] = Default, ["ja-JP"] = new("ja-JP"), @@ -25,18 +22,15 @@ public static class Cultures ["it-IT"] = new("it-IT"), ["fr-FR"] = new("fr-FR"), ["es"] = new("es"), - ["ru-RU"] = new("ru-RU") + ["ru-RU"] = new("ru-RU"), + ["tr-TR"] = new("tr-TR") }; - public static IReadOnlyList SupportedCultures => - SupportedCulturesByCode.Values.ToImmutableList(); + public static IReadOnlyList SupportedCultures => SupportedCulturesByCode.Values.ToImmutableList(); public static CultureInfo GetSupportedCultureOrDefault(string? cultureCode) { - if ( - cultureCode is null - || !SupportedCulturesByCode.TryGetValue(cultureCode, out var culture) - ) + if (cultureCode is null || !SupportedCulturesByCode.TryGetValue(cultureCode, out var culture)) { return Default; } @@ -54,10 +48,7 @@ public static class Cultures public static bool TrySetSupportedCulture(string? cultureCode) { - if ( - cultureCode is null - || !SupportedCulturesByCode.TryGetValue(cultureCode, out var culture) - ) + if (cultureCode is null || !SupportedCulturesByCode.TryGetValue(cultureCode, out var culture)) { return false; } diff --git a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs index 526de91f..4347bb9b 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs +++ b/StabilityMatrix.Avalonia/Languages/Resources.Designer.cs @@ -338,6 +338,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Open on Hugging Face. + /// + public static string Action_OpenOnHuggingFace { + get { + return ResourceManager.GetString("Action_OpenOnHuggingFace", resourceCulture); + } + } + /// /// Looks up a localized string similar to Open Project.... /// @@ -419,6 +428,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Replace Contents. + /// + public static string Action_ReplaceContents { + get { + return ResourceManager.GetString("Action_ReplaceContents", resourceCulture); + } + } + /// /// Looks up a localized string similar to Restart. /// @@ -491,6 +509,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Select File. + /// + public static string Action_SelectFile { + get { + return ResourceManager.GetString("Action_SelectFile", resourceCulture); + } + } + /// /// Looks up a localized string similar to Send. /// @@ -563,6 +590,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Update Existing Metadata. + /// + public static string Action_UpdateExistingMetadata { + get { + return ResourceManager.GetString("Action_UpdateExistingMetadata", resourceCulture); + } + } + /// /// Looks up a localized string similar to Upgrade. /// @@ -599,6 +635,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Addons. + /// + public static string Label_Addons { + get { + return ResourceManager.GetString("Label_Addons", resourceCulture); + } + } + /// /// Looks up a localized string similar to Add Stability Matrix to the Start Menu. /// @@ -662,6 +707,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Auto Completion. + /// + public static string Label_AutoCompletion { + get { + return ResourceManager.GetString("Label_AutoCompletion", resourceCulture); + } + } + /// /// Looks up a localized string similar to Automatically scroll to end of console output. /// @@ -761,6 +815,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to CivitAI. + /// + public static string Label_CivitAi { + get { + return ResourceManager.GetString("Label_CivitAi", resourceCulture); + } + } + /// /// Looks up a localized string similar to You must be logged in to download this checkpoint. Please enter a CivitAI API Key in the settings.. /// @@ -797,6 +860,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Replace underscores with spaces when inserting completions. + /// + public static string Label_CompletionReplaceUnderscoresWithSpaces { + get { + return ResourceManager.GetString("Label_CompletionReplaceUnderscoresWithSpaces", resourceCulture); + } + } + /// /// Looks up a localized string similar to Confirm Delete. /// @@ -1023,7 +1095,7 @@ namespace StabilityMatrix.Avalonia.Languages { } /// - /// Looks up a localized string similar to Emebeddings / Textual Inversion. + /// Looks up a localized string similar to Embeddings / Textual Inversion. /// public static string Label_EmbeddingsOrTextualInversion { get { @@ -1112,6 +1184,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to General. + /// + public static string Label_General { + get { + return ResourceManager.GetString("Label_General", resourceCulture); + } + } + /// /// Looks up a localized string similar to Height. /// @@ -1121,6 +1202,24 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Holiday Mode. + /// + public static string Label_HolidayMode { + get { + return ResourceManager.GetString("Label_HolidayMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hugging Face. + /// + public static string Label_HuggingFace { + get { + return ResourceManager.GetString("Label_HuggingFace", resourceCulture); + } + } + /// /// Looks up a localized string similar to Image to Image. /// @@ -1130,6 +1229,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Image Viewer. + /// + public static string Label_ImageViewer { + get { + return ResourceManager.GetString("Label_ImageViewer", resourceCulture); + } + } + /// /// Looks up a localized string similar to Import with Metadata. /// @@ -1166,6 +1274,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Inference. + /// + public static string Label_Inference { + get { + return ResourceManager.GetString("Label_Inference", resourceCulture); + } + } + /// /// Looks up a localized string similar to Inner exception. /// @@ -1310,6 +1427,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Missing Image File. + /// + public static string Label_MissingImageFile { + get { + return ResourceManager.GetString("Label_MissingImageFile", resourceCulture); + } + } + /// /// Looks up a localized string similar to Model. /// @@ -1436,6 +1562,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Output Image Files. + /// + public static string Label_OutputImageFiles { + get { + return ResourceManager.GetString("Label_OutputImageFiles", resourceCulture); + } + } + /// /// Looks up a localized string similar to Output Browser. /// @@ -1562,6 +1697,42 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Prompt. + /// + public static string Label_Prompt { + get { + return ResourceManager.GetString("Label_Prompt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prompt Tags. + /// + public static string Label_PromptTags { + get { + return ResourceManager.GetString("Label_PromptTags", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tags file to use for suggesting completions (Supports the a1111-sd-webui-tagcomplete .csv format). + /// + public static string Label_PromptTagsDescription { + get { + return ResourceManager.GetString("Label_PromptTagsDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Import Prompt tags. + /// + public static string Label_PromptTagsImport { + get { + return ResourceManager.GetString("Label_PromptTagsImport", resourceCulture); + } + } + /// /// Looks up a localized string similar to Python Packages. /// @@ -1670,6 +1841,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Save Intermediate Image. + /// + public static string Label_SaveIntermediateImage { + get { + return ResourceManager.GetString("Label_SaveIntermediateImage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Search.... /// @@ -1697,6 +1877,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Settings. + /// + public static string Label_Settings { + get { + return ResourceManager.GetString("Label_Settings", resourceCulture); + } + } + /// /// Looks up a localized string similar to Shared Model Folder Strategy. /// @@ -1814,6 +2003,15 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to System Information. + /// + public static string Label_SystemInformation { + get { + return ResourceManager.GetString("Label_SystemInformation", resourceCulture); + } + } + /// /// Looks up a localized string similar to Text to Image. /// @@ -1976,6 +2174,24 @@ namespace StabilityMatrix.Avalonia.Languages { } } + /// + /// Looks up a localized string similar to Not yet available. + /// + public static string Label_WipFeature { + get { + return ResourceManager.GetString("Label_WipFeature", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Feature will be available in a future update. + /// + public static string Label_WipFeatureDescription { + get { + return ResourceManager.GetString("Label_WipFeatureDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to You're up to date. /// diff --git a/StabilityMatrix.Avalonia/Languages/Resources.es.resx b/StabilityMatrix.Avalonia/Languages/Resources.es.resx index 3b586761..46da9160 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.es.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.es.resx @@ -680,4 +680,7 @@ Restablecer Diseño Predeterminado + + CivitAI + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx b/StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx index 8623c810..574d80e7 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx @@ -678,4 +678,7 @@ Rétablir la présentation par défaut + + CivitAI + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.it-it.resx b/StabilityMatrix.Avalonia/Languages/Resources.it-it.resx index cadda8ef..dd3bcc3b 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.it-it.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.it-it.resx @@ -680,4 +680,7 @@ + + CivitAI + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx index 63d37d58..4c5c7495 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx @@ -680,4 +680,7 @@ レイアウトを初期状態に戻す + + CivitAI + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.resx b/StabilityMatrix.Avalonia/Languages/Resources.resx index e6bc3a7c..93cdca80 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.resx @@ -175,7 +175,7 @@ Deemphasis - Emebeddings / Textual Inversion + Embeddings / Textual Inversion Networks (Lora / LyCORIS) @@ -825,4 +825,76 @@ Additional folders such as IPAdapters and TextualInversions (embeddings) can be enabled here + + Open on Hugging Face + + + Update Existing Metadata + + + GeneralA general settings category + + + InferenceThe Inference feature page + + + PromptA settings category for Inference generation prompts + + + Output Image Files + + + Image Viewer + + + Auto Completion + + + Replace underscores with spaces when inserting completions + + + Prompt TagsTags for image generation prompts + + + Import Prompt tags + + + Tags file to use for suggesting completions (Supports the a1111-sd-webui-tagcomplete .csv format) + + + System Information + + + CivitAI + + + Hugging Face + + + AddonsInference Sampler Addons + + + Save Intermediate ImageInference module step to save an intermediate image + + + Settings + + + Select File + + + Replace Contents + + + Not yet available + + + Feature will be available in a future update + + + Missing Image File + + + Holiday Mode + diff --git a/StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx b/StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx index 149c0a0b..24716a61 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx @@ -678,4 +678,7 @@ Восстановить вид по умолчанию + + CivitAI + \ No newline at end of file diff --git a/StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx b/StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx new file mode 100644 index 00000000..0726efc0 --- /dev/null +++ b/StabilityMatrix.Avalonia/Languages/Resources.tr-TR.resx @@ -0,0 +1,783 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Başlat + + + Çıkış + + + Kaydet + + + İptal + + + Dil + + + Yeni dil seçeneğinin etkili olması için yeniden başlatma gerekiyor + + + Yeniden başlat + + + Daha Sonra Yeniden Başlat + + + Yeniden Başlatma Gerekli + + + Bilinmeyen Paket + + + İçe Aktar + + + Paket Türü + + + Sürüm + + + Sürüm Türü + + + Sürümler + + + Dallar + + + İçe aktarmak için chekpoints'leri buraya sürükleyip bırakın + + + Vurgu + + + Vurguyu Kaldırma + + + Emebeddings / Textual Inversion + + + Networks (Lora / LyCORIS) + + + Yorumlar + + + Yüksek yakınlaştırma seviyesinde piksel ızgarasını göster + + + Adımlar + + + Adımlar - Temel + + + Adımlar - İyileştirici + + + CFG Ölçeği + + + Gürültü Azaltma Gücü + + + Genişlik + + + Yükseklik + + + İyileştirici + + + VAE + + + Model + + + Bağlan + + + Bağlanıyor... + + + Kapat + + + Bağlanmayı bekliyor... + + + Güncelleme Mevcut + + + Sponsor Ol + + + Discord Sunucusuna Katıl + + + İndirmeler + + + Yükle + + + İlk kurulum işlemini atla + + + Beklenmeyen bir hata oluştu + + + Uygulamadan Çık + + + Görünen Ad + + + Bu ada sahip bir yükleme zaten mevcut. + + + Lütfen farklı bir ad seçin veya başka bir yükleme konumu seçin. + + + Gelişmiş Seçenekler + + + İşlemek + + + Paylaşılan Model Klasör Stratejisi + + + PyTorch Sürümü + + + Bittiğinde iletişim kutusunu kapat + + + Veri Klasörü + + + Model chekpoints, Lora'ların, web UI'lerin, ayarların vb. kurulacağı yer burasıdır. + + + FAT32 veya exFAT sürücü kullanırken hatalarla karşılaşabilirsiniz. Daha sorunsuz bir deneyim için farklı bir sürücü seçin. + + + Taşınabilir Mod + + + Taşınabilir Modda tüm veriler ve ayarlar uygulamayla aynı dizinde saklanacaktır. Uygulamayı 'Veri' klasörüyle birlikte farklı bir konuma veya bilgisayara taşıyabileceksiniz. + + + Devam + + + Önceki Resim + + + Sonraki Resim + + + Model Açıklaması + + + Stabilite Matrisi için yeni bir sürüm mevcut! + + + En Yeniyi İçe Aktar - + + + Tüm Sürümler + + + Modelleri, #etiketleri veya @kullanıcıları ara + + + Ara + + + Sırala + + + Süre + + + Model Türü + + + Temel Model + + + NSFW İçerik Göster + + + CivitAI tarafından sağlanan veriler + + + Sayfa + + + İlk Sayfa + + + Önceki Sayfa + + + Sonraki Sayfa + + + Son Sayfa + + + Yeniden Adlandır + + + Sil + + + CivitAI'de Aç + + + Bağlı Model + + + Yerel Model + + + Explorer'da göster + + + Yeni + + + Klasör + + + İçe aktarma için dosyayı buraya bırakın + + + Metadata ile içeri aktar + + + Yeni yerel içe aktarmalar için bağlı meta veri arayın + + + İndeksleniyor... + + + Model Klasörü + + + Kategoriler + + + Başlayalım + + + Lisans Sözleşmesini Okudum ve Kabul Ediyorum. + + + Lisans Sözleşmesi. + + + Metadata'larını bul + + + Model Resimlerini Göster + + + Görünüm + + + Tema + + + Checkpoint Yöneticisi + + + Kapatma sırasında paylaşılan Checkpoint dizini sembolik bağlantılarını kaldırın + + + Stabilite Matrisini başka bir sürücüye taşımada sorun yaşıyorsanız bu seçeneği seçin + + + Checkpoints Önbelleğini Sıfırla + + + Kurulu checkpoints önbelleğini yeniden oluştur. Model tarayıcısında checkpointler yanlış etiketlenmişse kullanın + + + Paket Ortamı + + + Düzenle + + + Ortam Değişkenleri + + + Gömülü Python + + + Sürümü Kontrol Et + + + Entegrasyonlar + + + Discord Zengin Varlık + + + Sistem + + + Stability Matrix'i başlat menüsüne ekle + + + Mevcut uygulama konumu kullanılır, uygulamayı taşırsanız tekrar çalıştırabilirsiniz + + + Yalnızca Windows'ta kullanılabilir + + + Geçerli Kullanıcı için Ekle + + + Tüm Kullanıcılar İçin Ekle + + + Yeni Veri Dizini Seç + + + Mevcut veriyi taşımaz + + + Dizin Seçin + + + Hakkında + + + Stability Matrix + + + Lisans ve Açık Kaynak Bildirimleri + + + Başlamak için Başlat'a tıklayın! + + + Dur + + + Giriş Gönder + + + Giriş + + + Gönder + + + Giriş gerekiyor + + + Onaylamak? + + + Evet + + + Hayır + + + Web Arayüzünü Aç + + + Stability Matrix'e Hoş Geldiniz! + + + Tercih ettiğiniz arayüzü seçin ve başlamak için Yükle'ye tıklayın + + + Yükleniyor + + + Başlatma sayfasına geçiliyor + + + Paket indiriliyor... + + + İndirme tamamlandı + + + Kurulum tamamlandı + + + Önkoşullar kuruluyor... + + + Paket gereksinimleri kuruluyor... + + + Explorer'da Aç + + + Finder'da Aç + + + Kaldır + + + Güncellemeleri Kontrol Et + + + Güncelle + + + Paket Ekle + + + Başlamak için bir paket ekle! + + + Ad + + + Değer + + + Kaldır + + + Ayrıntılar + + + Çağrı Yığını + + + İç istisna + + + Ara... + + + Tamam + + + Tekrar Dene + + + Python Sürüm Bilgisi + + + Yeniden Başlat + + + Silmeyi Onayla + + + Bu, paket klasörünü ve içindeki tüm içeriği, eklediğiniz tüm oluşturulmuş resimleri ve dosyaları silecek. + + + Paket Kaldırılıyor... + + + Paket Kaldırıldı + + + Bazı dosyalar silinemedi. Lütfen paket klasöründeki açık dosyaları kapatın ve tekrar deneyin. + + + Geçersiz Paket Türü + + + {0} Güncelleniyor + + + Güncelleme tamamlandı + + + {0}, en son sürüme güncellendi + + + {0} güncelleme hatası + + + Güncelleme başarısız oldu + + + Tarayıcıda Aç + + + Paket kurulum hatası + + + Dal + + + Konsol çıktısının sonuna otomatik olarak kaydır + + + Lisans + + + Model Paylaşımı + + + Lütfen Bir Veri Klasörü Seçin + + + Veri Klasörü Adı + + + Geçerli klasör: + + + Uygulama güncelleme sonrası yeniden başlatılacaktır + + + Daha Sonra Hatırlat + + + Şimdi Yükle + + + Sürüm Notları + + + Projeyi Aç... + + + Farklı Kaydet... + + + Varsayılan Düzeni Geri Yükle + + + Çıktı Paylaşımı + + + Toplu İndeks + + + Kopyala + + + Resim Görüntüleyici'de aç + + + {0} resim seçildi + + + Çıkış Klasörü + + + Çıkış Türü + + + Seçimi Temizle + + + Tümünü Seç + + + Çıkarıma Gönder + + + Metinden Resime + + + Resimden Resime + + + Inpainting + + + Upscale + + + Çıkış Tarayıcısı + + + 1 resim seçildi + + + Python Paketleri + + + Birleştir + + + Emin misiniz? + + + Bu, seçilen paketlerden tüm oluşturulmuş resimleri, paylaşılan çıktılar klasörünün Konsolide dizinine taşıyacak. Bu işlem geri alınamaz. + + + Yenile + + + Yükselt + + + Düşür + + + GitHub'da Aç + + + Bağlandı + + + Bağlantıyı Kes + + + E-posta + + + Kullanıcı Adı + + + Parola + + + Giriş + + + Kaydol + + + Parolayı Onayla + + + API Key + + + Hesaplar + + diff --git a/StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx b/StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx index 99c38fb7..46957dca 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx @@ -647,4 +647,7 @@ 分支 + + CivitAI + diff --git a/StabilityMatrix.Avalonia/Languages/Resources.zh-Hant.resx b/StabilityMatrix.Avalonia/Languages/Resources.zh-Hant.resx index 49df915d..1ea14f2f 100644 --- a/StabilityMatrix.Avalonia/Languages/Resources.zh-Hant.resx +++ b/StabilityMatrix.Avalonia/Languages/Resources.zh-Hant.resx @@ -639,4 +639,7 @@ 分支 + + CivitAI + diff --git a/StabilityMatrix.Avalonia/MarkupExtensions/ShowDisabledTooltipExtension.cs b/StabilityMatrix.Avalonia/MarkupExtensions/ShowDisabledTooltipExtension.cs new file mode 100644 index 00000000..36ce253c --- /dev/null +++ b/StabilityMatrix.Avalonia/MarkupExtensions/ShowDisabledTooltipExtension.cs @@ -0,0 +1,113 @@ +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; +using JetBrains.Annotations; + +namespace StabilityMatrix.Avalonia.MarkupExtensions; + +/// +/// Show tooltip on Controls with IsEffectivelyEnabled = false +/// https://github.com/AvaloniaUI/Avalonia/issues/3847#issuecomment-1618790059 +/// +[PublicAPI] +public static class ShowDisabledTooltipExtension +{ + static ShowDisabledTooltipExtension() + { + ShowOnDisabledProperty.Changed.AddClassHandler(HandleShowOnDisabledChanged); + } + + public static bool GetShowOnDisabled(AvaloniaObject obj) + { + return obj.GetValue(ShowOnDisabledProperty); + } + + public static void SetShowOnDisabled(AvaloniaObject obj, bool value) + { + obj.SetValue(ShowOnDisabledProperty, value); + } + + public static readonly AttachedProperty ShowOnDisabledProperty = AvaloniaProperty.RegisterAttached< + object, + Control, + bool + >("ShowOnDisabled"); + + private static void HandleShowOnDisabledChanged(Control control, AvaloniaPropertyChangedEventArgs e) + { + if (e.GetNewValue()) + { + control.DetachedFromVisualTree += AttachedControl_DetachedFromVisualOrExtension; + control.AttachedToVisualTree += AttachedControl_AttachedToVisualTree; + if (control.IsInitialized) + { + // enabled after visual attached + AttachedControl_AttachedToVisualTree(control, null!); + } + } + else + { + AttachedControl_DetachedFromVisualOrExtension(control, null!); + } + } + + private static void AttachedControl_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + if (sender is not Control control || TopLevel.GetTopLevel(control) is not { } tl) + { + return; + } + // NOTE pointermove needed to be tunneled for me but you may not need to... + tl.AddHandler(InputElement.PointerMovedEvent, TopLevel_PointerMoved, RoutingStrategies.Tunnel); + } + + private static void AttachedControl_DetachedFromVisualOrExtension(object? s, VisualTreeAttachmentEventArgs e) + { + if (s is not Control control) + { + return; + } + control.DetachedFromVisualTree -= AttachedControl_DetachedFromVisualOrExtension; + control.AttachedToVisualTree -= AttachedControl_AttachedToVisualTree; + if (TopLevel.GetTopLevel(control) is not { } tl) + { + return; + } + tl.RemoveHandler(InputElement.PointerMovedEvent, TopLevel_PointerMoved); + } + + private static void TopLevel_PointerMoved(object? sender, PointerEventArgs e) + { + if (sender is not Control tl) + { + return; + } + + var attachedControls = tl.GetVisualDescendants().Where(GetShowOnDisabled).Cast().ToList(); + + // find disabled children under pointer w/ this extension enabled + var disabledChildUnderPointer = attachedControls.FirstOrDefault( + x => + x.Bounds.Contains(e.GetPosition(x.Parent as Visual)) + && x is { IsEffectivelyVisible: true, IsEffectivelyEnabled: false } + ); + + if (disabledChildUnderPointer != null) + { + // manually show tooltip + ToolTip.SetIsOpen(disabledChildUnderPointer, true); + } + + var disabledTooltipsToHide = attachedControls.Where( + x => ToolTip.GetIsOpen(x) && x != disabledChildUnderPointer && !x.IsEffectivelyEnabled + ); + + foreach (var control in disabledTooltipsToHide) + { + ToolTip.SetIsOpen(control, false); + } + } +} diff --git a/StabilityMatrix.Avalonia/MarkupExtensions/TernaryExtension.cs b/StabilityMatrix.Avalonia/MarkupExtensions/TernaryExtension.cs new file mode 100644 index 00000000..2654cccc --- /dev/null +++ b/StabilityMatrix.Avalonia/MarkupExtensions/TernaryExtension.cs @@ -0,0 +1,42 @@ +using System; +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.MarkupExtensions; + +namespace StabilityMatrix.Avalonia.MarkupExtensions; + +/// +/// https://github.com/AvaloniaUI/Avalonia/discussions/7408 +/// +/// +/// {e:Ternary SomeProperty, True=1, False=0} +/// +public class TernaryExtension : MarkupExtension +{ + public string Path { get; set; } + + public Type Type { get; set; } + + public object? True { get; set; } + + public object? False { get; set; } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + var cultureInfo = CultureInfo.GetCultureInfo("en-US"); + var binding = new ReflectionBindingExtension(Path) + { + Mode = BindingMode.OneWay, + Converter = new FuncValueConverter( + isTrue => + isTrue + ? Convert.ChangeType(True, Type, cultureInfo.NumberFormat) + : Convert.ChangeType(False, Type, cultureInfo.NumberFormat) + ) + }; + + return binding.ProvideValue(serviceProvider); + } +} diff --git a/StabilityMatrix.Avalonia/Models/AppArgs.cs b/StabilityMatrix.Avalonia/Models/AppArgs.cs index f1624103..b1e72bf9 100644 --- a/StabilityMatrix.Avalonia/Models/AppArgs.cs +++ b/StabilityMatrix.Avalonia/Models/AppArgs.cs @@ -57,6 +57,12 @@ public class AppArgs [Option("disable-gpu-rendering", HelpText = "Disable hardware acceleration / GPU rendering")] public bool DisableGpuRendering { get; set; } + /// + /// Flag to use OpenGL rendering + /// + [Option("opengl", HelpText = "Prefer OpenGL rendering")] + public bool UseOpenGlRendering { get; set; } + /// /// Override global app home directory /// Defaults to (%APPDATA%|~/.config)/StabilityMatrix diff --git a/StabilityMatrix.Avalonia/Models/HuggingFace/HuggingFaceModelType.cs b/StabilityMatrix.Avalonia/Models/HuggingFace/HuggingFaceModelType.cs new file mode 100644 index 00000000..e9237b50 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/HuggingFace/HuggingFaceModelType.cs @@ -0,0 +1,46 @@ +using System.Text.Json.Serialization; +using StabilityMatrix.Core.Converters.Json; +using StabilityMatrix.Core.Extensions; +using StabilityMatrix.Core.Models; + +namespace StabilityMatrix.Avalonia.Models.HuggingFace; + +[JsonConverter(typeof(DefaultUnknownEnumConverter))] +public enum HuggingFaceModelType +{ + [Description("Base Models")] + [ConvertTo(SharedFolderType.StableDiffusion)] + BaseModel, + + [Description("ControlNets")] + [ConvertTo(SharedFolderType.ControlNet)] + ControlNet, + + [Description("ControlNets (Diffusers)")] + [ConvertTo(SharedFolderType.ControlNet)] + DiffusersControlNet, + + [Description("IP Adapters")] + [ConvertTo(SharedFolderType.IpAdapter)] + IpAdapter, + + [Description("IP Adapters (Diffusers SD1.5)")] + [ConvertTo(SharedFolderType.InvokeIpAdapters15)] + DiffusersIpAdapter, + + [Description("IP Adapters (Diffusers SDXL)")] + [ConvertTo(SharedFolderType.InvokeIpAdaptersXl)] + DiffusersIpAdapterXl, + + [Description("CLIP Vision (Diffusers)")] + [ConvertTo(SharedFolderType.InvokeClipVision)] + DiffusersClipVision, + + [Description("T2I Adapters")] + [ConvertTo(SharedFolderType.T2IAdapter)] + T2IAdapter, + + [Description("T2I Adapters (Diffusers)")] + [ConvertTo(SharedFolderType.T2IAdapter)] + DiffusersT2IAdapter, +} diff --git a/StabilityMatrix.Avalonia/Models/HuggingFace/HuggingfaceItem.cs b/StabilityMatrix.Avalonia/Models/HuggingFace/HuggingfaceItem.cs new file mode 100644 index 00000000..95e02905 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/HuggingFace/HuggingfaceItem.cs @@ -0,0 +1,12 @@ +namespace StabilityMatrix.Avalonia.Models.HuggingFace; + +public class HuggingfaceItem +{ + public required HuggingFaceModelType ModelCategory { get; set; } + public required string ModelName { get; set; } + public required string RepositoryPath { get; set; } + public required string[] Files { get; set; } + public required string LicenseType { get; set; } + public string? LicensePath { get; set; } + public string? Subfolder { get; set; } +} diff --git a/StabilityMatrix.Avalonia/Models/ImageSource.cs b/StabilityMatrix.Avalonia/Models/ImageSource.cs index d5a33b87..4a8fffb7 100644 --- a/StabilityMatrix.Avalonia/Models/ImageSource.cs +++ b/StabilityMatrix.Avalonia/Models/ImageSource.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics; using System.IO; +using System.Text.Json.Serialization; using System.Threading.Tasks; using AsyncImageLoader; using Avalonia.Media.Imaging; @@ -27,8 +29,17 @@ public record ImageSource : IDisposable /// /// Bitmap /// + [JsonIgnore] public Bitmap? Bitmap { get; set; } + /// + /// Optional label for the image + /// + public string? Label { get; set; } + + [JsonConstructor] + public ImageSource() { } + public ImageSource(FilePath localFile) { LocalFile = localFile; @@ -44,6 +55,7 @@ public record ImageSource : IDisposable Bitmap = bitmap; } + [JsonIgnore] public Task BitmapAsync => GetBitmapAsync(); /// @@ -116,9 +128,20 @@ public record ImageSource : IDisposable throw new InvalidOperationException("ImageSource is not a local file"); } + // Calculate hash if not available if (contentHashBlake3 is null) { - throw new InvalidOperationException("Blake3 hash has not been calculated yet"); + // File must exist + if (!LocalFile.Exists) + { + throw new FileNotFoundException("Image file does not exist", LocalFile); + } + + // Fail in debug since hash should have been pre-calculated + Debug.Fail("Hash has not been calculated when GetHashGuidFileNameCached() was called"); + + var data = LocalFile.ReadAllBytes(); + contentHashBlake3 = FileHash.GetBlake3Parallel(data); } var extension = LocalFile.Info.Extension; diff --git a/StabilityMatrix.Avalonia/Models/Inference/IInputImageProvider.cs b/StabilityMatrix.Avalonia/Models/Inference/IInputImageProvider.cs new file mode 100644 index 00000000..a343a7f7 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/Inference/IInputImageProvider.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace StabilityMatrix.Avalonia.Models.Inference; + +public interface IInputImageProvider +{ + IEnumerable GetInputImages(); +} diff --git a/StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs b/StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs index 0e23ee17..f90baa4b 100644 --- a/StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs +++ b/StabilityMatrix.Avalonia/Models/Inference/ModuleApplyStepEventArgs.cs @@ -40,7 +40,7 @@ public class ModuleApplyStepEventArgs : EventArgs public ( ConditioningNodeConnection Positive, ConditioningNodeConnection Negative - ) Conditioning { get; set; } + )? Conditioning { get; set; } /// /// Temporary refiner conditioning apply step, used by samplers to apply control net. @@ -48,7 +48,7 @@ public class ModuleApplyStepEventArgs : EventArgs public ( ConditioningNodeConnection Positive, ConditioningNodeConnection Negative - ) RefinerConditioning { get; set; } + )? RefinerConditioning { get; set; } /// /// Temporary model apply step, used by samplers to apply control net. diff --git a/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs b/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs index bc998d5c..3cc0355e 100644 --- a/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs +++ b/StabilityMatrix.Avalonia/Models/InferenceProjectDocument.cs @@ -30,13 +30,10 @@ public class InferenceProjectDocument : ICloneable { ProjectType = loadableModel switch { + InferenceImageToImageViewModel => InferenceProjectType.ImageToImage, InferenceTextToImageViewModel => InferenceProjectType.TextToImage, InferenceImageUpscaleViewModel => InferenceProjectType.Upscale, InferenceImageToVideoViewModel => InferenceProjectType.ImageToVideo, - _ - => throw new InvalidOperationException( - $"Unknown loadable model type: {loadableModel.GetType()}" - ) }, State = loadableModel.SaveStateToJsonObject() }; @@ -119,8 +116,7 @@ public class InferenceProjectDocument : ICloneable var document = (InferenceProjectDocument)Clone(); var batchSizeCard = - document.State!["BatchSize"] - ?? throw new InvalidOperationException("BatchSize card is null"); + document.State!["BatchSize"] ?? throw new InvalidOperationException("BatchSize card is null"); batchSizeCard["BatchSize"] = batchSize; batchSizeCard["BatchCount"] = batchCount; @@ -133,8 +129,7 @@ public class InferenceProjectDocument : ICloneable { var newObj = (InferenceProjectDocument)MemberwiseClone(); // Clone State also since its mutable - newObj.State = - State == null ? null : JsonSerializer.SerializeToNode(State).Deserialize(); + newObj.State = State == null ? null : JsonSerializer.SerializeToNode(State).Deserialize(); return newObj; } } diff --git a/StabilityMatrix.Avalonia/Models/InferenceProjectType.cs b/StabilityMatrix.Avalonia/Models/InferenceProjectType.cs index 9d640e89..1b4bb017 100644 --- a/StabilityMatrix.Avalonia/Models/InferenceProjectType.cs +++ b/StabilityMatrix.Avalonia/Models/InferenceProjectType.cs @@ -20,7 +20,7 @@ public static class InferenceProjectTypeExtensions return type switch { InferenceProjectType.TextToImage => typeof(InferenceTextToImageViewModel), - InferenceProjectType.ImageToImage => null, + InferenceProjectType.ImageToImage => typeof(InferenceImageToImageViewModel), InferenceProjectType.Inpainting => null, InferenceProjectType.Upscale => typeof(InferenceImageUpscaleViewModel), InferenceProjectType.ImageToVideo => typeof(InferenceImageToVideoViewModel), diff --git a/StabilityMatrix.Avalonia/Program.cs b/StabilityMatrix.Avalonia/Program.cs index 616b6346..35777167 100644 --- a/StabilityMatrix.Avalonia/Program.cs +++ b/StabilityMatrix.Avalonia/Program.cs @@ -24,6 +24,7 @@ using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Updater; namespace StabilityMatrix.Avalonia; @@ -53,7 +54,8 @@ public static class Program SetDebugBuild(); - var parseResult = Parser.Default + var parseResult = Parser + .Default .ParseArguments(args) .WithNotParsed(errors => { @@ -68,6 +70,7 @@ public static class Program if (Args.HomeDirectoryOverride is { } homeDir) { Compat.SetAppDataHome(homeDir); + GlobalConfig.HomeDir = homeDir; } // Launched for custom URI scheme, handle and exit @@ -77,11 +80,7 @@ public static class Program { if ( Uri.TryCreate(uriArg, UriKind.Absolute, out var uri) - && string.Equals( - uri.Scheme, - UriHandler.Scheme, - StringComparison.OrdinalIgnoreCase - ) + && string.Equals(uri.Scheme, UriHandler.Scheme, StringComparison.OrdinalIgnoreCase) ) { UriHandler.SendAndExit(uri); @@ -145,22 +144,19 @@ public static class Program var app = AppBuilder.Configure().UsePlatformDetect().WithInterFont().LogToTrace(); - if (Args.DisableGpuRendering) + if (Args.UseOpenGlRendering) { app = app.With( - new Win32PlatformOptions - { - RenderingMode = new[] { Win32RenderingMode.Software } - } - ) - .With( - new X11PlatformOptions { RenderingMode = new[] { X11RenderingMode.Software } } - ) + new Win32PlatformOptions { RenderingMode = [Win32RenderingMode.Wgl, Win32RenderingMode.Software] } + ); + } + + if (Args.DisableGpuRendering) + { + app = app.With(new Win32PlatformOptions { RenderingMode = new[] { Win32RenderingMode.Software } }) + .With(new X11PlatformOptions { RenderingMode = new[] { X11RenderingMode.Software } }) .With( - new AvaloniaNativePlatformOptions - { - RenderingMode = new[] { AvaloniaNativeRenderingMode.Software } - } + new AvaloniaNativePlatformOptions { RenderingMode = new[] { AvaloniaNativeRenderingMode.Software } } ); } @@ -257,10 +253,7 @@ public static class Program try { var process = Process.GetProcessById(pid); - process - .WaitForExitAsync(new CancellationTokenSource(timeout).Token) - .GetAwaiter() - .GetResult(); + process.WaitForExitAsync(new CancellationTokenSource(timeout).Token).GetAwaiter().GetResult(); } catch (OperationCanceledException) { @@ -281,8 +274,7 @@ public static class Program { SentrySdk.Init(o => { - o.Dsn = - "https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328"; + o.Dsn = "https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328"; o.StackTraceMode = StackTraceMode.Enhanced; o.TracesSampleRate = 1.0; o.IsGlobalModeEnabled = true; @@ -309,10 +301,7 @@ public static class Program }); } - private static void TaskScheduler_UnobservedTaskException( - object? sender, - UnobservedTaskExceptionEventArgs e - ) + private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) { if (e.Exception is Exception ex) { @@ -320,10 +309,7 @@ public static class Program } } - private static void CurrentDomain_UnhandledException( - object sender, - UnhandledExceptionEventArgs e - ) + private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { if (e.ExceptionObject is not Exception ex) return; @@ -340,15 +326,9 @@ public static class Program Logger.Fatal(ex, "Unhandled {Type}: {Message}", ex.GetType().Name, ex.Message); } - if ( - Application.Current?.ApplicationLifetime - is IClassicDesktopStyleApplicationLifetime lifetime - ) + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) { - var dialog = new ExceptionDialog - { - DataContext = new ExceptionViewModel { Exception = ex } - }; + var dialog = new ExceptionDialog { DataContext = new ExceptionViewModel { Exception = ex } }; var mainWindow = lifetime.MainWindow; // We can only show dialog if main window exists, and is visible diff --git a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs index 2928b37f..995a93bb 100644 --- a/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs +++ b/StabilityMatrix.Avalonia/Services/InferenceClientManager.cs @@ -56,8 +56,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient private readonly SourceCache modelsSource = new(p => p.GetId()); - public IObservableCollection Models { get; } = - new ObservableCollectionExtended(); + public IObservableCollection Models { get; } = new ObservableCollectionExtended(); private readonly SourceCache vaeModelsSource = new(p => p.GetId()); @@ -66,29 +65,24 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient public IObservableCollection VaeModels { get; } = new ObservableCollectionExtended(); - private readonly SourceCache controlNetModelsSource = - new(p => p.GetId()); + private readonly SourceCache controlNetModelsSource = new(p => p.GetId()); - private readonly SourceCache downloadableControlNetModelsSource = - new(p => p.GetId()); + private readonly SourceCache downloadableControlNetModelsSource = new(p => p.GetId()); public IObservableCollection ControlNetModels { get; } = new ObservableCollectionExtended(); private readonly SourceCache samplersSource = new(p => p.Name); - public IObservableCollection Samplers { get; } = - new ObservableCollectionExtended(); + public IObservableCollection Samplers { get; } = new ObservableCollectionExtended(); private readonly SourceCache modelUpscalersSource = new(p => p.Name); private readonly SourceCache latentUpscalersSource = new(p => p.Name); - private readonly SourceCache downloadableUpscalersSource = - new(p => p.Name); + private readonly SourceCache downloadableUpscalersSource = new(p => p.Name); - public IObservableCollection Upscalers { get; } = - new ObservableCollectionExtended(); + public IObservableCollection Upscalers { get; } = new ObservableCollectionExtended(); private readonly SourceCache schedulersSource = new(p => p.Name); @@ -111,11 +105,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient modelsSource .Connect() - .SortBy( - f => f.ShortDisplayName, - SortDirection.Ascending, - SortOptimisations.ComparesImmutableValuesOnly - ) + .SortBy(f => f.ShortDisplayName, SortDirection.Ascending, SortOptimisations.ComparesImmutableValuesOnly) .DeferUntilLoaded() .Bind(Models) .Subscribe(); @@ -124,9 +114,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient .Connect() .Or(downloadableControlNetModelsSource.Connect()) .Sort( - SortExpressionComparer - .Ascending(f => f.Type) - .ThenByAscending(f => f.ShortDisplayName) + SortExpressionComparer.Ascending(f => f.Type).ThenByAscending(f => f.ShortDisplayName) ) .DeferUntilLoaded() .Bind(ControlNetModels) @@ -142,11 +130,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient .Connect() .Or(modelUpscalersSource.Connect()) .Or(downloadableUpscalersSource.Connect()) - .Sort( - SortExpressionComparer - .Ascending(f => f.Type) - .ThenByAscending(f => f.Name) - ) + .Sort(SortExpressionComparer.Ascending(f => f.Type).ThenByAscending(f => f.Name)) .Bind(Upscalers) .Subscribe(); @@ -169,9 +153,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient if (IsConnected) { LoadSharedPropertiesAsync() - .SafeFireAndForget( - onException: ex => logger.LogError(ex, "Error loading shared properties") - ); + .SafeFireAndForget(onException: ex => logger.LogError(ex, "Error loading shared properties")); } }; } @@ -190,17 +172,11 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient // Get model names if (await Client.GetModelNamesAsync() is { } modelNames) { - modelsSource.EditDiff( - modelNames.Select(HybridModelFile.FromRemote), - HybridModelFile.Comparer - ); + modelsSource.EditDiff(modelNames.Select(HybridModelFile.FromRemote), HybridModelFile.Comparer); } // Get control net model names - if ( - await Client.GetNodeOptionNamesAsync("ControlNetLoader", "control_net_name") is - { } controlNetModelNames - ) + if (await Client.GetNodeOptionNamesAsync("ControlNetLoader", "control_net_name") is { } controlNetModelNames) { controlNetModelsSource.EditDiff( controlNetModelNames.Select(HybridModelFile.FromRemote), @@ -211,19 +187,13 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient // Fetch sampler names from KSampler node if (await Client.GetSamplerNamesAsync() is { } samplerNames) { - samplersSource.EditDiff( - samplerNames.Select(name => new ComfySampler(name)), - ComfySampler.Comparer - ); + samplersSource.EditDiff(samplerNames.Select(name => new ComfySampler(name)), ComfySampler.Comparer); } // Upscalers is latent and esrgan combined // Add latent upscale methods from LatentUpscale node - if ( - await Client.GetNodeOptionNamesAsync("LatentUpscale", "upscale_method") is - { } latentUpscalerNames - ) + if (await Client.GetNodeOptionNamesAsync("LatentUpscale", "upscale_method") is { } latentUpscalerNames) { latentUpscalersSource.EditDiff( latentUpscalerNames.Select(s => new ComfyUpscaler(s, ComfyUpscalerType.Latent)), @@ -234,10 +204,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient } // Add Model upscale methods - if ( - await Client.GetNodeOptionNamesAsync("UpscaleModelLoader", "model_name") is - { } modelUpscalerNames - ) + if (await Client.GetNodeOptionNamesAsync("UpscaleModelLoader", "model_name") is { } modelUpscalerNames) { modelUpscalersSource.EditDiff( modelUpscalerNames.Select(s => new ComfyUpscaler(s, ComfyUpscalerType.ESRGAN)), @@ -249,10 +216,12 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient // Add scheduler names from Scheduler node if (await Client.GetNodeOptionNamesAsync("KSampler", "scheduler") is { } schedulerNames) { - schedulersSource.EditDiff( - schedulerNames.Select(s => new ComfyScheduler(s)), - ComfyScheduler.Comparer - ); + schedulersSource.Edit(updater => + { + updater.AddOrUpdate( + schedulerNames.Where(n => !schedulersSource.Keys.Contains(n)).Select(s => new ComfyScheduler(s)) + ); + }); logger.LogTrace("Loaded scheduler methods: {@Schedulers}", schedulerNames); } } @@ -264,34 +233,25 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient { // Load local models modelsSource.EditDiff( - modelIndexService - .GetFromModelIndex(SharedFolderType.StableDiffusion) - .Select(HybridModelFile.FromLocal), + modelIndexService.GetFromModelIndex(SharedFolderType.StableDiffusion).Select(HybridModelFile.FromLocal), HybridModelFile.Comparer ); // Load local control net models controlNetModelsSource.EditDiff( - modelIndexService - .GetFromModelIndex(SharedFolderType.ControlNet) - .Select(HybridModelFile.FromLocal), + modelIndexService.GetFromModelIndex(SharedFolderType.ControlNet).Select(HybridModelFile.FromLocal), HybridModelFile.Comparer ); // Downloadable ControlNet models - var downloadableControlNets = RemoteModels.ControlNetModels.Where( - u => !modelUpscalersSource.Lookup(u.GetId()).HasValue - ); - downloadableControlNetModelsSource.EditDiff( - downloadableControlNets, - HybridModelFile.Comparer - ); + var downloadableControlNets = RemoteModels + .ControlNetModels + .Where(u => !modelUpscalersSource.Lookup(u.GetId()).HasValue); + downloadableControlNetModelsSource.EditDiff(downloadableControlNets, HybridModelFile.Comparer); // Load local VAE models vaeModelsSource.EditDiff( - modelIndexService - .GetFromModelIndex(SharedFolderType.VAE) - .Select(HybridModelFile.FromLocal), + modelIndexService.GetFromModelIndex(SharedFolderType.VAE).Select(HybridModelFile.FromLocal), HybridModelFile.Comparer ); @@ -304,25 +264,20 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient // Load Upscalers modelUpscalersSource.EditDiff( modelIndexService - .GetFromModelIndex( - SharedFolderType.ESRGAN | SharedFolderType.RealESRGAN | SharedFolderType.SwinIR - ) + .GetFromModelIndex(SharedFolderType.ESRGAN | SharedFolderType.RealESRGAN | SharedFolderType.SwinIR) .Select(m => new ComfyUpscaler(m.FileName, ComfyUpscalerType.ESRGAN)), ComfyUpscaler.Comparer ); // Remote upscalers - var remoteUpscalers = ComfyUpscaler.DefaultDownloadableModels.Where( - u => !modelUpscalersSource.Lookup(u.Name).HasValue - ); + var remoteUpscalers = ComfyUpscaler + .DefaultDownloadableModels + .Where(u => !modelUpscalersSource.Lookup(u.Name).HasValue); downloadableUpscalersSource.EditDiff(remoteUpscalers, ComfyUpscaler.Comparer); } /// - public async Task UploadInputImageAsync( - ImageSource image, - CancellationToken cancellationToken = default - ) + public async Task UploadInputImageAsync(ImageSource image, CancellationToken cancellationToken = default) { EnsureConnected(); @@ -338,10 +293,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient } /// - public async Task CopyImageToInputAsync( - FilePath imageFile, - CancellationToken cancellationToken = default - ) + public async Task CopyImageToInputAsync(FilePath imageFile, CancellationToken cancellationToken = default) { if (!IsConnected) return; @@ -370,10 +322,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient } /// - public async Task WriteImageToInputAsync( - ImageSource imageSource, - CancellationToken cancellationToken = default - ) + public async Task WriteImageToInputAsync(ImageSource imageSource, CancellationToken cancellationToken = default) { if (!IsConnected) return; @@ -436,16 +385,14 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient // For locally installed packages only // Delete ./output/Inference - var legacyInferenceLinkDir = new DirectoryPath( - packagePair.InstalledPackage.FullPath - ).JoinDir("output", "Inference"); + var legacyInferenceLinkDir = new DirectoryPath(packagePair.InstalledPackage.FullPath).JoinDir( + "output", + "Inference" + ); if (legacyInferenceLinkDir.Exists) { - logger.LogInformation( - "Deleting legacy inference link at {LegacyDir}", - legacyInferenceLinkDir - ); + logger.LogInformation("Deleting legacy inference link at {LegacyDir}", legacyInferenceLinkDir); if (legacyInferenceLinkDir.IsSymbolicLink) { @@ -462,10 +409,7 @@ public partial class InferenceClientManager : ObservableObject, IInferenceClient } /// - public async Task ConnectAsync( - PackagePair packagePair, - CancellationToken cancellationToken = default - ) + public async Task ConnectAsync(PackagePair packagePair, CancellationToken cancellationToken = default) { if (IsConnected) return; diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj index f5a6066b..5da35214 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 @@ -8,12 +9,19 @@ app.manifest true ./Assets/Icon.ico - 2.7.0-dev.4 + 2.7.0-pre.999 $(Version) true true + + + StabilityMatrix.URL + stabilitymatrix;stabilitymatrix:// + + + @@ -32,12 +40,15 @@ + + + @@ -79,6 +90,8 @@ + + diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings index 8ee7adac..78669f79 100644 --- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings +++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj.DotSettings @@ -1,4 +1,3 @@ - - UI + + UI + True diff --git a/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml b/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml index ca95cb8d..b0af1750 100644 --- a/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml +++ b/StabilityMatrix.Avalonia/Styles/ButtonStyles.axaml @@ -377,8 +377,8 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs index df465d31..9b2f245b 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs @@ -1,9 +1,4 @@ -using System.Diagnostics; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Markup.Xaml; -using StabilityMatrix.Avalonia.Controls; -using StabilityMatrix.Avalonia.ViewModels; +using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Core.Attributes; namespace StabilityMatrix.Avalonia.Views; @@ -15,26 +10,4 @@ public partial class CheckpointBrowserPage : UserControlBase { InitializeComponent(); } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - - private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e) - { - if (sender is not ScrollViewer scrollViewer) - return; - - var isAtEnd = scrollViewer.Offset == scrollViewer.ScrollBarMaximum; - Debug.WriteLine($"IsAtEnd: {isAtEnd}"); - } - - private void InputElement_OnKeyDown(object? sender, KeyEventArgs e) - { - if (e.Key == Key.Escape && DataContext is CheckpointBrowserViewModel viewModel) - { - viewModel.ClearSearchQuery(); - } - } } diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml index 0ae40150..a044be5d 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml @@ -56,14 +56,13 @@ - @@ -71,11 +70,31 @@ IsVisible="{Binding CanShowTriggerWords}" Text="{x:Static lang:Resources.Action_CopyTriggerWords}" IconSource="Copy" /> + + + False + + + + + True + + + - + - + - + + + + + - @@ -463,6 +495,36 @@ HorizontalContentAlignment="Right" DefaultLabelPosition="Right"> + + + + + + + + + + + + + + + + + + + + + + @@ -546,7 +621,7 @@ + Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" /> diff --git a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs index 34fcc7aa..086780a3 100644 --- a/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs +++ b/StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs @@ -44,8 +44,7 @@ public partial class CheckpointsPage : UserControlBase if (DataContext is CheckpointsPageViewModel vm) { - subscription = vm.WhenPropertyChanged(m => m.ShowConnectedModelImages) - .Subscribe(_ => InvalidateRepeater()); + subscription = vm.WhenPropertyChanged(m => m.ShowConnectedModelImages).Subscribe(_ => InvalidateRepeater()); } } @@ -70,7 +69,10 @@ public partial class CheckpointsPage : UserControlBase case CheckpointFolder folder: { if (e.Data.Get("Context") is not CheckpointFile file) - return; + { + await folder.OnDrop(e); + break; + } var filePath = new FilePath(file.FilePath); if (filePath.Directory?.FullPath != folder.DirectoryPath) @@ -82,7 +84,10 @@ public partial class CheckpointsPage : UserControlBase case CheckpointFile file: { if (e.Data.Get("Context") is not CheckpointFile dragFile) - return; + { + await file.ParentFolder.OnDrop(e); + break; + } var parentFolder = file.ParentFolder; var dragFilePath = new FilePath(dragFile.FilePath); @@ -98,9 +103,14 @@ public partial class CheckpointsPage : UserControlBase private static void OnDragExit(object? sender, DragEventArgs e) { var sourceDataContext = (e.Source as Control)?.DataContext; - if (sourceDataContext is CheckpointFolder folder) + switch (sourceDataContext) { - folder.IsCurrentDragTarget = false; + case CheckpointFolder folder: + folder.IsCurrentDragTarget = false; + break; + case CheckpointFile file: + file.ParentFolder.IsCurrentDragTarget = false; + break; } } @@ -123,7 +133,10 @@ public partial class CheckpointsPage : UserControlBase { folder.IsExpanded = true; if (e.Data.Get("Context") is not CheckpointFile file) - return; + { + folder.IsCurrentDragTarget = true; + break; + } var filePath = new FilePath(file.FilePath); folder.IsCurrentDragTarget = filePath.Directory?.FullPath != folder.DirectoryPath; @@ -132,12 +145,14 @@ public partial class CheckpointsPage : UserControlBase case CheckpointFile file: { if (e.Data.Get("Context") is not CheckpointFile dragFile) - return; + { + file.ParentFolder.IsCurrentDragTarget = true; + break; + } var parentFolder = file.ParentFolder; var dragFilePath = new FilePath(dragFile.FilePath); - parentFolder.IsCurrentDragTarget = - dragFilePath.Directory?.FullPath != parentFolder.DirectoryPath; + parentFolder.IsCurrentDragTarget = dragFilePath.Directory?.FullPath != parentFolder.DirectoryPath; break; } } diff --git a/StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml b/StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml new file mode 100644 index 00000000..ff25d806 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml @@ -0,0 +1,545 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml.cs b/StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml.cs new file mode 100644 index 00000000..e149bf5f --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml.cs @@ -0,0 +1,41 @@ +using System.Diagnostics; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Avalonia.ViewModels; +using StabilityMatrix.Core.Attributes; +using CivitAiBrowserViewModel = StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser.CivitAiBrowserViewModel; + +namespace StabilityMatrix.Avalonia.Views; + +[Singleton] +public partial class CivitAiBrowserPage : UserControlBase +{ + public CivitAiBrowserPage() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e) + { + if (sender is not ScrollViewer scrollViewer) + return; + + var isAtEnd = scrollViewer.Offset == scrollViewer.ScrollBarMaximum; + Debug.WriteLine($"IsAtEnd: {isAtEnd}"); + } + + private void InputElement_OnKeyDown(object? sender, KeyEventArgs e) + { + if (e.Key == Key.Escape && DataContext is CivitAiBrowserViewModel viewModel) + { + viewModel.ClearSearchQuery(); + } + } +} diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/InferenceConnectionHelpDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/InferenceConnectionHelpDialog.axaml index c64594d8..69eee544 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/InferenceConnectionHelpDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/InferenceConnectionHelpDialog.axaml @@ -30,7 +30,7 @@ IsOpen="{Binding IsInstallMode}" IsVisible="{Binding IsInstallMode}" Margin="0,4" - Title="Would you like to be directed to install ComfyUI now?"/> + Title="Would you like to install ComfyUI now?"/> { - Dispatcher.UIThread.Invoke(() => - { - var editor = this.FindControl("Console"); - if (editor?.Document == null) - return; - var line = Math.Max(editor.Document.LineCount - 1, 1); - editor.ScrollToLine(line); - }); + Dispatcher + .UIThread + .Invoke(() => + { + var editor = this.FindControl("Console"); + if (editor?.Document == null) + return; + var line = Math.Max(editor.Document.LineCount - 1, 1); + editor.ScrollToLine(line); + }); }; } } diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml new file mode 100644 index 00000000..e00459fc --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml.cs b/StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml.cs new file mode 100644 index 00000000..7eb8526e --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/Dialogs/PropertyGridDialog.axaml.cs @@ -0,0 +1,13 @@ +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.Views.Dialogs; + +[Transient] +public partial class PropertyGridDialog : UserControlBase +{ + public PropertyGridDialog() + { + InitializeComponent(); + } +} diff --git a/StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml b/StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml new file mode 100644 index 00000000..4a1222b2 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml.cs b/StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml.cs new file mode 100644 index 00000000..dc52ab2d --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml.cs @@ -0,0 +1,13 @@ +using StabilityMatrix.Avalonia.Controls; +using StabilityMatrix.Core.Attributes; + +namespace StabilityMatrix.Avalonia.Views; + +[Singleton] +public partial class HuggingFacePage : UserControlBase +{ + public HuggingFacePage() + { + InitializeComponent(); + } +} diff --git a/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToImageView.axaml b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToImageView.axaml new file mode 100644 index 00000000..9def61c9 --- /dev/null +++ b/StabilityMatrix.Avalonia/Views/Inference/InferenceImageToImageView.axaml @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml index db6a5b5c..a347b077 100644 --- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml +++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml @@ -19,10 +19,13 @@ mc:Ignorable="d"> + + 24 17 6,3 Medium + diff --git a/StabilityMatrix.Core/Attributes/ManagedServiceAttribute.cs b/StabilityMatrix.Core/Attributes/ManagedServiceAttribute.cs index 1d78f9f7..41f0fd1f 100644 --- a/StabilityMatrix.Core/Attributes/ManagedServiceAttribute.cs +++ b/StabilityMatrix.Core/Attributes/ManagedServiceAttribute.cs @@ -1,4 +1,8 @@ -namespace StabilityMatrix.Core.Attributes; +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; +namespace StabilityMatrix.Core.Attributes; + +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors), MeansImplicitUse] [AttributeUsage(AttributeTargets.Class)] -public class ManagedServiceAttribute : Attribute { } +public class ManagedServiceAttribute : Attribute; diff --git a/StabilityMatrix.Core/Attributes/TransientAttribute.cs b/StabilityMatrix.Core/Attributes/TransientAttribute.cs index acccb775..ede9ff4c 100644 --- a/StabilityMatrix.Core/Attributes/TransientAttribute.cs +++ b/StabilityMatrix.Core/Attributes/TransientAttribute.cs @@ -1,13 +1,14 @@ using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; namespace StabilityMatrix.Core.Attributes; -[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors), MeansImplicitUse] [AttributeUsage(AttributeTargets.Class)] public class TransientAttribute : Attribute { [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - public Type? InterfaceType { get; init; } + public Type? InterfaceType { get; } public TransientAttribute() { } diff --git a/StabilityMatrix.Core/Converters/Json/StringJsonConverter.cs b/StabilityMatrix.Core/Converters/Json/StringJsonConverter.cs index 16904246..c7a4a20b 100644 --- a/StabilityMatrix.Core/Converters/Json/StringJsonConverter.cs +++ b/StabilityMatrix.Core/Converters/Json/StringJsonConverter.cs @@ -1,13 +1,19 @@ -using System.Text.Json; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json; using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace StabilityMatrix.Core.Converters.Json; /// /// Json converter for types that serialize to string by `ToString()` and /// can be created by `Activator.CreateInstance(Type, string)` +/// Types implementing will be formatted with /// -public class StringJsonConverter : JsonConverter +[PublicAPI] +public class StringJsonConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T> + : JsonConverter { /// public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -20,15 +26,28 @@ public class StringJsonConverter : JsonConverter var value = reader.GetString(); if (value is null) { - throw new JsonException(); + return default; } - return (T?) Activator.CreateInstance(typeToConvert, value); + return (T?)Activator.CreateInstance(typeToConvert, value); } /// public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) { - writer.WriteStringValue(value?.ToString()); + if (value is null) + { + writer.WriteNullValue(); + return; + } + + if (value is IFormattable formattable) + { + writer.WriteStringValue(formattable.ToString(null, CultureInfo.InvariantCulture)); + } + else + { + writer.WriteStringValue(value.ToString()); + } } } diff --git a/StabilityMatrix.Core/Database/LiteDbContext.cs b/StabilityMatrix.Core/Database/LiteDbContext.cs index fdf946f6..429c092b 100644 --- a/StabilityMatrix.Core/Database/LiteDbContext.cs +++ b/StabilityMatrix.Core/Database/LiteDbContext.cs @@ -16,15 +16,14 @@ public class LiteDbContext : ILiteDbContext private readonly ISettingsManager settingsManager; private readonly DebugOptions debugOptions; - private LiteDatabaseAsync? database; - public LiteDatabaseAsync Database => database ??= CreateDatabase(); + private readonly Lazy lazyDatabase; + public LiteDatabaseAsync Database => lazyDatabase.Value; // Notification events public event EventHandler? CivitModelsChanged; // Collections (Tables) - public ILiteCollectionAsync CivitModels => - Database.GetCollection("CivitModels"); + public ILiteCollectionAsync CivitModels => Database.GetCollection("CivitModels"); public ILiteCollectionAsync CivitModelVersions => Database.GetCollection("CivitModelVersions"); public ILiteCollectionAsync CivitModelQueryCache => @@ -47,6 +46,8 @@ public class LiteDbContext : ILiteDbContext this.logger = logger; this.settingsManager = settingsManager; this.debugOptions = debugOptions.Value; + + lazyDatabase = new Lazy(CreateDatabase); } private LiteDatabaseAsync CreateDatabase() @@ -64,19 +65,12 @@ public class LiteDbContext : ILiteDbContext { var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db"); db = new LiteDatabaseAsync( - new ConnectionString() - { - Filename = dbPath, - Connection = ConnectionType.Shared, - } + new ConnectionString() { Filename = dbPath, Connection = ConnectionType.Shared, } ); } catch (IOException e) { - logger.LogWarning( - "Database in use or not accessible ({Message}), using temporary database", - e.Message - ); + logger.LogWarning("Database in use or not accessible ({Message}), using temporary database", e.Message); } } @@ -84,30 +78,18 @@ public class LiteDbContext : ILiteDbContext db ??= new LiteDatabaseAsync(":temp:"); // Register reference fields - LiteDBExtensions.Register( - m => m.ModelVersions, - "CivitModelVersions" - ); - LiteDBExtensions.Register( - e => e.Items, - "CivitModels" - ); + LiteDBExtensions.Register(m => m.ModelVersions, "CivitModelVersions"); + LiteDBExtensions.Register(e => e.Items, "CivitModels"); return db; } - public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync( - string hashBlake3 - ) + public async Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3) { var version = await CivitModelVersions .Query() .Where( - mv => - mv.Files! - .Select(f => f.Hashes) - .Select(hashes => hashes.BLAKE3) - .Any(hash => hash == hashBlake3) + mv => mv.Files!.Select(f => f.Hashes).Select(hashes => hashes.BLAKE3).Any(hash => hash == hashBlake3) ) .FirstOrDefaultAsync() .ConfigureAwait(false); @@ -128,9 +110,7 @@ public class LiteDbContext : ILiteDbContext public async Task UpsertCivitModelAsync(CivitModel civitModel) { // Insert model versions first then model - var versionsUpdated = await CivitModelVersions - .UpsertAsync(civitModel.ModelVersions) - .ConfigureAwait(false); + var versionsUpdated = await CivitModelVersions.UpsertAsync(civitModel.ModelVersions).ConfigureAwait(false); var updated = await CivitModels.UpsertAsync(civitModel).ConfigureAwait(false); // Notify listeners on any change var anyUpdated = versionsUpdated > 0 || updated; @@ -182,20 +162,22 @@ public class LiteDbContext : ILiteDbContext return null; } - public Task UpsertGithubCacheEntry(GithubCacheEntry cacheEntry) => - GithubCache.UpsertAsync(cacheEntry); + public Task UpsertGithubCacheEntry(GithubCacheEntry cacheEntry) => GithubCache.UpsertAsync(cacheEntry); public void Dispose() { - if (database is not null) + if (lazyDatabase.IsValueCreated) { try { - database.Dispose(); + Database.Dispose(); } catch (ObjectDisposedException) { } - - database = null; + catch (ApplicationException) + { + // Ignores a mutex error from library + // https://stability-matrix.sentry.io/share/issue/5c62f37462444e7eab18cea314af231f/ + } } GC.SuppressFinalize(this); diff --git a/StabilityMatrix.Core/Extensions/EnumerableExtensions.cs b/StabilityMatrix.Core/Extensions/EnumerableExtensions.cs index 26ae5452..49dfa962 100644 --- a/StabilityMatrix.Core/Extensions/EnumerableExtensions.cs +++ b/StabilityMatrix.Core/Extensions/EnumerableExtensions.cs @@ -2,47 +2,61 @@ public static class EnumerableExtensions { - public static IEnumerable<(int, T)> Enumerate( - this IEnumerable items, - int start - ) { + public static IEnumerable<(int, T)> Enumerate(this IEnumerable items, int start) + { return items.Select((item, index) => (index + start, item)); } - - public static IEnumerable<(int, T)> Enumerate( - this IEnumerable items - ) { + + public static IEnumerable<(int, T)> Enumerate(this IEnumerable items) + { return items.Select((item, index) => (index, item)); } - + /// /// Nested for loop helper /// public static IEnumerable<(T, T)> Product(this IEnumerable items, IEnumerable other) { - return from item1 in items - from item2 in other - select (item1, item2); - } - + return from item1 in items from item2 in other select (item1, item2); + } + public static async Task> SelectAsync( - this IEnumerable source, Func> method, - int concurrency = int.MaxValue) + this IEnumerable source, + Func> method, + int concurrency = int.MaxValue + ) { using var semaphore = new SemaphoreSlim(concurrency); - return await Task.WhenAll(source.Select(async s => + return await Task.WhenAll( + source.Select(async s => + { + try + { + // ReSharper disable once AccessToDisposedClosure + await semaphore.WaitAsync().ConfigureAwait(false); + return await method(s).ConfigureAwait(false); + } + finally + { + // ReSharper disable once AccessToDisposedClosure + semaphore.Release(); + } + }) + ) + .ConfigureAwait(false); + } + + /// + /// Executes a specified action on each element in a collection. + /// + /// The type of elements in the collection. + /// The collection to iterate over. + /// The action to perform on each element in the collection. + public static void ForEach(this IEnumerable items, Action action) + { + foreach (var item in items) { - try - { - // ReSharper disable once AccessToDisposedClosure - await semaphore.WaitAsync().ConfigureAwait(false); - return await method(s).ConfigureAwait(false); - } - finally - { - // ReSharper disable once AccessToDisposedClosure - semaphore.Release(); - } - })).ConfigureAwait(false); + action(item); + } } } diff --git a/StabilityMatrix.Core/Extensions/LiteDBExtensions.cs b/StabilityMatrix.Core/Extensions/LiteDBExtensions.cs index a40b4213..3377a2e3 100644 --- a/StabilityMatrix.Core/Extensions/LiteDBExtensions.cs +++ b/StabilityMatrix.Core/Extensions/LiteDBExtensions.cs @@ -1,4 +1,5 @@ -using System.Linq.Expressions; +using System.Collections.Concurrent; +using System.Linq.Expressions; using System.Reflection; using LiteDB; using LiteDB.Async; @@ -8,7 +9,8 @@ namespace StabilityMatrix.Core.Extensions; // ReSharper disable once InconsistentNaming public static class LiteDBExtensions { - private static readonly Dictionary Mapper = new(); + private static readonly ConcurrentDictionary Mapper = + new(); public static void Register(Expression?>> exp, string? collection = null) { @@ -16,7 +18,7 @@ public static class LiteDBExtensions if (member == null) throw new ArgumentException("Expecting Member Expression"); BsonMapper.Global.Entity().DbRef(exp, collection); - Mapper.Add(typeof(T), (typeof(TU), member.Name, true)); + Mapper.TryAdd(typeof(T), (typeof(TU), member.Name, true)); } public static void Register(Expression> exp, string? collection = null) @@ -25,12 +27,13 @@ public static class LiteDBExtensions if (member == null) throw new ArgumentException("Expecting Member Expression"); BsonMapper.Global.Entity().DbRef(exp, collection); - Mapper.Add(typeof(T), (typeof(TU), member.Name, false)); + Mapper.TryAdd(typeof(T), (typeof(TU), member.Name, false)); } public static ILiteCollection? IncludeAll(this ILiteCollection col) { - if (!Mapper.ContainsKey(typeof(T))) return null; + if (!Mapper.ContainsKey(typeof(T))) + return null; var stringList = new List(); var key = typeof(T); @@ -45,12 +48,13 @@ public static class LiteDBExtensions flag = false; } - return stringList.Aggregate(col, (current, keySelector) => current.Include((BsonExpression) keySelector)); + return stringList.Aggregate(col, (current, keySelector) => current.Include((BsonExpression)keySelector)); } - + public static ILiteCollectionAsync IncludeAll(this ILiteCollectionAsync col) { - if (!Mapper.ContainsKey(typeof(T))) return col; + if (!Mapper.ContainsKey(typeof(T))) + return col; var stringList = new List(); var key = typeof(T); @@ -65,6 +69,6 @@ public static class LiteDBExtensions flag = false; } - return stringList.Aggregate(col, (current, keySelector) => current.Include((BsonExpression) keySelector)); + return stringList.Aggregate(col, (current, keySelector) => current.Include((BsonExpression)keySelector)); } } diff --git a/StabilityMatrix.Core/Extensions/ObjectExtensions.cs b/StabilityMatrix.Core/Extensions/ObjectExtensions.cs index 9cbd9923..fa54caee 100644 --- a/StabilityMatrix.Core/Extensions/ObjectExtensions.cs +++ b/StabilityMatrix.Core/Extensions/ObjectExtensions.cs @@ -1,29 +1,35 @@ -using System.Reflection; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using JetBrains.Annotations; using RockLib.Reflection.Optimized; namespace StabilityMatrix.Core.Extensions; +[PublicAPI] public static class ObjectExtensions { /// /// Cache of Types to named field getters /// - private static readonly Dictionary< - Type, - Dictionary> - > FieldGetterTypeCache = new(); + private static readonly Dictionary>> FieldGetterTypeCache = new(); /// /// Cache of Types to named field setters /// - private static readonly Dictionary< - Type, - Dictionary> - > FieldSetterTypeCache = new(); + private static readonly Dictionary>> FieldSetterTypeCache = new(); + + /// + /// Cache of Types to named property getters + /// + private static readonly Dictionary>> PropertyGetterTypeCache = new(); /// /// Get the value of a named private field from an object /// + /// + /// The field must be defined by the runtime type of or its first base type. + /// For higher inheritance levels, use to specify the exact defining type. + /// public static T? GetPrivateField(this object obj, string fieldName) { // Check cache @@ -32,16 +38,93 @@ public static class ObjectExtensions if (!fieldGetterCache.TryGetValue(fieldName, out var fieldGetter)) { // Get the field - var field = obj.GetType() - .GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); // Try get from parent - field ??= obj.GetType() - .BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + + if (field is null) + { + throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}"); + } + + // Create a getter for the field + fieldGetter = field.CreateGetter(); + + // Add to cache + fieldGetterCache.Add(fieldName, fieldGetter); + } + + return (T?)fieldGetter(obj); + } + + /// + /// Get the value of a protected property from an object + /// + /// + /// The property must be defined by the runtime type of or its first base type. + /// + public static object? GetProtectedProperty(this object obj, [LocalizationRequired(false)] string propertyName) + { + // Check cache + var fieldGetterCache = PropertyGetterTypeCache.GetOrAdd(obj.GetType()); + + if (!fieldGetterCache.TryGetValue(propertyName, out var propertyGetter)) + { + // Get the field + var propertyInfo = obj.GetType() + .GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + // Try get from parent + propertyInfo ??= obj.GetType() + .BaseType + ?.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + + if (propertyInfo is null) + { + throw new ArgumentException($"Property {propertyName} not found on type {obj.GetType().Name}"); + } + + // Create a getter for the field + propertyGetter = o => propertyInfo.GetValue(o)!; + + // Add to cache + fieldGetterCache.Add(propertyName, propertyGetter); + } + + return (object?)propertyGetter(obj); + } + + /// + /// Get the value of a protected property from an object + /// + /// + /// The property must be defined by the runtime type of or its first base type. + /// + public static T? GetProtectedProperty(this object obj, [LocalizationRequired(false)] string propertyName) + where T : class + { + return (T?)GetProtectedProperty(obj, propertyName); + } + + /// + /// Get the value of a named private field from an object + /// + /// Type of the object that defines the field, must be a base class of + /// Type of the field + public static T? GetPrivateField(this TObject obj, string fieldName) + where TObject : class + { + // Check cache + var fieldGetterCache = FieldGetterTypeCache.GetOrAdd(typeof(TObject)); + + if (!fieldGetterCache.TryGetValue(fieldName, out var fieldGetter)) + { + // Get the field + var field = typeof(TObject).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); if (field is null) { throw new ArgumentException( - $"Field {fieldName} not found on type {obj.GetType().Name}" + $"Field {typeof(TObject).Name}.{fieldName} not found on type {obj.GetType().Name}" ); } @@ -66,17 +149,13 @@ public static class ObjectExtensions if (!fieldSetterCache.TryGetValue(fieldName, out var fieldSetter)) { // Get the field - var field = obj.GetType() - .GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); // Try get from parent - field ??= obj.GetType() - .BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); if (field is null) { - throw new ArgumentException( - $"Field {fieldName} not found on type {obj.GetType().Name}" - ); + throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}"); } // Create a setter for the field @@ -100,17 +179,13 @@ public static class ObjectExtensions if (!fieldSetterCache.TryGetValue(fieldName, out var fieldSetter)) { // Get the field - var field = obj.GetType() - .GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + var field = obj.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); // Try get from parent - field ??= obj.GetType() - .BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + field ??= obj.GetType().BaseType?.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); if (field is null) { - throw new ArgumentException( - $"Field {fieldName} not found on type {obj.GetType().Name}" - ); + throw new ArgumentException($"Field {fieldName} not found on type {obj.GetType().Name}"); } // Create a setter for the field diff --git a/StabilityMatrix.Core/Helper/ArchiveHelper.cs b/StabilityMatrix.Core/Helper/ArchiveHelper.cs index 40c5d53b..e2774722 100644 --- a/StabilityMatrix.Core/Helper/ArchiveHelper.cs +++ b/StabilityMatrix.Core/Helper/ArchiveHelper.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.RegularExpressions; using NLog; @@ -59,8 +58,8 @@ public static partial class ArchiveHelper public static async Task TestArchive(string archivePath) { var process = ProcessRunner.StartAnsiProcess(SevenZipPath, new[] { "t", archivePath }); - await process.WaitForExitAsync(); - var output = await process.StandardOutput.ReadToEndAsync(); + await process.WaitForExitAsync().ConfigureAwait(false); + var output = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false); var matches = Regex7ZOutput().Matches(output); var size = ulong.Parse(matches[0].Value); var compressed = ulong.Parse(matches[1].Value); @@ -86,12 +85,9 @@ public static partial class ArchiveHelper public static async Task Extract7Z(string archivePath, string extractDirectory) { - var result = await ProcessRunner - .GetProcessResultAsync( - SevenZipPath, - new[] { "x", archivePath, "-o" + ProcessRunner.Quote(extractDirectory), "-y" } - ) - .ConfigureAwait(false); + var args = $"x {ProcessRunner.Quote(archivePath)} -o{ProcessRunner.Quote(extractDirectory)} -y"; + + var result = await ProcessRunner.GetProcessResultAsync(SevenZipPath, args).ConfigureAwait(false); result.EnsureSuccessExitCode(); @@ -131,27 +127,17 @@ public static partial class ArchiveHelper var percent = int.Parse(match.Groups[1].Value); var currentFile = match.Groups[2].Value; progress.Report( - new ProgressReport( - percent / (float)100, - "Extracting", - currentFile, - type: ProgressType.Extract - ) + new ProgressReport(percent / (float)100, "Extracting", currentFile, type: ProgressType.Extract) ); } }); progress.Report(new ProgressReport(-1, isIndeterminate: true, type: ProgressType.Extract)); // Need -bsp1 for progress reports - var args = - $"x {ProcessRunner.Quote(archivePath)} -o{ProcessRunner.Quote(extractDirectory)} -y -bsp1"; + var args = $"x {ProcessRunner.Quote(archivePath)} -o{ProcessRunner.Quote(extractDirectory)} -y -bsp1"; Logger.Debug($"Starting process '{SevenZipPath}' with arguments '{args}'"); - using var process = ProcessRunner.StartProcess( - SevenZipPath, - args, - outputDataReceived: onOutput - ); + using var process = ProcessRunner.StartProcess(SevenZipPath, args, outputDataReceived: onOutput); await ProcessRunner.WaitForExitConditionAsync(process).ConfigureAwait(false); progress.Report(new ProgressReport(1f, "Finished extracting", type: ProgressType.Extract)); @@ -185,7 +171,7 @@ public static partial class ArchiveHelper throw new ArgumentException("Archive must be a zipped tar."); } // Extract the tar.gz to tar - await Extract7Z(archivePath, extractDirectory); + await Extract7Z(archivePath, extractDirectory).ConfigureAwait(false); // Extract the tar var tarPath = Path.Combine(extractDirectory, Path.GetFileNameWithoutExtension(archivePath)); @@ -196,7 +182,7 @@ public static partial class ArchiveHelper try { - return await Extract7Z(tarPath, extractDirectory); + return await Extract7Z(tarPath, extractDirectory).ConfigureAwait(false); } finally { @@ -215,11 +201,11 @@ public static partial class ArchiveHelper { if (archivePath.EndsWith(".tar.gz")) { - return await Extract7ZTar(archivePath, extractDirectory); + return await Extract7ZTar(archivePath, extractDirectory).ConfigureAwait(false); } else { - return await Extract7Z(archivePath, extractDirectory); + return await Extract7Z(archivePath, extractDirectory).ConfigureAwait(false); } } @@ -241,7 +227,7 @@ public static partial class ArchiveHelper var count = 0ul; // Get true size - var (total, _) = await TestArchive(archivePath); + var (total, _) = await TestArchive(archivePath).ConfigureAwait(false); // If not available, use the size of the archive file if (total == 0) @@ -266,32 +252,30 @@ public static partial class ArchiveHelper }; } - await Task.Factory.StartNew( - () => - { - var extractOptions = new ExtractionOptions + await Task.Factory + .StartNew( + () => { - Overwrite = true, - ExtractFullPath = true, - }; - using var stream = File.OpenRead(archivePath); - using var archive = ReaderFactory.Open(stream); + var extractOptions = new ExtractionOptions { Overwrite = true, ExtractFullPath = true, }; + using var stream = File.OpenRead(archivePath); + using var archive = ReaderFactory.Open(stream); - // Start the progress reporting timer - progressMonitor?.Start(); + // Start the progress reporting timer + progressMonitor?.Start(); - while (archive.MoveToNextEntry()) - { - var entry = archive.Entry; - if (!entry.IsDirectory) + while (archive.MoveToNextEntry()) { - count += (ulong)entry.CompressedSize; + var entry = archive.Entry; + if (!entry.IsDirectory) + { + count += (ulong)entry.CompressedSize; + } + archive.WriteEntryToDirectory(outputDirectory, extractOptions); } - archive.WriteEntryToDirectory(outputDirectory, extractOptions); - } - }, - TaskCreationOptions.LongRunning - ); + }, + TaskCreationOptions.LongRunning + ) + .ConfigureAwait(false); progress?.Report(new ProgressReport(progress: 1, message: "Done extracting")); progressMonitor?.Stop(); @@ -305,7 +289,7 @@ public static partial class ArchiveHelper public static async Task ExtractManaged(string archivePath, string outputDirectory) { await using var stream = File.OpenRead(archivePath); - await ExtractManaged(stream, outputDirectory); + await ExtractManaged(stream, outputDirectory).ConfigureAwait(false); } /// @@ -372,16 +356,14 @@ public static partial class ArchiveHelper } catch (IOException e) { - Logger.Warn( - $"Could not extract symbolic link, copying file instead: {e.Message}" - ); + Logger.Warn($"Could not extract symbolic link, copying file instead: {e.Message}"); } } // Write file await using var entryStream = reader.OpenEntryStream(); await using var fileStream = File.Create(outputPath); - await entryStream.CopyToAsync(fileStream); + await entryStream.CopyToAsync(fileStream).ConfigureAwait(false); } } } diff --git a/StabilityMatrix.Core/Helper/FileHash.cs b/StabilityMatrix.Core/Helper/FileHash.cs index 1378db9d..4c4cfc80 100644 --- a/StabilityMatrix.Core/Helper/FileHash.cs +++ b/StabilityMatrix.Core/Helper/FileHash.cs @@ -39,10 +39,7 @@ public static class FileHash } } - public static async Task GetSha256Async( - string filePath, - IProgress? progress = default - ) + public static async Task GetSha256Async(string filePath, IProgress? progress = default) { if (!File.Exists(filePath)) { @@ -62,13 +59,7 @@ public static class FileHash buffer, totalBytesRead => { - progress?.Report( - new ProgressReport( - totalBytesRead, - totalBytes, - type: ProgressType.Hashing - ) - ); + progress?.Report(new ProgressReport(totalBytesRead, totalBytes, type: ProgressType.Hashing)); } ) .ConfigureAwait(false); @@ -80,10 +71,7 @@ public static class FileHash } } - public static async Task GetBlake3Async( - string filePath, - IProgress? progress = default - ) + public static async Task GetBlake3Async(string filePath, IProgress? progress = default) { if (!File.Exists(filePath)) { @@ -93,7 +81,7 @@ public static class FileHash var totalBytes = Convert.ToUInt64(new FileInfo(filePath).Length); var readBytes = 0ul; var shared = ArrayPool.Shared; - var buffer = shared.Rent((int)FileTransfers.GetBufferSize(totalBytes)); + var buffer = shared.Rent(GetBufferSize(totalBytes)); try { await using var stream = File.OpenRead(filePath); @@ -117,10 +105,7 @@ public static class FileHash } } - public static async Task GetBlake3Async( - Stream stream, - IProgress? progress = default - ) + public static async Task GetBlake3Async(Stream stream, IProgress? progress = default) { var totalBytes = Convert.ToUInt64(stream.Length); var readBytes = 0ul; @@ -191,11 +176,7 @@ public static class FileHash false ); - using var accessor = memoryMappedFile.CreateViewAccessor( - 0, - totalBytes, - MemoryMappedFileAccess.Read - ); + using var accessor = memoryMappedFile.CreateViewAccessor(0, totalBytes, MemoryMappedFileAccess.Read); Debug.Assert(accessor.Capacity == fileStream.Length); @@ -213,4 +194,16 @@ public static class FileHash { return Task.Run(() => GetBlake3MemoryMappedParallel(filePath)); } + + /// + /// Determines suitable buffer size for hashing based on stream length. + /// + private static int GetBufferSize(ulong totalBytes) => + totalBytes switch + { + < Size.MiB => 8 * (int)Size.KiB, + < 500 * Size.MiB => 16 * (int)Size.KiB, + < Size.GiB => 32 * (int)Size.KiB, + _ => 64 * (int)Size.KiB + }; } diff --git a/StabilityMatrix.Core/Helper/HardwareHelper.cs b/StabilityMatrix.Core/Helper/HardwareHelper.cs deleted file mode 100644 index b50fe42d..00000000 --- a/StabilityMatrix.Core/Helper/HardwareHelper.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System.Diagnostics; -using System.Runtime.Versioning; -using System.Text.RegularExpressions; -using Microsoft.Win32; - -namespace StabilityMatrix.Core.Helper; - -public static partial class HardwareHelper -{ - private static IReadOnlyList? cachedGpuInfos; - - private static string RunBashCommand(string command) - { - var processInfo = new ProcessStartInfo("bash", "-c \"" + command + "\"") - { - UseShellExecute = false, - RedirectStandardOutput = true - }; - - var process = Process.Start(processInfo); - - process.WaitForExit(); - - var output = process.StandardOutput.ReadToEnd(); - - return output; - } - - [SupportedOSPlatform("windows")] - private static IEnumerable IterGpuInfoWindows() - { - const string gpuRegistryKeyPath = - @"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}"; - - using var baseKey = Registry.LocalMachine.OpenSubKey(gpuRegistryKeyPath); - - if (baseKey == null) yield break; - - foreach (var subKeyName in baseKey.GetSubKeyNames().Where(k => k.StartsWith("0"))) - { - using var subKey = baseKey.OpenSubKey(subKeyName); - if (subKey != null) - { - yield return new GpuInfo - { - Name = subKey.GetValue("DriverDesc")?.ToString(), - MemoryBytes = Convert.ToUInt64(subKey.GetValue("HardwareInformation.qwMemorySize")), - }; - } - } - } - - [SupportedOSPlatform("linux")] - private static IEnumerable IterGpuInfoLinux() - { - var output = RunBashCommand("lspci | grep VGA"); - var gpuLines = output.Split("\n"); - - foreach (var line in gpuLines) - { - if (string.IsNullOrWhiteSpace(line)) continue; - - var gpuId = line.Split(' ')[0]; // The GPU ID is the first part of the line - var gpuOutput = RunBashCommand($"lspci -v -s {gpuId}"); - - ulong memoryBytes = 0; - string? name = null; - - // Parse output with regex - var match = Regex.Match(gpuOutput, @"VGA compatible controller: ([^\n]*)"); - if (match.Success) - { - name = match.Groups[1].Value.Trim(); - } - - match = Regex.Match(gpuOutput, @"prefetchable\) \[size=(\\d+)M\]"); - if (match.Success) - { - memoryBytes = ulong.Parse(match.Groups[1].Value) * 1024 * 1024; - } - - yield return new GpuInfo { Name = name, MemoryBytes = memoryBytes }; - } - } - - /// - /// Yields GpuInfo for each GPU in the system. - /// - public static IEnumerable IterGpuInfo() - { - if (Compat.IsWindows) - { - return IterGpuInfoWindows(); - } - else if (Compat.IsLinux) - { - // Since this requires shell commands, fetch cached value if available. - if (cachedGpuInfos is not null) - { - return cachedGpuInfos; - } - - // No cache, fetch and cache. - cachedGpuInfos = IterGpuInfoLinux().ToList(); - return cachedGpuInfos; - } - // TODO: Implement for macOS - return Enumerable.Empty(); - } - - /// - /// Return true if the system has at least one Nvidia GPU. - /// - public static bool HasNvidiaGpu() - { - return IterGpuInfo().Any(gpu => gpu.IsNvidia); - } - - /// - /// Return true if the system has at least one AMD GPU. - /// - public static bool HasAmdGpu() - { - return IterGpuInfo().Any(gpu => gpu.IsAmd); - } - - // Set ROCm for default if AMD and Linux - public static bool PreferRocm() => !HasNvidiaGpu() - && HasAmdGpu() - && Compat.IsLinux; - - // Set DirectML for default if AMD and Windows - public static bool PreferDirectML() => !HasNvidiaGpu() - && HasAmdGpu() - && Compat.IsWindows; -} - -public enum Level -{ - Unknown, - Low, - Medium, - High -} - -public record GpuInfo -{ - public string? Name { get; init; } = string.Empty; - public ulong MemoryBytes { get; init; } - public Level? MemoryLevel => MemoryBytes switch - { - <= 0 => Level.Unknown, - < 4 * Size.GiB => Level.Low, - < 8 * Size.GiB => Level.Medium, - _ => Level.High - }; - - public bool IsNvidia - { - get - { - var name = Name?.ToLowerInvariant(); - - if (string.IsNullOrEmpty(name)) return false; - - return name.Contains("nvidia") - || name.Contains("tesla"); - } - } - - public bool IsAmd => Name?.ToLowerInvariant().Contains("amd") ?? false; -} diff --git a/StabilityMatrix.Core/Helper/HardwareInfo/CpuInfo.cs b/StabilityMatrix.Core/Helper/HardwareInfo/CpuInfo.cs new file mode 100644 index 00000000..7ea9b2b6 --- /dev/null +++ b/StabilityMatrix.Core/Helper/HardwareInfo/CpuInfo.cs @@ -0,0 +1,7 @@ +namespace StabilityMatrix.Core.Helper.HardwareInfo; + +public readonly record struct CpuInfo +{ + public string ProcessorCaption { get; init; } + public string ProcessorName { get; init; } +} diff --git a/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs b/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs new file mode 100644 index 00000000..faabd558 --- /dev/null +++ b/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs @@ -0,0 +1,31 @@ +namespace StabilityMatrix.Core.Helper.HardwareInfo; + +public record GpuInfo +{ + public int Index { get; init; } + public string? Name { get; init; } = string.Empty; + public ulong MemoryBytes { get; init; } + public MemoryLevel? MemoryLevel => + MemoryBytes switch + { + <= 0 => HardwareInfo.MemoryLevel.Unknown, + < 4 * Size.GiB => HardwareInfo.MemoryLevel.Low, + < 8 * Size.GiB => HardwareInfo.MemoryLevel.Medium, + _ => HardwareInfo.MemoryLevel.High + }; + + public bool IsNvidia + { + get + { + var name = Name?.ToLowerInvariant(); + + if (string.IsNullOrEmpty(name)) + return false; + + return name.Contains("nvidia") || name.Contains("tesla"); + } + } + + public bool IsAmd => Name?.Contains("amd", StringComparison.OrdinalIgnoreCase) ?? false; +} diff --git a/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs b/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs new file mode 100644 index 00000000..5a8cec80 --- /dev/null +++ b/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs @@ -0,0 +1,234 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text.RegularExpressions; +using Hardware.Info; +using Microsoft.Win32; + +namespace StabilityMatrix.Core.Helper.HardwareInfo; + +public static partial class HardwareHelper +{ + private static IReadOnlyList? cachedGpuInfos; + + private static readonly Lazy HardwareInfoLazy = new(() => new Hardware.Info.HardwareInfo()); + + public static IHardwareInfo HardwareInfo => HardwareInfoLazy.Value; + + private static string RunBashCommand(string command) + { + var processInfo = new ProcessStartInfo("bash", "-c \"" + command + "\"") + { + UseShellExecute = false, + RedirectStandardOutput = true + }; + + var process = Process.Start(processInfo); + + process.WaitForExit(); + + var output = process.StandardOutput.ReadToEnd(); + + return output; + } + + [SupportedOSPlatform("windows")] + private static IEnumerable IterGpuInfoWindows() + { + const string gpuRegistryKeyPath = + @"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}"; + + using var baseKey = Registry.LocalMachine.OpenSubKey(gpuRegistryKeyPath); + + if (baseKey == null) + yield break; + + var gpuIndex = 0; + + foreach (var subKeyName in baseKey.GetSubKeyNames().Where(k => k.StartsWith("0"))) + { + using var subKey = baseKey.OpenSubKey(subKeyName); + if (subKey != null) + { + yield return new GpuInfo + { + Index = gpuIndex++, + Name = subKey.GetValue("DriverDesc")?.ToString(), + MemoryBytes = Convert.ToUInt64(subKey.GetValue("HardwareInformation.qwMemorySize")), + }; + } + } + } + + [SupportedOSPlatform("linux")] + private static IEnumerable IterGpuInfoLinux() + { + var output = RunBashCommand("lspci | grep VGA"); + var gpuLines = output.Split("\n"); + + var gpuIndex = 0; + + foreach (var line in gpuLines) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + + var gpuId = line.Split(' ')[0]; // The GPU ID is the first part of the line + var gpuOutput = RunBashCommand($"lspci -v -s {gpuId}"); + + ulong memoryBytes = 0; + string? name = null; + + // Parse output with regex + var match = Regex.Match(gpuOutput, @"VGA compatible controller: ([^\n]*)"); + if (match.Success) + { + name = match.Groups[1].Value.Trim(); + } + + match = Regex.Match(gpuOutput, @"prefetchable\) \[size=(\\d+)M\]"); + if (match.Success) + { + memoryBytes = ulong.Parse(match.Groups[1].Value) * 1024 * 1024; + } + + yield return new GpuInfo + { + Index = gpuIndex++, + Name = name, + MemoryBytes = memoryBytes + }; + } + } + + /// + /// Yields GpuInfo for each GPU in the system. + /// + public static IEnumerable IterGpuInfo() + { + if (Compat.IsWindows) + { + return IterGpuInfoWindows(); + } + else if (Compat.IsLinux) + { + // Since this requires shell commands, fetch cached value if available. + if (cachedGpuInfos is not null) + { + return cachedGpuInfos; + } + + // No cache, fetch and cache. + cachedGpuInfos = IterGpuInfoLinux().ToList(); + return cachedGpuInfos; + } + // TODO: Implement for macOS + return Enumerable.Empty(); + } + + /// + /// Return true if the system has at least one Nvidia GPU. + /// + public static bool HasNvidiaGpu() + { + return IterGpuInfo().Any(gpu => gpu.IsNvidia); + } + + /// + /// Return true if the system has at least one AMD GPU. + /// + public static bool HasAmdGpu() + { + return IterGpuInfo().Any(gpu => gpu.IsAmd); + } + + // Set ROCm for default if AMD and Linux + public static bool PreferRocm() => !HasNvidiaGpu() && HasAmdGpu() && Compat.IsLinux; + + // Set DirectML for default if AMD and Windows + public static bool PreferDirectML() => !HasNvidiaGpu() && HasAmdGpu() && Compat.IsWindows; + + /// + /// Gets the total and available physical memory in bytes. + /// + public static MemoryInfo GetMemoryInfo() => + Compat.IsWindows ? GetMemoryInfoImplWindows() : GetMemoryInfoImplGeneric(); + + [SupportedOSPlatform("windows")] + private static MemoryInfo GetMemoryInfoImplWindows() + { + var memoryStatus = new Win32MemoryStatusEx(); + + if (!GlobalMemoryStatusEx(ref memoryStatus)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (!GetPhysicallyInstalledSystemMemory(out var installedMemoryKb)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + return new MemoryInfo + { + TotalInstalledBytes = (ulong)installedMemoryKb * 1024, + TotalPhysicalBytes = memoryStatus.UllTotalPhys, + AvailablePhysicalBytes = memoryStatus.UllAvailPhys + }; + } + + private static MemoryInfo GetMemoryInfoImplGeneric() + { + HardwareInfo.RefreshMemoryList(); + + return new MemoryInfo + { + TotalPhysicalBytes = HardwareInfo.MemoryStatus.TotalPhysical, + AvailablePhysicalBytes = HardwareInfo.MemoryStatus.AvailablePhysical + }; + } + + /// + /// Gets cpu info + /// + public static Task GetCpuInfoAsync() => + Compat.IsWindows ? Task.FromResult(GetCpuInfoImplWindows()) : GetCpuInfoImplGenericAsync(); + + [SupportedOSPlatform("windows")] + private static CpuInfo GetCpuInfoImplWindows() + { + var info = new CpuInfo(); + + using var processorKey = Registry + .LocalMachine + .OpenSubKey(@"Hardware\Description\System\CentralProcessor\0", RegistryKeyPermissionCheck.ReadSubTree); + + if (processorKey?.GetValue("ProcessorNameString") is string processorName) + { + info = info with { ProcessorCaption = processorName.Trim() }; + } + + return info; + } + + private static Task GetCpuInfoImplGenericAsync() + { + return Task.Run(() => + { + HardwareInfo.RefreshCPUList(); + + return new CpuInfo { ProcessorCaption = HardwareInfo.CpuList.FirstOrDefault()?.Caption.Trim() ?? "" }; + }); + } + + [SupportedOSPlatform("windows")] + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool GetPhysicallyInstalledSystemMemory(out long totalMemoryInKilobytes); + + [SupportedOSPlatform("windows")] + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool GlobalMemoryStatusEx(ref Win32MemoryStatusEx lpBuffer); +} diff --git a/StabilityMatrix.Core/Helper/HardwareInfo/MemoryInfo.cs b/StabilityMatrix.Core/Helper/HardwareInfo/MemoryInfo.cs new file mode 100644 index 00000000..821f35fd --- /dev/null +++ b/StabilityMatrix.Core/Helper/HardwareInfo/MemoryInfo.cs @@ -0,0 +1,10 @@ +namespace StabilityMatrix.Core.Helper.HardwareInfo; + +public readonly record struct MemoryInfo +{ + public ulong TotalInstalledBytes { get; init; } + + public ulong TotalPhysicalBytes { get; init; } + + public ulong AvailablePhysicalBytes { get; init; } +} diff --git a/StabilityMatrix.Core/Helper/HardwareInfo/MemoryLevel.cs b/StabilityMatrix.Core/Helper/HardwareInfo/MemoryLevel.cs new file mode 100644 index 00000000..4f66acbf --- /dev/null +++ b/StabilityMatrix.Core/Helper/HardwareInfo/MemoryLevel.cs @@ -0,0 +1,9 @@ +namespace StabilityMatrix.Core.Helper.HardwareInfo; + +public enum MemoryLevel +{ + Unknown, + Low, + Medium, + High +} diff --git a/StabilityMatrix.Core/Helper/HardwareInfo/Win32MemoryStatusEx.cs b/StabilityMatrix.Core/Helper/HardwareInfo/Win32MemoryStatusEx.cs new file mode 100644 index 00000000..13811565 --- /dev/null +++ b/StabilityMatrix.Core/Helper/HardwareInfo/Win32MemoryStatusEx.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; + +namespace StabilityMatrix.Core.Helper.HardwareInfo; + +[StructLayout(LayoutKind.Sequential)] +public struct Win32MemoryStatusEx +{ + public uint DwLength = (uint)Marshal.SizeOf(typeof(Win32MemoryStatusEx)); + public uint DwMemoryLoad = 0; + public ulong UllTotalPhys = 0; + public ulong UllAvailPhys = 0; + public ulong UllTotalPageFile = 0; + public ulong UllAvailPageFile = 0; + public ulong UllTotalVirtual = 0; + public ulong UllAvailVirtual = 0; + public ulong UllAvailExtendedVirtual = 0; + + public Win32MemoryStatusEx() { } +} diff --git a/StabilityMatrix.Core/Helper/ModelFinder.cs b/StabilityMatrix.Core/Helper/ModelFinder.cs index 001383c7..f1640821 100644 --- a/StabilityMatrix.Core/Helper/ModelFinder.cs +++ b/StabilityMatrix.Core/Helper/ModelFinder.cs @@ -1,4 +1,5 @@ -using NLog; +using System.Net; +using NLog; using Refit; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Attributes; @@ -8,11 +9,7 @@ using StabilityMatrix.Core.Models.Api; namespace StabilityMatrix.Core.Helper; // return Model, ModelVersion, ModelFile -public record struct ModelSearchResult( - CivitModel Model, - CivitModelVersion ModelVersion, - CivitFile ModelFile -); +public record struct ModelSearchResult(CivitModel Model, CivitModelVersion ModelVersion, CivitFile ModelFile); [Singleton] public class ModelFinder @@ -36,9 +33,7 @@ public class ModelFinder return null; } - var file = version.Files!.First( - file => file.Hashes.BLAKE3?.ToLowerInvariant() == hashBlake3 - ); + var file = version.Files!.First(file => file.Hashes.BLAKE3?.ToLowerInvariant() == hashBlake3); return new ModelSearchResult(model, version, file); } @@ -61,16 +56,39 @@ public class ModelFinder // VersionResponse is not actually the full data of ModelVersion, so find it again var version = model.ModelVersions!.First(version => version.Id == versionResponse.Id); - var file = versionResponse.Files.First( - file => file.Hashes.BLAKE3?.ToLowerInvariant() == hashBlake3 - ); + var file = versionResponse + .Files + .First(file => hashBlake3.Equals(file.Hashes.BLAKE3, StringComparison.OrdinalIgnoreCase)); return new ModelSearchResult(model, version, file); } + catch (TaskCanceledException e) + { + Logger.Warn( + "Timed out while finding remote model version using hash {Hash}: {Error}", + hashBlake3, + e.Message + ); + return null; + } catch (ApiException e) { - Logger.Info( - "Could not find remote model version using hash {Hash}: {Error}", + if (e.StatusCode == HttpStatusCode.NotFound) + { + Logger.Info("Could not find remote model version using hash {Hash}", hashBlake3); + } + else + { + Logger.Warn(e, "Could not find remote model version using hash {Hash}: {Error}", hashBlake3, e.Message); + } + + return null; + } + catch (HttpRequestException e) + { + Logger.Warn( + e, + "Could not connect to api while finding remote model version using hash {Hash}: {Error}", hashBlake3, e.Message ); diff --git a/StabilityMatrix.Core/Helper/Size.cs b/StabilityMatrix.Core/Helper/Size.cs index 4f31ba60..9c7a8d78 100644 --- a/StabilityMatrix.Core/Helper/Size.cs +++ b/StabilityMatrix.Core/Helper/Size.cs @@ -9,25 +9,42 @@ public static class Size public const ulong MiB = KiB * 1024; public const ulong GiB = MiB * 1024; - public static string FormatBytes(ulong bytes) + private static string TrimZero(string value) + { + return value.TrimEnd('0').TrimEnd('.'); + } + + public static string FormatBytes(ulong bytes, bool trimZero = false) { return bytes switch { - < KiB => $"{bytes} B", - < MiB => $"{bytes / (double)KiB:0.0} KiB", - < GiB => $"{bytes / (double)MiB:0.0} MiB", - _ => $"{bytes / (double)GiB:0.0} GiB" + < KiB => $"{bytes:0} Bytes", + < MiB + => (trimZero ? $"{bytes / (double)KiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)KiB:0.0}") + + " KiB", + < GiB + => (trimZero ? $"{bytes / (double)MiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)MiB:0.0}") + + " MiB", + _ + => (trimZero ? $"{bytes / (double)GiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)GiB:0.0}") + + " GiB" }; } - public static string FormatBase10Bytes(ulong bytes) + public static string FormatBase10Bytes(ulong bytes, bool trimZero = false) { return bytes switch { - < KiB => $"{bytes} Bytes", - < MiB => $"{bytes / (double)KiB:0.0} KB", - < GiB => $"{bytes / (double)MiB:0.0} MB", - _ => $"{bytes / (double)GiB:0.00} GB" + < KiB => $"{bytes:0} Bytes", + < MiB + => (trimZero ? $"{bytes / (double)KiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)KiB:0.0}") + + " KB", + < GiB + => (trimZero ? $"{bytes / (double)MiB:0.0}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)MiB:0.0}") + + " MB", + _ + => (trimZero ? $"{bytes / (double)GiB:0.00}".TrimEnd('0').TrimEnd('.') : $"{bytes / (double)GiB:0.00}") + + " GB" }; } diff --git a/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs b/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs index d327a195..8b86fc61 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/ComfySampler.cs @@ -4,11 +4,15 @@ using System.Diagnostics.CodeAnalysis; namespace StabilityMatrix.Core.Models.Api.Comfy; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +[SuppressMessage("ReSharper", "StringLiteralTypo")] +[SuppressMessage("ReSharper", "InconsistentNaming")] +[SuppressMessage("ReSharper", "IdentifierTypo")] public readonly record struct ComfySampler(string Name) { public static ComfySampler Euler { get; } = new("euler"); public static ComfySampler EulerAncestral { get; } = new("euler_ancestral"); public static ComfySampler Heun { get; } = new("heun"); + public static ComfySampler HeunPp2 { get; } = new("heunpp2"); public static ComfySampler Dpm2 { get; } = new("dpm_2"); public static ComfySampler Dpm2Ancestral { get; } = new("dpm_2_ancestral"); public static ComfySampler LMS { get; } = new("lms"); @@ -24,8 +28,10 @@ public readonly record struct ComfySampler(string Name) public static ComfySampler Dpmpp3MSde { get; } = new("dpmpp_3m_sde"); public static ComfySampler Dpmpp3MSdeGpu { get; } = new("dpmpp_3m_sde_gpu"); public static ComfySampler DDIM { get; } = new("ddim"); + public static ComfySampler DDPM { get; } = new("ddpm"); public static ComfySampler UniPC { get; } = new("uni_pc"); public static ComfySampler UniPCBh2 { get; } = new("uni_pc_bh2"); + public static ComfySampler LCM { get; } = new("lcm"); private static Dictionary ConvertDict { get; } = new() @@ -33,6 +39,7 @@ public readonly record struct ComfySampler(string Name) [Euler] = "Euler", [EulerAncestral] = "Euler Ancestral", [Heun] = "Heun", + [HeunPp2] = "Heun++ 2", [Dpm2] = "DPM 2", [Dpm2Ancestral] = "DPM 2 Ancestral", [LMS] = "LMS", @@ -48,15 +55,15 @@ public readonly record struct ComfySampler(string Name) [Dpmpp3MSde] = "DPM++ 3M SDE", [Dpmpp3MSdeGpu] = "DPM++ 3M SDE GPU", [DDIM] = "DDIM", + [DDPM] = "DDPM", [UniPC] = "UniPC", - [UniPCBh2] = "UniPC BH2" + [UniPCBh2] = "UniPC BH2", + [LCM] = "LCM" }; - public static IReadOnlyList Defaults { get; } = - ConvertDict.Keys.ToImmutableArray(); + public static IReadOnlyList Defaults { get; } = ConvertDict.Keys.ToImmutableArray(); - public string DisplayName => - ConvertDict.TryGetValue(this, out var displayName) ? displayName : Name; + public string DisplayName => ConvertDict.GetValueOrDefault(this, Name); /// public bool Equals(ComfySampler other) diff --git a/StabilityMatrix.Core/Models/Api/Comfy/ComfyScheduler.cs b/StabilityMatrix.Core/Models/Api/Comfy/ComfyScheduler.cs index 0085aceb..b08cd13d 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/ComfyScheduler.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/ComfyScheduler.cs @@ -7,23 +7,24 @@ public readonly record struct ComfyScheduler(string Name) public static ComfyScheduler Normal { get; } = new("normal"); public static ComfyScheduler Karras { get; } = new("karras"); public static ComfyScheduler Exponential { get; } = new("exponential"); + public static ComfyScheduler SDTurbo { get; } = new("sd_turbo"); private static Dictionary ConvertDict { get; } = new() { [Normal.Name] = "Normal", - ["karras"] = "Karras", - ["exponential"] = "Exponential", + [Karras.Name] = "Karras", + [Exponential.Name] = "Exponential", ["sgm_uniform"] = "SGM Uniform", ["simple"] = "Simple", - ["ddim_uniform"] = "DDIM Uniform" + ["ddim_uniform"] = "DDIM Uniform", + [SDTurbo.Name] = "SD Turbo" }; public static IReadOnlyList Defaults { get; } = ConvertDict.Keys.Select(k => new ComfyScheduler(k)).ToImmutableArray(); - public string DisplayName => - ConvertDict.TryGetValue(Name, out var displayName) ? displayName : Name; + public string DisplayName => ConvertDict.GetValueOrDefault(Name, Name); private sealed class NameEqualityComparer : IEqualityComparer { diff --git a/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscaler.cs b/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscaler.cs index 06dfbb83..411b1474 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscaler.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/ComfyUpscaler.cs @@ -8,6 +8,8 @@ namespace StabilityMatrix.Core.Models.Api.Comfy; public readonly record struct ComfyUpscaler(string Name, ComfyUpscalerType Type) { + public static ComfyUpscaler NearestExact { get; } = new("nearest-exact", ComfyUpscalerType.Latent); + private static Dictionary ConvertDict { get; } = new() { @@ -19,9 +21,7 @@ public readonly record struct ComfyUpscaler(string Name, ComfyUpscalerType Type) }; public static IReadOnlyList Defaults { get; } = - ConvertDict.Keys - .Select(k => new ComfyUpscaler(k, ComfyUpscalerType.Latent)) - .ToImmutableArray(); + ConvertDict.Keys.Select(k => new ComfyUpscaler(k, ComfyUpscalerType.Latent)).ToImmutableArray(); public static ComfyUpscaler FromDownloadable(RemoteResource resource) { @@ -110,6 +110,5 @@ public readonly record struct ComfyUpscaler(string Name, ComfyUpscalerType Type) } } - public static IEqualityComparer Comparer { get; } = - new NameTypeEqualityComparer(); + public static IEqualityComparer Comparer { get; } = new NameTypeEqualityComparer(); } diff --git a/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs index 259c99c4..c540019e 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/NodeTypes/NodeConnections.cs @@ -19,3 +19,7 @@ public class ClipNodeConnection : NodeConnectionBase { } public class ControlNetNodeConnection : NodeConnectionBase { } public class ClipVisionNodeConnection : NodeConnectionBase { } + +public class SamplerNodeConnection : NodeConnectionBase { } + +public class SigmasNodeConnection : NodeConnectionBase { } diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs index eb6bce79..6430ea7f 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/ComfyNodeBuilder.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Runtime.Serialization; using System.Text.Json.Serialization; @@ -26,9 +27,7 @@ public class ComfyNodeBuilder { if (i > 1_000_000) { - throw new InvalidOperationException( - $"Could not find unique name for base {nameBase}" - ); + throw new InvalidOperationException($"Could not find unique name for base {nameBase}"); } name = $"{nameBase}_{i + 1}"; @@ -37,38 +36,16 @@ public class ComfyNodeBuilder return name; } - public static NamedComfyNode VAEEncode( - string name, - ImageNodeConnection pixels, - VAENodeConnection vae - ) + public record VAEEncode : ComfyTypedNodeBase { - return new NamedComfyNode(name) - { - ClassType = "VAEEncode", - Inputs = new Dictionary - { - ["pixels"] = pixels.Data, - ["vae"] = vae.Data - } - }; + public required ImageNodeConnection Pixels { get; init; } + public required VAENodeConnection Vae { get; init; } } - public static NamedComfyNode VAEDecode( - string name, - LatentNodeConnection samples, - VAENodeConnection vae - ) + public record VAEDecode : ComfyTypedNodeBase { - return new NamedComfyNode(name) - { - ClassType = "VAEDecode", - Inputs = new Dictionary - { - ["samples"] = samples.Data, - ["vae"] = vae.Data - } - }; + public required LatentNodeConnection Samples { get; init; } + public required VAENodeConnection Vae { get; init; } } public record KSampler : ComfyTypedNodeBase @@ -85,39 +62,6 @@ public class ComfyNodeBuilder public required double Denoise { get; init; } } - /*public static NamedComfyNode KSampler( - string name, - ModelNodeConnection model, - ulong seed, - int steps, - double cfg, - ComfySampler sampler, - ComfyScheduler scheduler, - ConditioningNodeConnection positive, - ConditioningNodeConnection negative, - LatentNodeConnection latentImage, - double denoise - ) - { - return new NamedComfyNode(name) - { - ClassType = "KSampler", - Inputs = new Dictionary - { - ["model"] = model.Data, - ["seed"] = seed, - ["steps"] = steps, - ["cfg"] = cfg, - ["sampler_name"] = sampler.Name, - ["scheduler"] = scheduler.Name, - ["positive"] = positive.Data, - ["negative"] = negative.Data, - ["latent_image"] = latentImage.Data, - ["denoise"] = denoise - } - }; - }*/ - public record KSamplerAdvanced : ComfyTypedNodeBase { public required ModelNodeConnection Model { get; init; } @@ -139,44 +83,34 @@ public class ComfyNodeBuilder public bool ReturnWithLeftoverNoise { get; init; } } - /*public static NamedComfyNode KSamplerAdvanced( - string name, - ModelNodeConnection model, - bool addNoise, - ulong noiseSeed, - int steps, - double cfg, - ComfySampler sampler, - ComfyScheduler scheduler, - ConditioningNodeConnection positive, - ConditioningNodeConnection negative, - LatentNodeConnection latentImage, - int startAtStep, - int endAtStep, - bool returnWithLeftoverNoise - ) + public record SamplerCustom : ComfyTypedNodeBase { - return new NamedComfyNode(name) - { - ClassType = "KSamplerAdvanced", - Inputs = new Dictionary - { - ["model"] = model.Data, - ["add_noise"] = addNoise ? "enable" : "disable", - ["noise_seed"] = noiseSeed, - ["steps"] = steps, - ["cfg"] = cfg, - ["sampler_name"] = sampler.Name, - ["scheduler"] = scheduler.Name, - ["positive"] = positive.Data, - ["negative"] = negative.Data, - ["latent_image"] = latentImage.Data, - ["start_at_step"] = startAtStep, - ["end_at_step"] = endAtStep, - ["return_with_leftover_noise"] = returnWithLeftoverNoise ? "enable" : "disable" - } - }; - }*/ + public required ModelNodeConnection Model { get; init; } + public required bool AddNoise { get; init; } + public required ulong NoiseSeed { get; init; } + + [Range(0d, 100d)] + public required double Cfg { get; init; } + + public required ConditioningNodeConnection Positive { get; init; } + public required ConditioningNodeConnection Negative { get; init; } + public required SamplerNodeConnection Sampler { get; init; } + public required SigmasNodeConnection Sigmas { get; init; } + public required LatentNodeConnection LatentImage { get; init; } + } + + public record KSamplerSelect : ComfyTypedNodeBase + { + public required string SamplerName { get; init; } + } + + public record SDTurboScheduler : ComfyTypedNodeBase + { + public required ModelNodeConnection Model { get; init; } + + [Range(1, 10)] + public required int Steps { get; init; } + } public record EmptyLatentImage : ComfyTypedNodeBase { @@ -213,18 +147,11 @@ public class ComfyNodeBuilder return new NamedComfyNode(name) { ClassType = "ImageUpscaleWithModel", - Inputs = new Dictionary - { - ["upscale_model"] = upscaleModel.Data, - ["image"] = image.Data - } + Inputs = new Dictionary { ["upscale_model"] = upscaleModel.Data, ["image"] = image.Data } }; } - public static NamedComfyNode UpscaleModelLoader( - string name, - string modelName - ) + public static NamedComfyNode UpscaleModelLoader(string name, string modelName) { return new NamedComfyNode(name) { @@ -351,8 +278,7 @@ public class ComfyNodeBuilder public required string ControlNetName { get; init; } } - public record ControlNetApplyAdvanced - : ComfyTypedNodeBase + public record ControlNetApplyAdvanced : ComfyTypedNodeBase { public required ConditioningNodeConnection Positive { get; init; } public required ConditioningNodeConnection Negative { get; init; } @@ -364,11 +290,7 @@ public class ComfyNodeBuilder } public record SVD_img2vid_Conditioning - : ComfyTypedNodeBase< - ConditioningNodeConnection, - ConditioningNodeConnection, - LatentNodeConnection - > + : ComfyTypedNodeBase { public required ClipVisionNodeConnection ClipVision { get; init; } public required ImageNodeConnection InitImage { get; init; } @@ -397,22 +319,34 @@ public class ComfyNodeBuilder public required string Method { get; init; } } - public ImageNodeConnection Lambda_LatentToImage( - LatentNodeConnection latent, - VAENodeConnection vae - ) + public ImageNodeConnection Lambda_LatentToImage(LatentNodeConnection latent, VAENodeConnection vae) { var name = GetUniqueName("VAEDecode"); - return Nodes.AddNamedNode(VAEDecode(name, latent, vae)).Output; + return Nodes + .AddTypedNode( + new VAEDecode + { + Name = name, + Samples = latent, + Vae = vae + } + ) + .Output; } - public LatentNodeConnection Lambda_ImageToLatent( - ImageNodeConnection pixels, - VAENodeConnection vae - ) + public LatentNodeConnection Lambda_ImageToLatent(ImageNodeConnection pixels, VAENodeConnection vae) { var name = GetUniqueName("VAEEncode"); - return Nodes.AddNamedNode(VAEEncode(name, pixels, vae)).Output; + return Nodes + .AddTypedNode( + new VAEEncode + { + Name = name, + Pixels = pixels, + Vae = vae + } + ) + .Output; } /// @@ -424,9 +358,7 @@ public class ComfyNodeBuilder ImageNodeConnection image ) { - var modelLoader = Nodes.AddNamedNode( - UpscaleModelLoader($"{name}_UpscaleModelLoader", modelName) - ); + var modelLoader = Nodes.AddNamedNode(UpscaleModelLoader($"{name}_UpscaleModelLoader", modelName)); var upscaler = Nodes.AddNamedNode( ImageUpscaleWithModel($"{name}_ImageUpscaleWithModel", modelLoader.Output, image) @@ -469,16 +401,7 @@ public class ComfyNodeBuilder .Output, image => Nodes - .AddNamedNode( - ImageScale( - $"{name}_ImageUpscale", - image, - upscaleInfo.Name, - height, - width, - false - ) - ) + .AddNamedNode(ImageScale($"{name}_ImageUpscale", image, upscaleInfo.Name, height, width, false)) .Output ); } @@ -489,22 +412,11 @@ public class ComfyNodeBuilder var samplerImage = GetPrimaryAsImage(primary, vae); // Do group upscale - var modelUpscaler = Group_UpscaleWithModel( - $"{name}_ModelUpscale", - upscaleInfo.Name, - samplerImage - ); + var modelUpscaler = Group_UpscaleWithModel($"{name}_ModelUpscale", upscaleInfo.Name, samplerImage); // Since the model upscale is fixed to model (2x/4x), scale it again to the requested size var resizedScaled = Nodes.AddNamedNode( - ImageScale( - $"{name}_ImageScale", - modelUpscaler.Output, - "bilinear", - height, - width, - false - ) + ImageScale($"{name}_ImageScale", modelUpscaler.Output, "bilinear", height, width, false) ); return resizedScaled.Output; @@ -546,29 +458,32 @@ public class ComfyNodeBuilder if (upscaleInfo.Type == ComfyUpscalerType.ESRGAN) { // Convert to image space - var samplerImage = Nodes.AddNamedNode(VAEDecode($"{name}_VAEDecode", latent, vae)); + var samplerImage = Nodes.AddTypedNode( + new VAEDecode + { + Name = $"{name}_VAEDecode", + Samples = latent, + Vae = vae + } + ); // Do group upscale - var modelUpscaler = Group_UpscaleWithModel( - $"{name}_ModelUpscale", - upscaleInfo.Name, - samplerImage.Output - ); + var modelUpscaler = Group_UpscaleWithModel($"{name}_ModelUpscale", upscaleInfo.Name, samplerImage.Output); // Since the model upscale is fixed to model (2x/4x), scale it again to the requested size var resizedScaled = Nodes.AddNamedNode( - ImageScale( - $"{name}_ImageScale", - modelUpscaler.Output, - "bilinear", - height, - width, - false - ) + ImageScale($"{name}_ImageScale", modelUpscaler.Output, "bilinear", height, width, false) ); // Convert back to latent space - return Nodes.AddNamedNode(VAEEncode($"{name}_VAEEncode", resizedScaled.Output, vae)); + return Nodes.AddTypedNode( + new VAEEncode + { + Name = $"{name}_VAEEncode", + Pixels = resizedScaled.Output, + Vae = vae + } + ); } throw new InvalidOperationException($"Unknown upscaler type: {upscaleInfo.Type}"); @@ -604,31 +519,34 @@ public class ComfyNodeBuilder ); // Convert to image space - return Nodes.AddNamedNode(VAEDecode($"{name}_VAEDecode", latentUpscale.Output, vae)); + return Nodes.AddTypedNode( + new VAEDecode + { + Name = $"{name}_VAEDecode", + Samples = latentUpscale.Output, + Vae = vae + } + ); } if (upscaleInfo.Type == ComfyUpscalerType.ESRGAN) { // Convert to image space - var samplerImage = Nodes.AddNamedNode(VAEDecode($"{name}_VAEDecode", latent, vae)); + var samplerImage = Nodes.AddTypedNode( + new VAEDecode + { + Name = $"{name}_VAEDecode", + Samples = latent, + Vae = vae + } + ); // Do group upscale - var modelUpscaler = Group_UpscaleWithModel( - $"{name}_ModelUpscale", - upscaleInfo.Name, - samplerImage.Output - ); + var modelUpscaler = Group_UpscaleWithModel($"{name}_ModelUpscale", upscaleInfo.Name, samplerImage.Output); // Since the model upscale is fixed to model (2x/4x), scale it again to the requested size var resizedScaled = Nodes.AddNamedNode( - ImageScale( - $"{name}_ImageScale", - modelUpscaler.Output, - "bilinear", - height, - width, - false - ) + ImageScale($"{name}_ImageScale", modelUpscaler.Output, "bilinear", height, width, false) ); // No need to convert back to latent space @@ -670,22 +588,11 @@ public class ComfyNodeBuilder if (upscaleInfo.Type == ComfyUpscalerType.ESRGAN) { // Do group upscale - var modelUpscaler = Group_UpscaleWithModel( - $"{name}_ModelUpscale", - upscaleInfo.Name, - image - ); + var modelUpscaler = Group_UpscaleWithModel($"{name}_ModelUpscale", upscaleInfo.Name, image); // Since the model upscale is fixed to model (2x/4x), scale it again to the requested size var resizedScaled = Nodes.AddNamedNode( - ImageScale( - $"{name}_ImageScale", - modelUpscaler.Output, - "bilinear", - height, - width, - false - ) + ImageScale($"{name}_ImageScale", modelUpscaler.Output, "bilinear", height, width, false) ); // No need to convert back to latent space @@ -780,10 +687,7 @@ public class ComfyNodeBuilder /// /// Get or convert latest primary connection to latent /// - public LatentNodeConnection GetPrimaryAsLatent( - PrimaryNodeConnection primary, - VAENodeConnection vae - ) + public LatentNodeConnection GetPrimaryAsLatent(PrimaryNodeConnection primary, VAENodeConnection vae) { return primary.Match(latent => latent, image => Lambda_ImageToLatent(image, vae)); } @@ -823,10 +727,7 @@ public class ComfyNodeBuilder /// /// Get or convert latest primary connection to image /// - public ImageNodeConnection GetPrimaryAsImage( - PrimaryNodeConnection primary, - VAENodeConnection vae - ) + public ImageNodeConnection GetPrimaryAsImage(PrimaryNodeConnection primary, VAENodeConnection vae) { return primary.Match(latent => Lambda_LatentToImage(latent, vae), image => image); } @@ -841,10 +742,7 @@ public class ComfyNodeBuilder return Connections.Primary.AsT1; } - return GetPrimaryAsImage( - Connections.Primary ?? throw new NullReferenceException("No primary connection"), - vae - ); + return GetPrimaryAsImage(Connections.Primary ?? throw new NullReferenceException("No primary connection"), vae); } /// @@ -860,6 +758,9 @@ public class ComfyNodeBuilder { public ulong Seed { get; set; } + 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; } @@ -892,9 +793,7 @@ public class ComfyNodeBuilder public ConditioningNodeConnection GetRefinerOrBaseConditioning() { - return RefinerConditioning - ?? BaseConditioning - ?? throw new NullReferenceException("No Conditioning"); + return RefinerConditioning ?? BaseConditioning ?? throw new NullReferenceException("No Conditioning"); } public ConditioningNodeConnection GetRefinerOrBaseNegativeConditioning() @@ -906,10 +805,7 @@ public class ComfyNodeBuilder public VAENodeConnection GetDefaultVAE() { - return PrimaryVAE - ?? RefinerVAE - ?? BaseVAE - ?? throw new NullReferenceException("No VAE"); + return PrimaryVAE ?? RefinerVAE ?? BaseVAE ?? throw new NullReferenceException("No VAE"); } } diff --git a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs index e15343b3..745abb74 100644 --- a/StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs +++ b/StabilityMatrix.Core/Models/Api/Comfy/Nodes/NodeDictionary.cs @@ -5,23 +5,32 @@ namespace StabilityMatrix.Core.Models.Api.Comfy.Nodes; public class NodeDictionary : Dictionary { + /// + /// Tracks base names and their highest index resulting from + /// + private readonly Dictionary _baseNameIndex = new(); + + /// + /// Finds a unique node name given a base name, by appending _2, _3, etc. + /// public string GetUniqueName(string nameBase) { - var name = nameBase; - - for (var i = 0; ContainsKey(name); i++) + if (_baseNameIndex.TryGetValue(nameBase, out var index)) { - if (i > 1_000_000) - { - throw new InvalidOperationException( - $"Could not find unique name for base {nameBase}" - ); - } + var newIndex = checked(index + 1); + _baseNameIndex[nameBase] = newIndex; + return $"{nameBase}_{newIndex}"; + } - name = $"{nameBase}_{i + 2}"; + // Ensure new name does not exist + if (ContainsKey(nameBase)) + { + throw new InvalidOperationException($"Initial unique name already exists for base {nameBase}"); } - return name; + _baseNameIndex.Add(nameBase, 1); + + return nameBase; } public TNamedNode AddNamedNode(TNamedNode node) diff --git a/StabilityMatrix.Core/Models/ConnectedModelInfo.cs b/StabilityMatrix.Core/Models/ConnectedModelInfo.cs index 26454e21..f9c1f8d0 100644 --- a/StabilityMatrix.Core/Models/ConnectedModelInfo.cs +++ b/StabilityMatrix.Core/Models/ConnectedModelInfo.cs @@ -23,6 +23,7 @@ public class ConnectedModelInfo public DateTimeOffset ImportedAt { get; set; } public CivitFileHashes Hashes { get; set; } public string[]? TrainedWords { get; set; } + public CivitModelStats Stats { get; set; } // User settings public string? UserTitle { get; set; } @@ -39,7 +40,7 @@ public class ConnectedModelInfo { ModelId = civitModel.Id; ModelName = civitModel.Name; - ModelDescription = civitModel.Description; + ModelDescription = civitModel.Description ?? string.Empty; Nsfw = civitModel.Nsfw; Tags = civitModel.Tags; ModelType = civitModel.Type; @@ -51,16 +52,14 @@ public class ConnectedModelInfo FileMetadata = civitFile.Metadata; Hashes = civitFile.Hashes; TrainedWords = civitModelVersion.TrainedWords; + Stats = civitModel.Stats; } public static ConnectedModelInfo? FromJson(string json) { return JsonSerializer.Deserialize( json, - new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - } + new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull } ); } @@ -78,6 +77,5 @@ public class ConnectedModelInfo } [JsonIgnore] - public string TrainedWordsString => - TrainedWords != null ? string.Join(", ", TrainedWords) : string.Empty; + public string TrainedWordsString => TrainedWords != null ? string.Join(", ", TrainedWords) : string.Empty; } diff --git a/StabilityMatrix.Core/Models/Database/LocalModelFile.cs b/StabilityMatrix.Core/Models/Database/LocalModelFile.cs index a5de912b..3fae7841 100644 --- a/StabilityMatrix.Core/Models/Database/LocalModelFile.cs +++ b/StabilityMatrix.Core/Models/Database/LocalModelFile.cs @@ -42,8 +42,7 @@ public class LocalModelFile /// /// Relative file path from the shared folder type model directory. /// - public string RelativePathFromSharedFolder => - Path.GetRelativePath(SharedFolderType.GetStringValue(), RelativePath); + public string RelativePathFromSharedFolder => Path.GetRelativePath(SharedFolderType.GetStringValue(), RelativePath); public string GetFullPath(string rootModelDirectory) { @@ -52,15 +51,12 @@ public class LocalModelFile public string? GetPreviewImageFullPath(string rootModelDirectory) { - return PreviewImageRelativePath == null - ? null - : Path.Combine(rootModelDirectory, PreviewImageRelativePath); + return PreviewImageRelativePath == null ? null : Path.Combine(rootModelDirectory, PreviewImageRelativePath); } public string FullPathGlobal => GetFullPath(GlobalConfig.LibraryDir.JoinDir("Models")); - public string? PreviewImageFullPathGlobal => - GetPreviewImageFullPath(GlobalConfig.LibraryDir.JoinDir("Models")); + public string? PreviewImageFullPathGlobal => GetPreviewImageFullPath(GlobalConfig.LibraryDir.JoinDir("Models")); protected bool Equals(LocalModelFile other) { @@ -86,8 +82,13 @@ public class LocalModelFile } public static readonly HashSet SupportedCheckpointExtensions = - new() { ".safetensors", ".pt", ".ckpt", ".pth", ".bin" }; - public static readonly HashSet SupportedImageExtensions = - new() { ".png", ".jpg", ".jpeg" }; - public static readonly HashSet SupportedMetadataExtensions = new() { ".json" }; + [ + ".safetensors", + ".pt", + ".ckpt", + ".pth", + ".bin" + ]; + public static readonly HashSet SupportedImageExtensions = [".png", ".jpg", ".jpeg", ".gif"]; + public static readonly HashSet SupportedMetadataExtensions = [".json"]; } diff --git a/StabilityMatrix.Core/Models/FileInterfaces/FileSystemPath.cs b/StabilityMatrix.Core/Models/FileInterfaces/FileSystemPath.cs index e9d22fa2..8fc04491 100644 --- a/StabilityMatrix.Core/Models/FileInterfaces/FileSystemPath.cs +++ b/StabilityMatrix.Core/Models/FileInterfaces/FileSystemPath.cs @@ -1,6 +1,6 @@ namespace StabilityMatrix.Core.Models.FileInterfaces; -public class FileSystemPath : IEquatable, IEquatable +public class FileSystemPath : IEquatable, IEquatable, IFormattable { public string FullPath { get; } @@ -8,27 +8,42 @@ public class FileSystemPath : IEquatable, IEquatable { FullPath = path; } - - protected FileSystemPath(FileSystemPath path) : this(path.FullPath) + + protected FileSystemPath(FileSystemPath path) + : this(path.FullPath) { } + + protected FileSystemPath(params string[] paths) + : this(Path.Combine(paths)) { } + + public override string ToString() { + return FullPath; } - protected FileSystemPath(params string[] paths) : this(Path.Combine(paths)) + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) { + return ToString(format, formatProvider); } - - public override string ToString() + + /// + /// Overridable IFormattable.ToString method. + /// By default, returns . + /// + protected virtual string ToString(string? format, IFormatProvider? formatProvider) { return FullPath; } public bool Equals(FileSystemPath? other) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; return FullPath == other.FullPath; } - + public bool Equals(string? other) { return string.Equals(FullPath, other); @@ -48,8 +63,9 @@ public class FileSystemPath : IEquatable, IEquatable { return FullPath.GetHashCode(); } - + // Implicit conversions to and from string public static implicit operator string(FileSystemPath path) => path.FullPath; + public static implicit operator FileSystemPath(string path) => new(path); } diff --git a/StabilityMatrix.Core/Models/GlobalConfig.cs b/StabilityMatrix.Core/Models/GlobalConfig.cs index f932142b..15100294 100644 --- a/StabilityMatrix.Core/Models/GlobalConfig.cs +++ b/StabilityMatrix.Core/Models/GlobalConfig.cs @@ -7,7 +7,7 @@ namespace StabilityMatrix.Core.Models; public static class GlobalConfig { private static DirectoryPath? libraryDir; - + /// /// Absolute path to the library directory. /// Needs to be set by SettingsManager.TryFindLibrary() before being accessed. @@ -19,23 +19,23 @@ public static class GlobalConfig { if (libraryDir is null) { - throw new NullReferenceException( - "GlobalConfig.LibraryDir was not set before being accessed."); + throw new NullReferenceException("GlobalConfig.LibraryDir was not set before being accessed."); } return libraryDir; } set => libraryDir = value; } - + /// /// Full path to the %APPDATA% directory. /// Usually C:\Users\{username}\AppData\Roaming /// - public static DirectoryPath AppDataDir { get; } = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - + public static DirectoryPath AppDataDir { get; } = + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + /// /// Full path to the fixed home directory. /// Currently %APPDATA%\StabilityMatrix /// - public static DirectoryPath HomeDir { get; } = AppDataDir.JoinDir("StabilityMatrix"); + public static DirectoryPath HomeDir { get; set; } = AppDataDir.JoinDir("StabilityMatrix"); } diff --git a/StabilityMatrix.Core/Models/InstalledPackage.cs b/StabilityMatrix.Core/Models/InstalledPackage.cs index 2d202675..8f2d5c33 100644 --- a/StabilityMatrix.Core/Models/InstalledPackage.cs +++ b/StabilityMatrix.Core/Models/InstalledPackage.cs @@ -48,8 +48,13 @@ public class InstalledPackage : IJsonOnDeserialized public List? LaunchArgs { get; set; } public DateTimeOffset? LastUpdateCheck { get; set; } public bool UpdateAvailable { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] public TorchVersion? PreferredTorchVersion { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] public SharedFolderMethod? PreferredSharedFolderMethod { get; set; } + public bool UseSharedOutputFolder { get; set; } /// @@ -188,10 +193,7 @@ public class InstalledPackage : IJsonOnDeserialized var suffix = 2; while (Directory.Exists(newPackagePath)) { - newPackagePath = System.IO.Path.Combine( - newPackagesDir, - $"{packageFolderName}-{suffix}" - ); + newPackagePath = System.IO.Path.Combine(newPackagesDir, $"{packageFolderName}-{suffix}"); suffix++; } @@ -234,16 +236,10 @@ public class InstalledPackage : IJsonOnDeserialized if (Version != null) return; - if ( - string.IsNullOrWhiteSpace(InstalledBranch) && !string.IsNullOrWhiteSpace(PackageVersion) - ) + if (string.IsNullOrWhiteSpace(InstalledBranch) && !string.IsNullOrWhiteSpace(PackageVersion)) { // release mode - Version = new InstalledPackageVersion - { - InstalledReleaseVersion = PackageVersion, - IsPrerelease = false - }; + Version = new InstalledPackageVersion { InstalledReleaseVersion = PackageVersion, IsPrerelease = false }; } else if (!string.IsNullOrWhiteSpace(PackageVersion)) { diff --git a/StabilityMatrix.Core/Models/LaunchOptionType.cs b/StabilityMatrix.Core/Models/LaunchOptionType.cs index 4ba00501..86904965 100644 --- a/StabilityMatrix.Core/Models/LaunchOptionType.cs +++ b/StabilityMatrix.Core/Models/LaunchOptionType.cs @@ -1,8 +1,11 @@ -namespace StabilityMatrix.Core.Models; +using System.Text.Json.Serialization; +namespace StabilityMatrix.Core.Models; + +[JsonConverter(typeof(JsonStringEnumConverter))] public enum LaunchOptionType { Bool, String, - Int, + Int } diff --git a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs index bad8db64..6f3efd8d 100644 --- a/StabilityMatrix.Core/Models/Packages/A3WebUI.cs +++ b/StabilityMatrix.Core/Models/Packages/A3WebUI.cs @@ -5,6 +5,7 @@ using NLog; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; @@ -14,7 +15,12 @@ using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [Singleton(typeof(BasePackage))] -public class A3WebUI : BaseGitPackage +public class A3WebUI( + IGithubApiCache githubApi, + ISettingsManager settingsManager, + IDownloadService downloadService, + IPrerequisiteHelper prerequisiteHelper +) : BaseGitPackage(githubApi, settingsManager, downloadService, prerequisiteHelper) { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); @@ -24,8 +30,7 @@ public class A3WebUI : BaseGitPackage public override string LicenseType => "AGPL-3.0"; public override string LicenseUrl => "https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/master/LICENSE.txt"; - public override string Blurb => - "A browser interface based on Gradio library for Stable Diffusion"; + public override string Blurb => "A browser interface based on Gradio library for Stable Diffusion"; public override string LaunchCommand => "launch.py"; public override Uri PreviewImageUri => new("https://github.com/AUTOMATIC1111/stable-diffusion-webui/raw/master/screenshot.png"); @@ -35,14 +40,6 @@ public class A3WebUI : BaseGitPackage public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; - public A3WebUI( - IGithubApiCache githubApi, - ISettingsManager settingsManager, - IDownloadService downloadService, - IPrerequisiteHelper prerequisiteHelper - ) - : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { } - // From https://github.com/AUTOMATIC1111/stable-diffusion-webui/tree/master/models public override Dictionary> SharedFolders => new() @@ -59,10 +56,14 @@ public class A3WebUI : BaseGitPackage [SharedFolderType.Karlo] = new[] { "models/karlo" }, [SharedFolderType.TextualInversion] = new[] { "embeddings" }, [SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }, - [SharedFolderType.ControlNet] = new[] { "models/ControlNet" }, + [SharedFolderType.ControlNet] = new[] { "models/controlnet/ControlNet" }, [SharedFolderType.Codeformer] = new[] { "models/Codeformer" }, [SharedFolderType.LDSR] = new[] { "models/LDSR" }, - [SharedFolderType.AfterDetailer] = new[] { "models/adetailer" } + [SharedFolderType.AfterDetailer] = new[] { "models/adetailer" }, + [SharedFolderType.T2IAdapter] = new[] { "models/controlnet/T2IAdapter" }, + [SharedFolderType.IpAdapter] = new[] { "models/controlnet/IpAdapter" }, + [SharedFolderType.InvokeIpAdapters15] = new[] { "models/controlnet/DiffusersIpAdapters" }, + [SharedFolderType.InvokeIpAdaptersXl] = new[] { "models/controlnet/DiffusersIpAdaptersXL" } }; public override Dictionary>? SharedOutputFolders => @@ -78,71 +79,67 @@ public class A3WebUI : BaseGitPackage [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")] public override List LaunchOptions => - new() - { + [ new() { Name = "Host", Type = LaunchOptionType.String, DefaultValue = "localhost", - Options = new() { "--server-name" } + Options = ["--server-name"] }, new() { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "7860", - Options = new() { "--port" } + Options = ["--port"] }, new() { Name = "VRAM", Type = LaunchOptionType.Bool, - InitialValue = HardwareHelper - .IterGpuInfo() - .Select(gpu => gpu.MemoryLevel) - .Max() switch + InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch { - Level.Low => "--lowvram", - Level.Medium => "--medvram", + MemoryLevel.Low => "--lowvram", + MemoryLevel.Medium => "--medvram", _ => null }, - Options = new() { "--lowvram", "--medvram", "--medvram-sdxl" } + Options = ["--lowvram", "--medvram", "--medvram-sdxl"] }, new() { Name = "Xformers", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.HasNvidiaGpu(), - Options = new() { "--xformers" } + Options = ["--xformers"] }, new() { Name = "API", Type = LaunchOptionType.Bool, InitialValue = true, - Options = new() { "--api" } + Options = ["--api"] }, new() { Name = "Auto Launch Web UI", Type = LaunchOptionType.Bool, InitialValue = false, - Options = new() { "--autolaunch" } + Options = ["--autolaunch"] }, new() { Name = "Skip Torch CUDA Check", Type = LaunchOptionType.Bool, InitialValue = !HardwareHelper.HasNvidiaGpu(), - Options = new() { "--skip-torch-cuda-test" } + Options = ["--skip-torch-cuda-test"] }, new() { Name = "Skip Python Version Check", Type = LaunchOptionType.Bool, InitialValue = true, - Options = new() { "--skip-python-version-check" } + Options = ["--skip-python-version-check"] }, new() { @@ -150,23 +147,23 @@ public class A3WebUI : BaseGitPackage Type = LaunchOptionType.Bool, Description = "Do not switch the model to 16-bit floats", InitialValue = HardwareHelper.PreferRocm() || HardwareHelper.PreferDirectML(), - Options = new() { "--no-half" } + Options = ["--no-half"] }, new() { Name = "Skip SD Model Download", Type = LaunchOptionType.Bool, InitialValue = false, - Options = new() { "--no-download-sd-model" } + Options = ["--no-download-sd-model"] }, new() { Name = "Skip Install", Type = LaunchOptionType.Bool, - Options = new() { "--skip-install" } + Options = ["--skip-install"] }, LaunchOptionDefinition.Extras - }; + ]; public override IEnumerable AvailableSharedFolderMethods => new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None }; @@ -188,41 +185,44 @@ public class A3WebUI : BaseGitPackage ) { progress?.Report(new ProgressReport(-1f, "Setting up venv", isIndeterminate: true)); - // Setup venv - await using var venvRunner = new PyVenvRunner(Path.Combine(installLocation, "venv")); - venvRunner.WorkingDirectory = installLocation; - await venvRunner.Setup(true, onConsoleOutput).ConfigureAwait(false); - switch (torchVersion) + var venvRunner = await SetupVenv(installLocation, forceRecreate: true).ConfigureAwait(false); + + await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); + + progress?.Report(new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true)); + + var requirements = new FilePath(installLocation, "requirements_versions.txt"); + + var pipArgs = new PipInstallArgs() + .WithTorch("==2.0.1") + .WithTorchVision("==0.15.2") + .WithTorchExtraIndex( + torchVersion switch + { + TorchVersion.Cpu => "cpu", + TorchVersion.Cuda => "cu118", + TorchVersion.Rocm => "rocm5.1.1", + _ => throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null) + } + ) + .WithParsedFromRequirementsTxt( + await requirements.ReadAllTextAsync().ConfigureAwait(false), + excludePattern: "torch" + ); + + if (torchVersion == TorchVersion.Cuda) { - case TorchVersion.Cpu: - await InstallCpuTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); - break; - case TorchVersion.Cuda: - await InstallCudaTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); - break; - case TorchVersion.Rocm: - await InstallRocmTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); - break; - default: - throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null); + pipArgs = pipArgs.WithXFormers("==0.0.20"); } + // v1.6.0 needs a httpx qualifier to fix a gradio issue if (versionOptions.VersionTag?.Contains("1.6.0") ?? false) { - await venvRunner.PipInstall("httpx==0.24.1", onConsoleOutput); + pipArgs = pipArgs.AddArg("httpx==0.24.1"); } - // Install requirements file - progress?.Report( - new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true) - ); - Logger.Info("Installing requirements_versions.txt"); - - var requirements = new FilePath(installLocation, "requirements_versions.txt"); - await venvRunner - .PipInstallFromRequirements(requirements, onConsoleOutput, excludes: "torch") - .ConfigureAwait(false); + await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Updating configuration", isIndeterminate: true)); @@ -268,26 +268,22 @@ public class A3WebUI : BaseGitPackage VenvRunner.RunDetached(args.TrimEnd(), HandleConsoleOutput, OnExit); } - private async Task InstallRocmTorch( - PyVenvRunner venvRunner, - IProgress? progress = null, - Action? onConsoleOutput = null - ) + /// + public override async Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) { - progress?.Report( - new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true) - ); - - await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); + // Migration for `controlnet` -> `controlnet/ControlNet` and `controlnet/T2IAdapter` + // If the original link exists, delete it first + if (installDirectory.JoinDir("models/controlnet") is { IsSymbolicLink: true } controlnetOldLink) + { + Logger.Info("Migration: Removing old controlnet link {Path}", controlnetOldLink); + await controlnetOldLink.DeleteAsync(false).ConfigureAwait(false); + } - await venvRunner - .PipInstall( - new PipInstallArgs() - .WithTorch("==2.0.1") - .WithTorchVision() - .WithTorchExtraIndex("rocm5.1.1"), - onConsoleOutput - ) - .ConfigureAwait(false); + // Resume base setup + await base.SetupModelFolders(installDirectory, sharedFolderMethod).ConfigureAwait(false); } + + /// + public override Task UpdateModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) => + SetupModelFolders(installDirectory, sharedFolderMethod); } diff --git a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs index e2686bb2..e22082b1 100644 --- a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs @@ -36,8 +36,7 @@ public abstract class BaseGitPackage : BasePackage public override string GithubUrl => $"https://github.com/{Author}/{Name}"; - public string DownloadLocation => - Path.Combine(SettingsManager.LibraryDir, "Packages", $"{Name}.zip"); + public string DownloadLocation => Path.Combine(SettingsManager.LibraryDir, "Packages", $"{Name}.zip"); protected string GetDownloadUrl(DownloadPackageVersionOptions versionOptions) { @@ -72,9 +71,7 @@ public abstract class BaseGitPackage : BasePackage PrerequisiteHelper = prerequisiteHelper; } - public override async Task GetLatestVersion( - bool includePrerelease = false - ) + public override async Task GetLatestVersion(bool includePrerelease = false) { if (ShouldIgnoreReleases) { @@ -87,9 +84,7 @@ public abstract class BaseGitPackage : BasePackage } var releases = await GithubApi.GetAllReleases(Author, Name).ConfigureAwait(false); - var latestRelease = includePrerelease - ? releases.First() - : releases.First(x => !x.Prerelease); + var latestRelease = includePrerelease ? releases.First() : releases.First(x => !x.Prerelease); return new DownloadPackageVersionOptions { @@ -99,11 +94,7 @@ public abstract class BaseGitPackage : BasePackage }; } - public override Task?> GetAllCommits( - string branch, - int page = 1, - int perPage = 10 - ) + public override Task?> GetAllCommits(string branch, int page = 1, int perPage = 10) { return GithubApi.GetAllCommits(Author, Name, branch, page, perPage); } @@ -223,10 +214,7 @@ public abstract class BaseGitPackage : BasePackage zipDirName = entry.FullName; } - var folderPath = Path.Combine( - installLocation, - entry.FullName.Replace(zipDirName, string.Empty) - ); + var folderPath = Path.Combine(installLocation, entry.FullName.Replace(zipDirName, string.Empty)); Directory.CreateDirectory(folderPath); continue; } @@ -237,10 +225,7 @@ public abstract class BaseGitPackage : BasePackage entry.ExtractToFile(destinationPath, true); progress?.Report( - new ProgressReport( - current: Convert.ToUInt64(currentEntry), - total: Convert.ToUInt64(totalEntries) - ) + new ProgressReport(current: Convert.ToUInt64(currentEntry), total: Convert.ToUInt64(totalEntries)) ); } @@ -264,16 +249,12 @@ public abstract class BaseGitPackage : BasePackage { if (currentVersion.IsReleaseMode) { - var latestVersion = await GetLatestVersion(currentVersion.IsPrerelease) - .ConfigureAwait(false); - UpdateAvailable = - latestVersion.VersionTag != currentVersion.InstalledReleaseVersion; + var latestVersion = await GetLatestVersion(currentVersion.IsPrerelease).ConfigureAwait(false); + UpdateAvailable = latestVersion.VersionTag != currentVersion.InstalledReleaseVersion; return UpdateAvailable; } - var allCommits = ( - await GetAllCommits(currentVersion.InstalledBranch!).ConfigureAwait(false) - )?.ToList(); + var allCommits = (await GetAllCommits(currentVersion.InstalledBranch!).ConfigureAwait(false))?.ToList(); if (allCommits == null || !allCommits.Any()) { @@ -305,18 +286,10 @@ public abstract class BaseGitPackage : BasePackage if (!Directory.Exists(Path.Combine(installedPackage.FullPath!, ".git"))) { Logger.Info("not a git repo, initializing..."); - progress?.Report( - new ProgressReport(-1f, "Initializing git repo", isIndeterminate: true) - ); + progress?.Report(new ProgressReport(-1f, "Initializing git repo", isIndeterminate: true)); + await PrerequisiteHelper.RunGit("init", onConsoleOutput, installedPackage.FullPath).ConfigureAwait(false); await PrerequisiteHelper - .RunGit(installedPackage.FullPath!, onConsoleOutput, "init") - .ConfigureAwait(false); - await PrerequisiteHelper - .RunGit( - new[] { "remote", "add", "origin", GithubUrl }, - onConsoleOutput, - installedPackage.FullPath - ) + .RunGit(new[] { "remote", "add", "origin", GithubUrl }, onConsoleOutput, installedPackage.FullPath) .ConfigureAwait(false); } @@ -328,11 +301,7 @@ public abstract class BaseGitPackage : BasePackage .ConfigureAwait(false); progress?.Report( - new ProgressReport( - -1f, - $"Checking out {versionOptions.VersionTag}", - isIndeterminate: true - ) + new ProgressReport(-1f, $"Checking out {versionOptions.VersionTag}", isIndeterminate: true) ); await PrerequisiteHelper .RunGit( @@ -361,9 +330,7 @@ public abstract class BaseGitPackage : BasePackage // fetch progress?.Report(new ProgressReport(-1f, "Fetching data...", isIndeterminate: true)); - await PrerequisiteHelper - .RunGit("fetch", onConsoleOutput, installedPackage.FullPath) - .ConfigureAwait(false); + await PrerequisiteHelper.RunGit("fetch", onConsoleOutput, installedPackage.FullPath).ConfigureAwait(false); if (versionOptions.IsLatest) { @@ -387,13 +354,7 @@ public abstract class BaseGitPackage : BasePackage progress?.Report(new ProgressReport(-1f, "Pulling changes...", isIndeterminate: true)); await PrerequisiteHelper .RunGit( - new[] - { - "pull", - "--autostash", - "origin", - installedPackage.Version.InstalledBranch! - }, + new[] { "pull", "--autostash", "origin", installedPackage.Version.InstalledBranch! }, onConsoleOutput, installedPackage.FullPath! ) @@ -436,51 +397,39 @@ public abstract class BaseGitPackage : BasePackage }; } - public override Task SetupModelFolders( - DirectoryPath installDirectory, - SharedFolderMethod sharedFolderMethod - ) + public override Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) { if (sharedFolderMethod == SharedFolderMethod.Symlink && SharedFolders is { } folders) { - return StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage( - folders, - SettingsManager.ModelsDirectory, - installDirectory - ); + return StabilityMatrix + .Core + .Helper + .SharedFolders + .UpdateLinksForPackage(folders, SettingsManager.ModelsDirectory, installDirectory); } return Task.CompletedTask; } - public override Task UpdateModelFolders( - DirectoryPath installDirectory, - SharedFolderMethod sharedFolderMethod - ) + public override Task UpdateModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) { if (sharedFolderMethod == SharedFolderMethod.Symlink && SharedFolders is { } sharedFolders) { - return StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage( - sharedFolders, - SettingsManager.ModelsDirectory, - installDirectory - ); + return StabilityMatrix + .Core + .Helper + .SharedFolders + .UpdateLinksForPackage(sharedFolders, SettingsManager.ModelsDirectory, installDirectory); } return Task.CompletedTask; } - public override Task RemoveModelFolderLinks( - DirectoryPath installDirectory, - SharedFolderMethod sharedFolderMethod - ) + public override Task RemoveModelFolderLinks(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) { if (SharedFolders is not null && sharedFolderMethod == SharedFolderMethod.Symlink) { - StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage( - SharedFolders, - installDirectory - ); + StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(SharedFolders, installDirectory); } return Task.CompletedTask; } @@ -489,12 +438,16 @@ public abstract class BaseGitPackage : BasePackage { if (SharedOutputFolders is { } sharedOutputFolders) { - return StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage( - sharedOutputFolders, - SettingsManager.ImagesDirectory, - installDirectory, - recursiveDelete: true - ); + return StabilityMatrix + .Core + .Helper + .SharedFolders + .UpdateLinksForPackage( + sharedOutputFolders, + SettingsManager.ImagesDirectory, + installDirectory, + recursiveDelete: true + ); } return Task.CompletedTask; @@ -504,10 +457,7 @@ public abstract class BaseGitPackage : BasePackage { if (SharedOutputFolders is { } sharedOutputFolders) { - StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage( - sharedOutputFolders, - installDirectory - ); + StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(sharedOutputFolders, installDirectory); } return Task.CompletedTask; } diff --git a/StabilityMatrix.Core/Models/Packages/BasePackage.cs b/StabilityMatrix.Core/Models/Packages/BasePackage.cs index 29ff06b9..f0f0a22a 100644 --- a/StabilityMatrix.Core/Models/Packages/BasePackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BasePackage.cs @@ -1,5 +1,6 @@ using Octokit; using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; @@ -80,29 +81,15 @@ public abstract class BasePackage ); public virtual IEnumerable AvailableSharedFolderMethods => - new[] - { - SharedFolderMethod.Symlink, - SharedFolderMethod.Configuration, - SharedFolderMethod.None - }; + new[] { SharedFolderMethod.Symlink, SharedFolderMethod.Configuration, SharedFolderMethod.None }; 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); @@ -125,14 +112,16 @@ public abstract class BasePackage return TorchVersion.Rocm; } - if ( - HardwareHelper.PreferDirectML() - && AvailableTorchVersions.Contains(TorchVersion.DirectMl) - ) + if (HardwareHelper.PreferDirectML() && AvailableTorchVersions.Contains(TorchVersion.DirectMl)) { return TorchVersion.DirectMl; } + if (Compat.IsMacOS && Compat.IsArm && AvailableTorchVersions.Contains(TorchVersion.Mps)) + { + return TorchVersion.Mps; + } + return TorchVersion.Cpu; } @@ -155,20 +144,11 @@ public abstract class BasePackage /// Mapping of to the relative paths from the package root. /// public abstract Dictionary>? SharedFolders { get; } - public abstract Dictionary< - SharedOutputType, - IReadOnlyList - >? SharedOutputFolders { get; } + public abstract Dictionary>? SharedOutputFolders { get; } public abstract Task GetAllVersionOptions(); - public abstract Task?> GetAllCommits( - string branch, - int page = 1, - int perPage = 10 - ); - public abstract Task GetLatestVersion( - bool includePrerelease = false - ); + 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; public event EventHandler? StartupComplete; @@ -178,9 +158,7 @@ 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, @@ -188,9 +166,7 @@ public abstract class BasePackage Action? onConsoleOutput = null ) { - progress?.Report( - new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true) - ); + progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true)); await venvRunner .PipInstall( @@ -210,9 +186,7 @@ public abstract class BasePackage Action? onConsoleOutput = null ) { - progress?.Report( - new ProgressReport(-1f, "Installing PyTorch for DirectML", isIndeterminate: true) - ); + progress?.Report(new ProgressReport(-1f, "Installing PyTorch for DirectML", isIndeterminate: true)); return venvRunner.PipInstall(new PipInstallArgs().WithTorchDirectML(), onConsoleOutput); } @@ -223,13 +197,8 @@ public abstract class BasePackage Action? onConsoleOutput = null ) { - progress?.Report( - new ProgressReport(-1f, "Installing PyTorch for CPU", isIndeterminate: true) - ); - - return venvRunner.PipInstall( - new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision(), - onConsoleOutput - ); + progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CPU", isIndeterminate: true)); + + return venvRunner.PipInstall(new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision(), onConsoleOutput); } } diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index c7a824b7..1b221a62 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -4,11 +4,13 @@ using NLog; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; +using YamlDotNet.Core; using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -16,15 +18,19 @@ using YamlDotNet.Serialization.NamingConventions; namespace StabilityMatrix.Core.Models.Packages; [Singleton(typeof(BasePackage))] -public class ComfyUI : BaseGitPackage +public class ComfyUI( + IGithubApiCache githubApi, + ISettingsManager settingsManager, + IDownloadService downloadService, + IPrerequisiteHelper prerequisiteHelper +) : BaseGitPackage(githubApi, settingsManager, downloadService, prerequisiteHelper) { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); public override string Name => "ComfyUI"; public override string DisplayName { get; set; } = "ComfyUI"; public override string Author => "comfyanonymous"; public override string LicenseType => "GPL-3.0"; - public override string LicenseUrl => - "https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE"; + public override string LicenseUrl => "https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE"; public override string Blurb => "A powerful and modular stable diffusion GUI and backend"; public override string LaunchCommand => "main.py"; @@ -35,16 +41,7 @@ public class ComfyUI : BaseGitPackage public override string OutputFolderName => "output"; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Advanced; - public override SharedFolderMethod RecommendedSharedFolderMethod => - SharedFolderMethod.Configuration; - - public ComfyUI( - IGithubApiCache githubApi, - ISettingsManager settingsManager, - IDownloadService downloadService, - IPrerequisiteHelper prerequisiteHelper - ) - : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { } + public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration; // https://github.com/comfyanonymous/ComfyUI/blob/master/folder_paths.py#L11 public override Dictionary> SharedFolders => @@ -57,106 +54,92 @@ public class ComfyUI : BaseGitPackage [SharedFolderType.TextualInversion] = new[] { "models/embeddings" }, [SharedFolderType.VAE] = new[] { "models/vae" }, [SharedFolderType.ApproxVAE] = new[] { "models/vae_approx" }, - [SharedFolderType.ControlNet] = new[] { "models/controlnet" }, + [SharedFolderType.ControlNet] = new[] { "models/controlnet/ControlNet" }, [SharedFolderType.GLIGEN] = new[] { "models/gligen" }, [SharedFolderType.ESRGAN] = new[] { "models/upscale_models" }, [SharedFolderType.Hypernetwork] = new[] { "models/hypernetworks" }, + [SharedFolderType.IpAdapter] = new[] { "models/ipadapter" }, + [SharedFolderType.T2IAdapter] = new[] { "models/controlnet/T2IAdapter" }, }; public override Dictionary>? SharedOutputFolders => new() { [SharedOutputType.Text2Img] = new[] { "output" } }; public override List LaunchOptions => - new List - { - new() + [ + new LaunchOptionDefinition { Name = "Host", Type = LaunchOptionType.String, DefaultValue = "127.0.0.1", - Options = { "--listen" } + Options = ["--listen"] }, - new() + new LaunchOptionDefinition { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "8188", - Options = { "--port" } + Options = ["--port"] }, - new() + new LaunchOptionDefinition { Name = "VRAM", Type = LaunchOptionType.Bool, - InitialValue = HardwareHelper - .IterGpuInfo() - .Select(gpu => gpu.MemoryLevel) - .Max() switch + InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch { - Level.Low => "--lowvram", - Level.Medium => "--normalvram", + MemoryLevel.Low => "--lowvram", + MemoryLevel.Medium => "--normalvram", _ => null }, - Options = { "--highvram", "--normalvram", "--lowvram", "--novram" } + Options = ["--highvram", "--normalvram", "--lowvram", "--novram"] }, - new() + new LaunchOptionDefinition { Name = "Preview Method", Type = LaunchOptionType.Bool, InitialValue = "--preview-method auto", - Options = - { - "--preview-method auto", - "--preview-method latent2rgb", - "--preview-method taesd" - } + Options = ["--preview-method auto", "--preview-method latent2rgb", "--preview-method taesd"] }, - new() + new LaunchOptionDefinition { Name = "Enable DirectML", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.PreferDirectML(), - Options = { "--directml" } + Options = ["--directml"] }, - new() + new LaunchOptionDefinition { Name = "Use CPU only", Type = LaunchOptionType.Bool, InitialValue = !HardwareHelper.HasNvidiaGpu() && !HardwareHelper.HasAmdGpu(), - Options = { "--cpu" } + Options = ["--cpu"] }, - new() + new LaunchOptionDefinition { Name = "Disable Xformers", Type = LaunchOptionType.Bool, InitialValue = !HardwareHelper.HasNvidiaGpu(), - Options = { "--disable-xformers" } + Options = ["--disable-xformers"] }, - new() + new LaunchOptionDefinition { Name = "Disable upcasting of attention", Type = LaunchOptionType.Bool, - Options = { "--dont-upcast-attention" } + Options = ["--dont-upcast-attention"] }, - new() + new LaunchOptionDefinition { Name = "Auto-Launch", Type = LaunchOptionType.Bool, - Options = { "--auto-launch" } + Options = ["--auto-launch"] }, LaunchOptionDefinition.Extras - }; + ]; 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, @@ -173,87 +156,48 @@ public class ComfyUI : BaseGitPackage venvRunner.WorkingDirectory = installLocation; await venvRunner.Setup(true, onConsoleOutput).ConfigureAwait(false); - // Install torch / xformers based on gpu info - switch (torchVersion) + await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); + + progress?.Report(new ProgressReport(-1f, "Installing Package Requirements...", isIndeterminate: true)); + + var pipArgs = new PipInstallArgs(); + + pipArgs = torchVersion switch { - case TorchVersion.Cpu: - await InstallCpuTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); - break; - case TorchVersion.Cuda: - await venvRunner - .PipInstall( - new PipInstallArgs() - .WithTorch("~=2.1.0") - .WithTorchVision() - .WithXFormers("==0.0.22.post4") - .AddArg("--upgrade") - .WithTorchExtraIndex("cu121"), - onConsoleOutput - ) - .ConfigureAwait(false); - break; - case TorchVersion.DirectMl: - await venvRunner - .PipInstall(new PipInstallArgs().WithTorchDirectML(), onConsoleOutput) - .ConfigureAwait(false); - break; - case TorchVersion.Rocm: - await InstallRocmTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); - break; - case TorchVersion.Mps: - await venvRunner - .PipInstall( - new PipInstallArgs() - .AddArg("--pre") - .WithTorch() - .WithTorchVision() - .WithTorchExtraIndex("nightly/cpu"), - onConsoleOutput + TorchVersion.DirectMl => pipArgs.WithTorchDirectML(), + TorchVersion.Mps + => pipArgs.AddArg("--pre").WithTorch().WithTorchVision().WithTorchExtraIndex("nightly/cpu"), + _ + => pipArgs + .AddArg("--upgrade") + .WithTorch("~=2.1.0") + .WithTorchVision() + .WithTorchExtraIndex( + torchVersion switch + { + TorchVersion.Cpu => "cpu", + TorchVersion.Cuda => "cu121", + TorchVersion.Rocm => "rocm5.6", + _ => throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null) + } ) - .ConfigureAwait(false); - break; - default: - throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null); - } - - // Install requirements file (skip torch) - progress?.Report( - new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true) - ); + }; - var requirementsFile = new FilePath(installLocation, "requirements.txt"); + if (torchVersion == TorchVersion.Cuda) + { + pipArgs = pipArgs.WithXFormers("==0.0.22.post4"); + } - await venvRunner - .PipInstallFromRequirements(requirementsFile, onConsoleOutput, excludes: "torch") - .ConfigureAwait(false); + var requirements = new FilePath(installLocation, "requirements.txt"); - progress?.Report( - new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false) + pipArgs = pipArgs.WithParsedFromRequirementsTxt( + await requirements.ReadAllTextAsync().ConfigureAwait(false), + excludePattern: "torch" ); - } - private async Task AutoDetectAndInstallTorch( - PyVenvRunner venvRunner, - IProgress? progress = null - ) - { - var gpus = HardwareHelper.IterGpuInfo().ToList(); - if (gpus.Any(g => g.IsNvidia)) - { - await InstallCudaTorch(venvRunner, progress).ConfigureAwait(false); - } - else if (HardwareHelper.PreferRocm()) - { - await InstallRocmTorch(venvRunner, progress).ConfigureAwait(false); - } - else if (HardwareHelper.PreferDirectML()) - { - await InstallDirectMlTorch(venvRunner, progress).ConfigureAwait(false); - } - else - { - await InstallCpuTorch(venvRunner, progress).ConfigureAwait(false); - } + await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); + + progress?.Report(new ProgressReport(1, "Installed Package Requirements", isIndeterminate: false)); } public override async Task RunPackage( @@ -264,57 +208,83 @@ public class ComfyUI : BaseGitPackage ) { 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); - if (s.Text.Contains("To see the GUI go to", StringComparison.OrdinalIgnoreCase)) + if (!s.Text.Contains("To see the GUI go to", StringComparison.OrdinalIgnoreCase)) + return; + + var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); + var match = regex.Match(s.Text); + if (match.Success) { - var regex = new Regex(@"(https?:\/\/)([^:\s]+):(\d+)"); - var match = regex.Match(s.Text); - if (match.Success) - { - WebUrl = match.Value; - } - OnStartupComplete(WebUrl); + WebUrl = match.Value; } + OnStartupComplete(WebUrl); } + } - void HandleExit(int i) + public override Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) => + sharedFolderMethod switch { - Debug.WriteLine($"Venv process exited with code {i}"); - OnExit(i); - } + SharedFolderMethod.Symlink => SetupModelFoldersSymlink(installDirectory), + SharedFolderMethod.Configuration => SetupModelFoldersConfig(installDirectory), + SharedFolderMethod.None => Task.CompletedTask, + _ => throw new ArgumentOutOfRangeException(nameof(sharedFolderMethod), sharedFolderMethod, null) + }; - var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}"; + public override Task UpdateModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) => + SetupModelFolders(installDirectory, sharedFolderMethod); - VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit); + public override Task RemoveModelFolderLinks(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) + { + return sharedFolderMethod switch + { + SharedFolderMethod.Symlink => base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod), + SharedFolderMethod.Configuration => RemoveConfigSection(installDirectory), + SharedFolderMethod.None => Task.CompletedTask, + _ => throw new ArgumentOutOfRangeException(nameof(sharedFolderMethod), sharedFolderMethod, null) + }; } - public override Task SetupModelFolders( - DirectoryPath installDirectory, - SharedFolderMethod sharedFolderMethod - ) + private async Task SetupModelFoldersSymlink(DirectoryPath installDirectory) { - switch (sharedFolderMethod) + // Migration for `controlnet` -> `controlnet/ControlNet` and `controlnet/T2IAdapter` + // If the original link exists, delete it first + if (installDirectory.JoinDir("models/controlnet") is { IsSymbolicLink: true } controlnetOldLink) { - case SharedFolderMethod.None: - return Task.CompletedTask; - case SharedFolderMethod.Symlink: - return base.SetupModelFolders(installDirectory, sharedFolderMethod); + Logger.Info("Migration: Removing old controlnet link {Path}", controlnetOldLink); + await controlnetOldLink.DeleteAsync(false).ConfigureAwait(false); } - var extraPathsYamlPath = installDirectory + "extra_model_paths.yaml"; + // Resume base setup + await base.SetupModelFolders(installDirectory, SharedFolderMethod.Symlink).ConfigureAwait(false); + } + + private async Task SetupModelFoldersConfig(DirectoryPath installDirectory) + { + var extraPathsYamlPath = installDirectory.JoinFile("extra_model_paths.yaml"); var modelsDir = SettingsManager.ModelsDirectory; - var exists = File.Exists(extraPathsYamlPath); - if (!exists) + if (!extraPathsYamlPath.Exists) { Logger.Info("Creating extra_model_paths.yaml"); - File.WriteAllText(extraPathsYamlPath, string.Empty); + extraPathsYamlPath.Create(); } - var yaml = File.ReadAllText(extraPathsYamlPath); + + var yaml = await extraPathsYamlPath.ReadAllTextAsync().ConfigureAwait(false); using var sr = new StringReader(yaml); var yamlStream = new YamlStream(); yamlStream.Load(sr); @@ -330,14 +300,12 @@ public class ComfyUI : BaseGitPackage 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) { if (stabilityMatrixNode.Value is not YamlMappingNode nodeValue) - return Task.CompletedTask; + return; nodeValue.Children["checkpoints"] = Path.Combine(modelsDir, "StableDiffusion"); nodeValue.Children["vae"] = Path.Combine(modelsDir, "VAE"); @@ -349,7 +317,11 @@ public class ComfyUI : BaseGitPackage + $"{Path.Combine(modelsDir, "SwinIR")}"; nodeValue.Children["embeddings"] = Path.Combine(modelsDir, "TextualInversion"); nodeValue.Children["hypernetworks"] = Path.Combine(modelsDir, "Hypernetwork"); - nodeValue.Children["controlnet"] = Path.Combine(modelsDir, "ControlNet"); + nodeValue.Children["controlnet"] = string.Join( + '\n', + Path.Combine(modelsDir, "ControlNet"), + Path.Combine(modelsDir, "T2IAdapter") + ); nodeValue.Children["clip"] = Path.Combine(modelsDir, "CLIP"); nodeValue.Children["diffusers"] = Path.Combine(modelsDir, "Diffusers"); nodeValue.Children["gligen"] = Path.Combine(modelsDir, "GLIGEN"); @@ -363,17 +335,17 @@ public class ComfyUI : BaseGitPackage { { "checkpoints", Path.Combine(modelsDir, "StableDiffusion") }, { "vae", Path.Combine(modelsDir, "VAE") }, - { - "loras", - $"{Path.Combine(modelsDir, "Lora")}\n{Path.Combine(modelsDir, "LyCORIS")}" - }, + { "loras", $"{Path.Combine(modelsDir, "Lora")}\n{Path.Combine(modelsDir, "LyCORIS")}" }, { "upscale_models", $"{Path.Combine(modelsDir, "ESRGAN")}\n{Path.Combine(modelsDir, "RealESRGAN")}\n{Path.Combine(modelsDir, "SwinIR")}" }, { "embeddings", Path.Combine(modelsDir, "TextualInversion") }, { "hypernetworks", Path.Combine(modelsDir, "Hypernetwork") }, - { "controlnet", Path.Combine(modelsDir, "ControlNet") }, + { + "controlnet", + string.Join('\n', Path.Combine(modelsDir, "ControlNet"), Path.Combine(modelsDir, "T2IAdapter")) + }, { "clip", Path.Combine(modelsDir, "CLIP") }, { "diffusers", Path.Combine(modelsDir, "Diffusers") }, { "gligen", Path.Combine(modelsDir, "GLIGEN") }, @@ -383,9 +355,7 @@ public class ComfyUI : BaseGitPackage } var newRootNode = new YamlMappingNode(); - foreach ( - var child in mappingNode.Children.Where(c => c.Key.ToString() != "stability_matrix") - ) + foreach (var child in mappingNode.Children.Where(c => c.Key.ToString() != "stability_matrix")) { newRootNode.Children.Add(child); } @@ -394,121 +364,43 @@ public class ComfyUI : BaseGitPackage var serializer = new SerializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) + .WithDefaultScalarStyle(ScalarStyle.Literal) .Build(); - var yamlData = serializer.Serialize(newRootNode); - File.WriteAllText(extraPathsYamlPath, yamlData); - return Task.CompletedTask; + var yamlData = serializer.Serialize(newRootNode); + await extraPathsYamlPath.WriteAllTextAsync(yamlData).ConfigureAwait(false); } - public override Task UpdateModelFolders( - DirectoryPath installDirectory, - SharedFolderMethod sharedFolderMethod - ) => - sharedFolderMethod switch - { - SharedFolderMethod.Symlink - => base.UpdateModelFolders(installDirectory, sharedFolderMethod), - SharedFolderMethod.Configuration - => SetupModelFolders(installDirectory, sharedFolderMethod), - SharedFolderMethod.None => Task.CompletedTask, - _ => Task.CompletedTask - }; - - public override Task RemoveModelFolderLinks( - DirectoryPath installDirectory, - SharedFolderMethod sharedFolderMethod - ) + private static async Task RemoveConfigSection(DirectoryPath installDirectory) { - return sharedFolderMethod switch - { - SharedFolderMethod.Configuration => RemoveConfigSection(installDirectory), - SharedFolderMethod.None => Task.CompletedTask, - SharedFolderMethod.Symlink - => base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod), - _ => Task.CompletedTask - }; - } + var extraPathsYamlPath = installDirectory.JoinFile("extra_model_paths.yaml"); - private Task RemoveConfigSection(string installDirectory) - { - var extraPathsYamlPath = Path.Combine(installDirectory, "extra_model_paths.yaml"); - var exists = File.Exists(extraPathsYamlPath); - if (!exists) + if (!extraPathsYamlPath.Exists) { - return Task.CompletedTask; + return; } - var yaml = File.ReadAllText(extraPathsYamlPath); + var yaml = await extraPathsYamlPath.ReadAllTextAsync().ConfigureAwait(false); using var sr = new StringReader(yaml); var yamlStream = new YamlStream(); yamlStream.Load(sr); if (!yamlStream.Documents.Any()) { - return Task.CompletedTask; + return; } var root = yamlStream.Documents[0].RootNode; if (root is not YamlMappingNode mappingNode) { - return Task.CompletedTask; + return; } 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); - File.WriteAllText(extraPathsYamlPath, yamlData); - - return Task.CompletedTask; - } - - private async Task InstallRocmTorch( - PyVenvRunner venvRunner, - IProgress? progress = null, - Action? onConsoleOutput = null - ) - { - progress?.Report( - new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true) - ); - - await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); - - await venvRunner - .PipInstall( - new PipInstallArgs() - .WithTorch("==2.0.1") - .WithTorchVision() - .WithTorchExtraIndex("rocm5.6"), - onConsoleOutput - ) - .ConfigureAwait(false); - } - - public async Task SetupInferenceOutputFolderLinks(DirectoryPath installDirectory) - { - var inferenceDir = installDirectory.JoinDir("output", "Inference"); - - var sharedInferenceDir = SettingsManager.ImagesInferenceDirectory; - - if (inferenceDir.IsSymbolicLink) - { - if (inferenceDir.Info.ResolveLinkTarget(true)?.FullName == sharedInferenceDir.FullPath) - { - // Already valid link, skip - return; - } - - // Otherwise delete so we don't have to move files - await sharedInferenceDir.DeleteAsync(false).ConfigureAwait(false); - } - await Helper.SharedFolders - .CreateOrUpdateLink(sharedInferenceDir, inferenceDir) - .ConfigureAwait(false); + await extraPathsYamlPath.WriteAllTextAsync(yamlData).ConfigureAwait(false); } } diff --git a/StabilityMatrix.Core/Models/Packages/FocusControlNet.cs b/StabilityMatrix.Core/Models/Packages/FocusControlNet.cs index 001b6156..9291c1e8 100644 --- a/StabilityMatrix.Core/Models/Packages/FocusControlNet.cs +++ b/StabilityMatrix.Core/Models/Packages/FocusControlNet.cs @@ -12,23 +12,18 @@ using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [Singleton(typeof(BasePackage))] -public class FocusControlNet : Fooocus +public class FocusControlNet( + IGithubApiCache githubApi, + ISettingsManager settingsManager, + IDownloadService downloadService, + IPrerequisiteHelper prerequisiteHelper +) : Fooocus(githubApi, settingsManager, downloadService, prerequisiteHelper) { - public FocusControlNet( - IGithubApiCache githubApi, - ISettingsManager settingsManager, - IDownloadService downloadService, - IPrerequisiteHelper prerequisiteHelper - ) - : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { } - public override string Name => "Fooocus-ControlNet-SDXL"; public override string DisplayName { get; set; } = "Fooocus-ControlNet"; public override string Author => "fenneishi"; - public override string Blurb => - "Fooocus-ControlNet adds more control to the original Fooocus software."; - public override string LicenseUrl => - "https://github.com/fenneishi/Fooocus-ControlNet-SDXL/blob/main/LICENSE"; + public override string Blurb => "Fooocus-ControlNet adds more control to the original Fooocus software."; + public override string LicenseUrl => "https://github.com/fenneishi/Fooocus-ControlNet-SDXL/blob/main/LICENSE"; public override Uri PreviewImageUri => new("https://github.com/fenneishi/Fooocus-ControlNet-SDXL/raw/main/asset/canny/snip.png"); public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Expert; diff --git a/StabilityMatrix.Core/Models/Packages/Fooocus.cs b/StabilityMatrix.Core/Models/Packages/Fooocus.cs index b25e2ed1..698c9f1a 100644 --- a/StabilityMatrix.Core/Models/Packages/Fooocus.cs +++ b/StabilityMatrix.Core/Models/Packages/Fooocus.cs @@ -1,8 +1,10 @@ -using System.Diagnostics; +using System.Collections.Immutable; +using System.Diagnostics; using System.Text.RegularExpressions; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; @@ -12,31 +14,25 @@ using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [Singleton(typeof(BasePackage))] -public class Fooocus : BaseGitPackage +public class Fooocus( + IGithubApiCache githubApi, + ISettingsManager settingsManager, + IDownloadService downloadService, + IPrerequisiteHelper prerequisiteHelper +) : BaseGitPackage(githubApi, settingsManager, downloadService, prerequisiteHelper) { - public Fooocus( - IGithubApiCache githubApi, - ISettingsManager settingsManager, - IDownloadService downloadService, - IPrerequisiteHelper prerequisiteHelper - ) - : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { } - public override string Name => "Fooocus"; public override string DisplayName { get; set; } = "Fooocus"; public override string Author => "lllyasviel"; - public override string Blurb => - "Fooocus is a rethinking of Stable Diffusion and Midjourney’s designs"; + public override string Blurb => "Fooocus is a rethinking of Stable Diffusion and Midjourney’s designs"; public override string LicenseType => "GPL-3.0"; public override string LicenseUrl => "https://github.com/lllyasviel/Fooocus/blob/main/LICENSE"; public override string LaunchCommand => "launch.py"; public override Uri PreviewImageUri => - new( - "https://user-images.githubusercontent.com/19834515/261830306-f79c5981-cf80-4ee3-b06b-3fef3f8bfbc7.png" - ); + new("https://user-images.githubusercontent.com/19834515/261830306-f79c5981-cf80-4ee3-b06b-3fef3f8bfbc7.png"); public override List LaunchOptions => new() @@ -79,16 +75,13 @@ public class Fooocus : BaseGitPackage { Name = "VRAM", Type = LaunchOptionType.Bool, - InitialValue = HardwareHelper - .IterGpuInfo() - .Select(gpu => gpu.MemoryLevel) - .Max() switch + InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch { - Level.Low => "--lowvram", - Level.Medium => "--normalvram", + MemoryLevel.Low => "--always-low-vram", + MemoryLevel.Medium => "--always-normal-vram", _ => null }, - Options = { "--highvram", "--normalvram", "--lowvram", "--novram" } + Options = { "--always-high-vram", "--always-normal-vram", "--always-low-vram", "--always-no-vram" } }, new LaunchOptionDefinition { @@ -158,42 +151,40 @@ public class Fooocus : BaseGitPackage Action? onConsoleOutput = null ) { - var venvRunner = await SetupVenv(installLocation, forceRecreate: true) - .ConfigureAwait(false); + var venvRunner = await SetupVenv(installLocation, forceRecreate: true).ConfigureAwait(false); + + progress?.Report(new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true)); - progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true)); + var pipArgs = new PipInstallArgs(); if (torchVersion == TorchVersion.DirectMl) { - await venvRunner - .PipInstall(new PipInstallArgs().WithTorchDirectML(), onConsoleOutput) - .ConfigureAwait(false); + pipArgs = pipArgs.WithTorchDirectML(); } else { - var extraIndex = torchVersion switch - { - TorchVersion.Cpu => "cpu", - TorchVersion.Cuda => "cu121", - TorchVersion.Rocm => "rocm5.6", - _ => throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null) - }; - - await venvRunner - .PipInstall( - new PipInstallArgs() - .WithTorch("==2.1.0") - .WithTorchVision("==0.16.0") - .WithTorchExtraIndex(extraIndex), - onConsoleOutput - ) - .ConfigureAwait(false); + pipArgs = pipArgs + .WithTorch("==2.1.0") + .WithTorchVision("==0.16.0") + .WithTorchExtraIndex( + torchVersion switch + { + TorchVersion.Cpu => "cpu", + TorchVersion.Cuda => "cu121", + TorchVersion.Rocm => "rocm5.6", + _ => throw new ArgumentOutOfRangeException(nameof(torchVersion), torchVersion, null) + } + ); } var requirements = new FilePath(installLocation, "requirements_versions.txt"); - await venvRunner - .PipInstallFromRequirements(requirements, onConsoleOutput, excludes: "torch") - .ConfigureAwait(false); + + pipArgs = pipArgs.WithParsedFromRequirementsTxt( + await requirements.ReadAllTextAsync().ConfigureAwait(false), + excludePattern: "torch" + ); + + await venvRunner.PipInstall(pipArgs, onConsoleOutput).ConfigureAwait(false); } public override async Task RunPackage( diff --git a/StabilityMatrix.Core/Models/Packages/FooocusMre.cs b/StabilityMatrix.Core/Models/Packages/FooocusMre.cs index 6eec6946..d5d3516a 100644 --- a/StabilityMatrix.Core/Models/Packages/FooocusMre.cs +++ b/StabilityMatrix.Core/Models/Packages/FooocusMre.cs @@ -12,16 +12,13 @@ using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [Singleton(typeof(BasePackage))] -public class FooocusMre : BaseGitPackage +public class FooocusMre( + IGithubApiCache githubApi, + ISettingsManager settingsManager, + IDownloadService downloadService, + IPrerequisiteHelper prerequisiteHelper +) : BaseGitPackage(githubApi, settingsManager, downloadService, prerequisiteHelper) { - public FooocusMre( - IGithubApiCache githubApi, - ISettingsManager settingsManager, - IDownloadService downloadService, - IPrerequisiteHelper prerequisiteHelper - ) - : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { } - public override string Name => "Fooocus-MRE"; public override string DisplayName { get; set; } = "Fooocus-MRE"; public override string Author => "MoonRide303"; @@ -31,14 +28,11 @@ public class FooocusMre : BaseGitPackage public override string LicenseType => "GPL-3.0"; - public override string LicenseUrl => - "https://github.com/MoonRide303/Fooocus-MRE/blob/moonride-main/LICENSE"; + public override string LicenseUrl => "https://github.com/MoonRide303/Fooocus-MRE/blob/moonride-main/LICENSE"; public override string LaunchCommand => "launch.py"; public override Uri PreviewImageUri => - new( - "https://user-images.githubusercontent.com/130458190/265366059-ce430ea0-0995-4067-98dd-cef1d7dc1ab6.png" - ); + new("https://user-images.githubusercontent.com/130458190/265366059-ce430ea0-0995-4067-98dd-cef1d7dc1ab6.png"); public override string Disclaimer => "This package may no longer receive updates from its author. It may be removed from Stability Matrix in the future."; @@ -112,8 +106,7 @@ public class FooocusMre : BaseGitPackage Action? onConsoleOutput = null ) { - var venvRunner = await SetupVenv(installLocation, forceRecreate: true) - .ConfigureAwait(false); + var venvRunner = await SetupVenv(installLocation, forceRecreate: true).ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true)); diff --git a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs index f54d7e68..fbe54f5a 100644 --- a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs +++ b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs @@ -42,9 +42,7 @@ public class InvokeAI : BaseGitPackage }; public override Uri PreviewImageUri => - new( - "https://raw.githubusercontent.com/invoke-ai/InvokeAI/main/docs/assets/canvas_preview.png" - ); + new("https://raw.githubusercontent.com/invoke-ai/InvokeAI/main/docs/assets/canvas_preview.png"); public override IEnumerable AvailableSharedFolderMethods => new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None }; @@ -63,58 +61,48 @@ public class InvokeAI : BaseGitPackage public override Dictionary> SharedFolders => new() { - [SharedFolderType.StableDiffusion] = new[] + [SharedFolderType.StableDiffusion] = new[] { Path.Combine(RelativeRootPath, "autoimport", "main") }, + [SharedFolderType.Lora] = new[] { Path.Combine(RelativeRootPath, "autoimport", "lora") }, + [SharedFolderType.TextualInversion] = new[] { Path.Combine(RelativeRootPath, "autoimport", "embedding") }, + [SharedFolderType.ControlNet] = new[] { Path.Combine(RelativeRootPath, "autoimport", "controlnet") }, + [SharedFolderType.InvokeIpAdapters15] = new[] { - Path.Combine(RelativeRootPath, "autoimport", "main") + Path.Combine(RelativeRootPath, "models", "sd-1", "ip_adapter") }, - [SharedFolderType.Lora] = new[] + [SharedFolderType.InvokeIpAdaptersXl] = new[] { - Path.Combine(RelativeRootPath, "autoimport", "lora") + Path.Combine(RelativeRootPath, "models", "sdxl", "ip_adapter") }, - [SharedFolderType.TextualInversion] = new[] + [SharedFolderType.InvokeClipVision] = new[] { - Path.Combine(RelativeRootPath, "autoimport", "embedding") + Path.Combine(RelativeRootPath, "models", "any", "clip_vision") }, - [SharedFolderType.ControlNet] = new[] - { - Path.Combine(RelativeRootPath, "autoimport", "controlnet") - }, - [SharedFolderType.IpAdapter] = new[] - { - Path.Combine(RelativeRootPath, "autoimport", "ip_adapter") - } + [SharedFolderType.T2IAdapter] = new[] { Path.Combine(RelativeRootPath, "autoimport", "t2i_adapter") } }; public override Dictionary>? SharedOutputFolders => - new() - { - [SharedOutputType.Text2Img] = new[] - { - Path.Combine("invokeai-root", "outputs", "images") - } - }; + new() { [SharedOutputType.Text2Img] = new[] { Path.Combine("invokeai-root", "outputs", "images") } }; public override string OutputFolderName => Path.Combine("invokeai-root", "outputs", "images"); // https://github.com/invoke-ai/InvokeAI/blob/main/docs/features/CONFIGURATION.md public override List LaunchOptions => - new List - { - new() + [ + new LaunchOptionDefinition { Name = "Host", Type = LaunchOptionType.String, DefaultValue = "localhost", - Options = new List { "--host" } + Options = ["--host"] }, - new() + new LaunchOptionDefinition { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "9090", - Options = new List { "--port" } + Options = ["--port"] }, - new() + new LaunchOptionDefinition { Name = "Allow Origins", Description = @@ -122,33 +110,28 @@ public class InvokeAI : BaseGitPackage + "InvokeAI API in the format ['host1','host2',...]", Type = LaunchOptionType.String, DefaultValue = "[]", - Options = new List { "--allow-origins" } + Options = ["--allow-origins"] }, - new() + new LaunchOptionDefinition { Name = "Always use CPU", Type = LaunchOptionType.Bool, - Options = new List { "--always_use_cpu" } + Options = ["--always_use_cpu"] }, - new() + new LaunchOptionDefinition { Name = "Precision", Type = LaunchOptionType.Bool, - Options = new List - { - "--precision auto", - "--precision float16", - "--precision float32", - } + Options = ["--precision auto", "--precision float16", "--precision float32"] }, - new() + new LaunchOptionDefinition { Name = "Aggressively free up GPU memory after each operation", Type = LaunchOptionType.Bool, - Options = new List { "--free_gpu_mem" } + Options = ["--free_gpu_mem"] }, LaunchOptionDefinition.Extras - }; + ]; public override IEnumerable AvailableTorchVersions => new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.Rocm, TorchVersion.Mps }; @@ -185,24 +168,19 @@ public class InvokeAI : BaseGitPackage venvRunner.EnvironmentVariables = GetEnvVars(installLocation); progress?.Report(new ProgressReport(-1f, "Installing Package", isIndeterminate: true)); - var pipCommandArgs = - "-e . --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu"; + var pipCommandArgs = "-e . --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu"; switch (torchVersion) { // If has Nvidia Gpu, install CUDA version case TorchVersion.Cuda: - progress?.Report( - new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true) - ); + progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true)); var args = new List(); if (exists) { var pipPackages = await venvRunner.PipList().ConfigureAwait(false); - var hasCuda121 = pipPackages.Any( - p => p.Name == "torch" && p.Version.Contains("cu121") - ); + var hasCuda121 = pipPackages.Any(p => p.Name == "torch" && p.Version.Contains("cu121")); if (!hasCuda121) { args.Add("--upgrade"); @@ -222,23 +200,18 @@ public class InvokeAI : BaseGitPackage .ConfigureAwait(false); Logger.Info("Starting InvokeAI install (CUDA)..."); - pipCommandArgs = - "-e .[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu121"; + pipCommandArgs = "-e .[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu121"; break; // For AMD, Install ROCm version case TorchVersion.Rocm: await venvRunner .PipInstall( - new PipInstallArgs() - .WithTorch("==2.0.1") - .WithTorchVision() - .WithExtraIndex("rocm5.4.2"), + new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision().WithExtraIndex("rocm5.4.2"), onConsoleOutput ) .ConfigureAwait(false); Logger.Info("Starting InvokeAI install (ROCm)..."); - pipCommandArgs = - "-e . --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2"; + pipCommandArgs = "-e . --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2"; break; case TorchVersion.Mps: // For Apple silicon, use MPS @@ -251,9 +224,7 @@ public class InvokeAI : BaseGitPackage .PipInstall($"{pipCommandArgs}{(exists ? " --upgrade" : "")}", onConsoleOutput) .ConfigureAwait(false); - await venvRunner - .PipInstall("rich packaging python-dotenv", onConsoleOutput) - .ConfigureAwait(false); + await venvRunner.PipInstall("rich packaging python-dotenv", onConsoleOutput).ConfigureAwait(false); progress?.Report(new ProgressReport(-1f, "Configuring InvokeAI", isIndeterminate: true)); @@ -349,13 +320,7 @@ public class InvokeAI : BaseGitPackage { onConsoleOutput?.Invoke(s); - if ( - spam3 - && s.Text.Contains( - "[3] Accept the best guess;", - StringComparison.OrdinalIgnoreCase - ) - ) + if (spam3 && s.Text.Contains("[3] Accept the best guess;", StringComparison.OrdinalIgnoreCase)) { VenvRunner.Process?.StandardInput.WriteLine("3"); return; @@ -373,17 +338,11 @@ public class InvokeAI : BaseGitPackage OnStartupComplete(WebUrl); } - VenvRunner.RunDetached( - $"-c \"{code}\" {arguments}".TrimEnd(), - HandleConsoleOutput, - OnExit - ); + VenvRunner.RunDetached($"-c \"{code}\" {arguments}".TrimEnd(), HandleConsoleOutput, OnExit); } else { - var result = await VenvRunner - .Run($"-c \"{code}\" {arguments}".TrimEnd()) - .ConfigureAwait(false); + var result = await VenvRunner.Run($"-c \"{code}\" {arguments}".TrimEnd()).ConfigureAwait(false); onConsoleOutput?.Invoke(new ProcessOutput { Text = result.StandardOutput }); } } diff --git a/StabilityMatrix.Core/Models/Packages/KohyaSs.cs b/StabilityMatrix.Core/Models/Packages/KohyaSs.cs index 99b2a6a3..fa3adb34 100644 --- a/StabilityMatrix.Core/Models/Packages/KohyaSs.cs +++ b/StabilityMatrix.Core/Models/Packages/KohyaSs.cs @@ -4,6 +4,7 @@ using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; @@ -13,30 +14,20 @@ using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [Singleton(typeof(BasePackage))] -public class KohyaSs : BaseGitPackage +public class KohyaSs( + IGithubApiCache githubApi, + ISettingsManager settingsManager, + IDownloadService downloadService, + IPrerequisiteHelper prerequisiteHelper, + IPyRunner runner +) : BaseGitPackage(githubApi, settingsManager, downloadService, prerequisiteHelper) { - private readonly IPyRunner pyRunner; - - public KohyaSs( - IGithubApiCache githubApi, - ISettingsManager settingsManager, - IDownloadService downloadService, - IPrerequisiteHelper prerequisiteHelper, - IPyRunner pyRunner - ) - : base(githubApi, settingsManager, downloadService, prerequisiteHelper) - { - this.pyRunner = pyRunner; - } - public override string Name => "kohya_ss"; public override string DisplayName { get; set; } = "kohya_ss"; public override string Author => "bmaltais"; - public override string Blurb => - "A Windows-focused Gradio GUI for Kohya's Stable Diffusion trainers"; + public override string Blurb => "A Windows-focused Gradio GUI for Kohya's Stable Diffusion trainers"; public override string LicenseType => "Apache-2.0"; - public override string LicenseUrl => - "https://github.com/bmaltais/kohya_ss/blob/master/LICENSE.md"; + public override string LicenseUrl => "https://github.com/bmaltais/kohya_ss/blob/master/LICENSE.md"; public override string LaunchCommand => "kohya_gui.py"; public override Uri PreviewImageUri => @@ -49,71 +40,68 @@ public class KohyaSs : BaseGitPackage 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 AvailableSharedFolderMethods => new[] { SharedFolderMethod.None }; public override List LaunchOptions => - new() - { + [ new LaunchOptionDefinition { Name = "Listen Address", Type = LaunchOptionType.String, DefaultValue = "127.0.0.1", - Options = new List { "--listen" } + Options = ["--listen"] }, new LaunchOptionDefinition { Name = "Port", Type = LaunchOptionType.String, - Options = new List { "--port" } + Options = ["--port"] }, new LaunchOptionDefinition { Name = "Username", Type = LaunchOptionType.String, - Options = new List { "--username" } + Options = ["--username"] }, new LaunchOptionDefinition { Name = "Password", Type = LaunchOptionType.String, - Options = new List { "--password" } + Options = ["--password"] }, new LaunchOptionDefinition { Name = "Auto-Launch Browser", Type = LaunchOptionType.Bool, - Options = new List { "--inbrowser" } + Options = ["--inbrowser"] }, new LaunchOptionDefinition { Name = "Share", Type = LaunchOptionType.Bool, - Options = new List { "--share" } + Options = ["--share"] }, new LaunchOptionDefinition { Name = "Headless", Type = LaunchOptionType.Bool, - Options = new List { "--headless" } + Options = ["--headless"] }, new LaunchOptionDefinition { Name = "Language", Type = LaunchOptionType.String, - Options = new List { "--language" } + Options = ["--language"] }, LaunchOptionDefinition.Extras - }; + ]; public override async Task InstallPackage( string installLocation, @@ -126,9 +114,7 @@ public class KohyaSs : BaseGitPackage { if (Compat.IsWindows) { - progress?.Report( - new ProgressReport(-1f, "Installing prerequisites...", isIndeterminate: true) - ); + progress?.Report(new ProgressReport(-1f, "Installing prerequisites...", isIndeterminate: true)); await PrerequisiteHelper.InstallTkinterIfNecessary(progress).ConfigureAwait(false); } @@ -177,7 +163,7 @@ public class KohyaSs : BaseGitPackage await SetupVenv(installedPackagePath).ConfigureAwait(false); // update gui files to point to venv accelerate - await pyRunner.RunInThreadWithLock(() => + await runner.RunInThreadWithLock(() => { var scope = Py.CreateScope(); scope.Exec( @@ -203,9 +189,7 @@ public class KohyaSs : BaseGitPackage """ ); - 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", @@ -259,10 +243,7 @@ public class KohyaSs : BaseGitPackage } public override Dictionary>? SharedFolders { get; } - public override Dictionary< - SharedOutputType, - IReadOnlyList - >? SharedOutputFolders { get; } + public override Dictionary>? SharedOutputFolders { get; } public override string MainBranch => "master"; @@ -278,13 +259,7 @@ public class KohyaSs : BaseGitPackage if (!Compat.IsWindows) return env; - var tkPath = Path.Combine( - SettingsManager.LibraryDir, - "Assets", - "Python310", - "tcl", - "tcl8.6" - ); + var tkPath = Path.Combine(SettingsManager.LibraryDir, "Assets", "Python310", "tcl", "tcl8.6"); env["TCL_LIBRARY"] = tkPath; env["TK_LIBRARY"] = tkPath; diff --git a/StabilityMatrix.Core/Models/Packages/RuinedFooocus.cs b/StabilityMatrix.Core/Models/Packages/RuinedFooocus.cs index 6300b259..fbf2535f 100644 --- a/StabilityMatrix.Core/Models/Packages/RuinedFooocus.cs +++ b/StabilityMatrix.Core/Models/Packages/RuinedFooocus.cs @@ -1,8 +1,7 @@ -using System.Diagnostics; -using System.Text.RegularExpressions; -using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; @@ -12,27 +11,96 @@ using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [Singleton(typeof(BasePackage))] -public class RuinedFooocus : Fooocus +public class RuinedFooocus( + IGithubApiCache githubApi, + ISettingsManager settingsManager, + IDownloadService downloadService, + IPrerequisiteHelper prerequisiteHelper +) : Fooocus(githubApi, settingsManager, downloadService, prerequisiteHelper) { - public RuinedFooocus( - IGithubApiCache githubApi, - ISettingsManager settingsManager, - IDownloadService downloadService, - IPrerequisiteHelper prerequisiteHelper - ) - : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { } - public override string Name => "RuinedFooocus"; public override string DisplayName { get; set; } = "RuinedFooocus"; public override string Author => "runew0lf"; public override string Blurb => "RuinedFooocus combines the best aspects of Stable Diffusion and Midjourney into one seamless, cutting-edge experience"; - public override string LicenseUrl => - "https://github.com/runew0lf/RuinedFooocus/blob/main/LICENSE"; + public override string LicenseUrl => "https://github.com/runew0lf/RuinedFooocus/blob/main/LICENSE"; public override Uri PreviewImageUri => new("https://raw.githubusercontent.com/runew0lf/pmmconfigs/main/RuinedFooocus_ss.png"); public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Expert; + public override List LaunchOptions => + new() + { + new LaunchOptionDefinition + { + Name = "Preset", + Type = LaunchOptionType.Bool, + Options = { "--preset anime", "--preset realistic" } + }, + new LaunchOptionDefinition + { + Name = "Port", + Type = LaunchOptionType.String, + Description = "Sets the listen port", + Options = { "--port" } + }, + new LaunchOptionDefinition + { + Name = "Share", + Type = LaunchOptionType.Bool, + Description = "Set whether to share on Gradio", + Options = { "--share" } + }, + new LaunchOptionDefinition + { + Name = "Listen", + Type = LaunchOptionType.String, + Description = "Set the listen interface", + Options = { "--listen" } + }, + new LaunchOptionDefinition + { + Name = "Output Directory", + Type = LaunchOptionType.String, + Description = "Override the output directory", + Options = { "--output-directory" } + }, + new() + { + Name = "VRAM", + Type = LaunchOptionType.Bool, + InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch + { + MemoryLevel.Low => "--lowvram", + MemoryLevel.Medium => "--normalvram", + _ => null + }, + Options = { "--highvram", "--normalvram", "--lowvram", "--novram" } + }, + new LaunchOptionDefinition + { + Name = "Use DirectML", + Type = LaunchOptionType.Bool, + Description = "Use pytorch with DirectML support", + InitialValue = HardwareHelper.PreferDirectML(), + Options = { "--directml" } + }, + new LaunchOptionDefinition + { + Name = "Disable Xformers", + Type = LaunchOptionType.Bool, + InitialValue = !HardwareHelper.HasNvidiaGpu(), + Options = { "--disable-xformers" } + }, + new LaunchOptionDefinition + { + Name = "Auto-Launch", + Type = LaunchOptionType.Bool, + Options = { "--auto-launch" } + }, + LaunchOptionDefinition.Extras + }; + public override async Task InstallPackage( string installLocation, TorchVersion torchVersion, @@ -44,16 +112,25 @@ public class RuinedFooocus : Fooocus { if (torchVersion == TorchVersion.Cuda) { - var venvRunner = await SetupVenv(installLocation, forceRecreate: true) - .ConfigureAwait(false); - - progress?.Report(new ProgressReport(-1f, "Installing torch...", isIndeterminate: true)); + var venvRunner = await SetupVenv(installLocation, forceRecreate: true).ConfigureAwait(false); - await InstallCudaTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); + progress?.Report(new ProgressReport(-1f, "Installing requirements...", isIndeterminate: true)); var requirements = new FilePath(installLocation, "requirements_versions.txt"); + await venvRunner - .PipInstallFromRequirements(requirements, onConsoleOutput, excludes: "torch") + .PipInstall( + new PipInstallArgs() + .WithTorch("==2.0.1") + .WithTorchVision("==0.15.2") + .WithXFormers("==0.0.20") + .WithTorchExtraIndex("cu118") + .WithParsedFromRequirementsTxt( + await requirements.ReadAllTextAsync().ConfigureAwait(false), + excludePattern: "torch" + ), + onConsoleOutput + ) .ConfigureAwait(false); } else @@ -68,5 +145,9 @@ public class RuinedFooocus : Fooocus ) .ConfigureAwait(false); } + + // Create output folder since it's not created by default + var outputFolder = new DirectoryPath(installLocation, OutputFolderName); + outputFolder.Create(); } } diff --git a/StabilityMatrix.Core/Models/Packages/StableDiffusionDirectMl.cs b/StabilityMatrix.Core/Models/Packages/StableDiffusionDirectMl.cs index 74d71078..34321d04 100644 --- a/StabilityMatrix.Core/Models/Packages/StableDiffusionDirectMl.cs +++ b/StabilityMatrix.Core/Models/Packages/StableDiffusionDirectMl.cs @@ -14,7 +14,12 @@ using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [Singleton(typeof(BasePackage))] -public class StableDiffusionDirectMl : A3WebUI +public class StableDiffusionDirectMl( + IGithubApiCache githubApi, + ISettingsManager settingsManager, + IDownloadService downloadService, + IPrerequisiteHelper prerequisiteHelper +) : A3WebUI(githubApi, settingsManager, downloadService, prerequisiteHelper) { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); @@ -24,26 +29,15 @@ public class StableDiffusionDirectMl : A3WebUI public override string LicenseType => "AGPL-3.0"; public override string LicenseUrl => "https://github.com/lshqqytiger/stable-diffusion-webui-directml/blob/master/LICENSE.txt"; - public override string Blurb => - "A fork of Automatic1111's Stable Diffusion WebUI with DirectML support"; + public override string Blurb => "A fork of Automatic1111's Stable Diffusion WebUI with DirectML support"; public override string LaunchCommand => "launch.py"; public override Uri PreviewImageUri => - new( - "https://github.com/lshqqytiger/stable-diffusion-webui-directml/raw/master/screenshot.png" - ); + new("https://github.com/lshqqytiger/stable-diffusion-webui-directml/raw/master/screenshot.png"); public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Recommended; - public StableDiffusionDirectMl( - IGithubApiCache githubApi, - ISettingsManager settingsManager, - IDownloadService downloadService, - IPrerequisiteHelper prerequisiteHelper - ) - : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { } - public override IEnumerable AvailableTorchVersions => new[] { TorchVersion.Cpu, TorchVersion.DirectMl }; @@ -67,8 +61,7 @@ public class StableDiffusionDirectMl : A3WebUI switch (torchVersion) { case TorchVersion.DirectMl: - await InstallDirectMlTorch(venvRunner, progress, onConsoleOutput) - .ConfigureAwait(false); + await InstallDirectMlTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); break; case TorchVersion.Cpu: await InstallCpuTorch(venvRunner, progress, onConsoleOutput).ConfigureAwait(false); @@ -78,9 +71,7 @@ public class StableDiffusionDirectMl : A3WebUI await venvRunner.PipInstall("httpx==0.24.1").ConfigureAwait(false); // Install requirements file - progress?.Report( - new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true) - ); + progress?.Report(new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true)); Logger.Info("Installing requirements_versions.txt"); var requirements = new FilePath(installLocation, "requirements_versions.txt"); diff --git a/StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs b/StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs index 9b4a9b45..1f646d06 100644 --- a/StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs +++ b/StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs @@ -5,6 +5,7 @@ using NLog; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; @@ -14,7 +15,12 @@ using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [Singleton(typeof(BasePackage))] -public class StableDiffusionUx : BaseGitPackage +public class StableDiffusionUx( + IGithubApiCache githubApi, + ISettingsManager settingsManager, + IDownloadService downloadService, + IPrerequisiteHelper prerequisiteHelper +) : BaseGitPackage(githubApi, settingsManager, downloadService, prerequisiteHelper) { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); @@ -22,29 +28,18 @@ public class StableDiffusionUx : BaseGitPackage public override string DisplayName { get; set; } = "Stable Diffusion Web UI-UX"; public override string Author => "anapnoe"; public override string LicenseType => "AGPL-3.0"; - public override string LicenseUrl => - "https://github.com/anapnoe/stable-diffusion-webui-ux/blob/master/LICENSE.txt"; + public override string LicenseUrl => "https://github.com/anapnoe/stable-diffusion-webui-ux/blob/master/LICENSE.txt"; public override string Blurb => "A pixel perfect design, mobile friendly, customizable interface that adds accessibility, " + "ease of use and extended functionallity to the stable diffusion web ui."; public override string LaunchCommand => "launch.py"; public override Uri PreviewImageUri => - new( - "https://raw.githubusercontent.com/anapnoe/stable-diffusion-webui-ux/master/screenshot.png" - ); + new("https://raw.githubusercontent.com/anapnoe/stable-diffusion-webui-ux/master/screenshot.png"); public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Symlink; public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Simple; - public StableDiffusionUx( - IGithubApiCache githubApi, - ISettingsManager settingsManager, - IDownloadService downloadService, - IPrerequisiteHelper prerequisiteHelper - ) - : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { } - public override Dictionary> SharedFolders => new() { @@ -79,95 +74,91 @@ public class StableDiffusionUx : BaseGitPackage [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")] public override List LaunchOptions => - new() - { + [ new() { Name = "Host", Type = LaunchOptionType.String, DefaultValue = "localhost", - Options = new() { "--server-name" } + Options = ["--server-name"] }, new() { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "7860", - Options = new() { "--port" } + Options = ["--port"] }, new() { Name = "VRAM", Type = LaunchOptionType.Bool, - InitialValue = HardwareHelper - .IterGpuInfo() - .Select(gpu => gpu.MemoryLevel) - .Max() switch + InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch { - Level.Low => "--lowvram", - Level.Medium => "--medvram", + MemoryLevel.Low => "--lowvram", + MemoryLevel.Medium => "--medvram", _ => null }, - Options = new() { "--lowvram", "--medvram", "--medvram-sdxl" } + Options = ["--lowvram", "--medvram", "--medvram-sdxl"] }, new() { Name = "Xformers", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.HasNvidiaGpu(), - Options = new() { "--xformers" } + Options = ["--xformers"] }, new() { Name = "API", Type = LaunchOptionType.Bool, InitialValue = true, - Options = new() { "--api" } + Options = ["--api"] }, new() { Name = "Auto Launch Web UI", Type = LaunchOptionType.Bool, InitialValue = false, - Options = new() { "--autolaunch" } + Options = ["--autolaunch"] }, new() { Name = "Skip Torch CUDA Check", Type = LaunchOptionType.Bool, InitialValue = !HardwareHelper.HasNvidiaGpu(), - Options = new() { "--skip-torch-cuda-test" } + Options = ["--skip-torch-cuda-test"] }, new() { Name = "Skip Python Version Check", Type = LaunchOptionType.Bool, InitialValue = true, - Options = new() { "--skip-python-version-check" } + Options = ["--skip-python-version-check"] }, new() { Name = "No Half", Type = LaunchOptionType.Bool, Description = "Do not switch the model to 16-bit floats", - InitialValue = HardwareHelper.HasAmdGpu(), - Options = new() { "--no-half" } + InitialValue = HardwareHelper.PreferRocm() || HardwareHelper.PreferDirectML(), + Options = ["--no-half"] }, new() { Name = "Skip SD Model Download", Type = LaunchOptionType.Bool, InitialValue = false, - Options = new() { "--no-download-sd-model" } + Options = ["--no-download-sd-model"] }, new() { Name = "Skip Install", Type = LaunchOptionType.Bool, - Options = new() { "--skip-install" } + Options = ["--skip-install"] }, LaunchOptionDefinition.Extras - }; + ]; public override IEnumerable AvailableSharedFolderMethods => new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None }; @@ -210,9 +201,7 @@ public class StableDiffusionUx : BaseGitPackage } // Install requirements file - progress?.Report( - new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true) - ); + progress?.Report(new ProgressReport(-1f, "Installing Package Requirements", isIndeterminate: true)); Logger.Info("Installing requirements_versions.txt"); var requirements = new FilePath(installLocation, "requirements_versions.txt"); @@ -259,18 +248,13 @@ public class StableDiffusionUx : BaseGitPackage Action? onConsoleOutput = null ) { - progress?.Report( - new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true) - ); + progress?.Report(new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true)); await venvRunner.PipInstall("--upgrade pip wheel", onConsoleOutput).ConfigureAwait(false); await venvRunner .PipInstall( - new PipInstallArgs() - .WithTorch("==2.0.1") - .WithTorchVision() - .WithTorchExtraIndex("rocm5.1.1"), + new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision().WithTorchExtraIndex("rocm5.1.1"), onConsoleOutput ) .ConfigureAwait(false); diff --git a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs index 6a5c6308..383ace11 100644 --- a/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs +++ b/StabilityMatrix.Core/Models/Packages/VladAutomatic.cs @@ -7,6 +7,7 @@ using NLog; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; +using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Processes; @@ -16,7 +17,12 @@ using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [Singleton(typeof(BasePackage))] -public class VladAutomatic : BaseGitPackage +public class VladAutomatic( + IGithubApiCache githubApi, + ISettingsManager settingsManager, + IDownloadService downloadService, + IPrerequisiteHelper prerequisiteHelper +) : BaseGitPackage(githubApi, settingsManager, downloadService, prerequisiteHelper) { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); @@ -24,8 +30,7 @@ public class VladAutomatic : BaseGitPackage public override string DisplayName { get; set; } = "SD.Next Web UI"; public override string Author => "vladmandic"; public override string LicenseType => "AGPL-3.0"; - public override string LicenseUrl => - "https://github.com/vladmandic/automatic/blob/master/LICENSE.txt"; + public override string LicenseUrl => "https://github.com/vladmandic/automatic/blob/master/LICENSE.txt"; public override string Blurb => "Stable Diffusion implementation with advanced features"; public override string LaunchCommand => "launch.py"; @@ -39,14 +44,6 @@ public class VladAutomatic : BaseGitPackage public override IEnumerable AvailableTorchVersions => new[] { TorchVersion.Cpu, TorchVersion.Cuda, TorchVersion.DirectMl, TorchVersion.Rocm }; - public VladAutomatic( - IGithubApiCache githubApi, - ISettingsManager settingsManager, - IDownloadService downloadService, - IPrerequisiteHelper prerequisiteHelper - ) - : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { } - // https://github.com/vladmandic/automatic/blob/master/modules/shared.py#L324 public override Dictionary> SharedFolders => new() @@ -85,90 +82,86 @@ public class VladAutomatic : BaseGitPackage [SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")] public override List LaunchOptions => - new() - { + [ new() { Name = "Host", Type = LaunchOptionType.String, DefaultValue = "localhost", - Options = new() { "--server-name" } + Options = ["--server-name"] }, new() { Name = "Port", Type = LaunchOptionType.String, DefaultValue = "7860", - Options = new() { "--port" } + Options = ["--port"] }, new() { Name = "VRAM", Type = LaunchOptionType.Bool, - InitialValue = HardwareHelper - .IterGpuInfo() - .Select(gpu => gpu.MemoryLevel) - .Max() switch + InitialValue = HardwareHelper.IterGpuInfo().Select(gpu => gpu.MemoryLevel).Max() switch { - Level.Low => "--lowvram", - Level.Medium => "--medvram", + MemoryLevel.Low => "--lowvram", + MemoryLevel.Medium => "--medvram", _ => null }, - Options = new() { "--lowvram", "--medvram" } + Options = ["--lowvram", "--medvram"] }, new() { Name = "Auto-Launch Web UI", Type = LaunchOptionType.Bool, - Options = new() { "--autolaunch" } + Options = ["--autolaunch"] }, new() { Name = "Force use of Intel OneAPI XPU backend", Type = LaunchOptionType.Bool, - Options = new() { "--use-ipex" } + Options = ["--use-ipex"] }, new() { Name = "Use DirectML if no compatible GPU is detected", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.PreferDirectML(), - Options = new() { "--use-directml" } + Options = ["--use-directml"] }, new() { Name = "Force use of Nvidia CUDA backend", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.HasNvidiaGpu(), - Options = new() { "--use-cuda" } + Options = ["--use-cuda"] }, new() { Name = "Force use of AMD ROCm backend", Type = LaunchOptionType.Bool, InitialValue = HardwareHelper.PreferRocm(), - Options = new() { "--use-rocm" } + Options = ["--use-rocm"] }, new() { Name = "CUDA Device ID", Type = LaunchOptionType.String, - Options = new() { "--device-id" } + Options = ["--device-id"] }, new() { Name = "API", Type = LaunchOptionType.Bool, - Options = new() { "--api" } + Options = ["--api"] }, new() { Name = "Debug Logging", Type = LaunchOptionType.Bool, - Options = new() { "--debug" } + Options = ["--debug"] }, LaunchOptionDefinition.Extras - }; + ]; public override string ExtraLaunchArguments => ""; @@ -211,9 +204,7 @@ public class VladAutomatic : BaseGitPackage break; default: // CPU - await venvRunner - .CustomInstall("launch.py --debug --test", onConsoleOutput) - .ConfigureAwait(false); + await venvRunner.CustomInstall("launch.py --debug --test", onConsoleOutput).ConfigureAwait(false); break; } @@ -323,20 +314,14 @@ public class VladAutomatic : BaseGitPackage ); await PrerequisiteHelper - .RunGit( - new[] { "checkout", versionOptions.BranchName! }, - onConsoleOutput, - installedPackage.FullPath - ) + .RunGit(new[] { "checkout", versionOptions.BranchName! }, onConsoleOutput, installedPackage.FullPath) .ConfigureAwait(false); var venvRunner = new PyVenvRunner(Path.Combine(installedPackage.FullPath!, "venv")); venvRunner.WorkingDirectory = installedPackage.FullPath!; venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables; - await venvRunner - .CustomInstall("launch.py --upgrade --test", onConsoleOutput) - .ConfigureAwait(false); + await venvRunner.CustomInstall("launch.py --upgrade --test", onConsoleOutput).ConfigureAwait(false); try { @@ -358,12 +343,7 @@ public class VladAutomatic : BaseGitPackage finally { progress?.Report( - new ProgressReport( - 1f, - message: "Update Complete", - isIndeterminate: false, - type: ProgressType.Update - ) + new ProgressReport(1f, message: "Update Complete", isIndeterminate: false, type: ProgressType.Update) ); } @@ -374,10 +354,7 @@ public class VladAutomatic : BaseGitPackage }; } - public override Task SetupModelFolders( - DirectoryPath installDirectory, - SharedFolderMethod sharedFolderMethod - ) + public override Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) { switch (sharedFolderMethod) { @@ -414,54 +391,31 @@ public class VladAutomatic : BaseGitPackage configRoot["vae_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "VAE"); configRoot["lora_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "Lora"); configRoot["lyco_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "LyCORIS"); - configRoot["embeddings_dir"] = Path.Combine( - SettingsManager.ModelsDirectory, - "TextualInversion" - ); - configRoot["hypernetwork_dir"] = Path.Combine( - SettingsManager.ModelsDirectory, - "Hypernetwork" - ); - configRoot["codeformer_models_path"] = Path.Combine( - SettingsManager.ModelsDirectory, - "Codeformer" - ); + configRoot["embeddings_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "TextualInversion"); + configRoot["hypernetwork_dir"] = Path.Combine(SettingsManager.ModelsDirectory, "Hypernetwork"); + configRoot["codeformer_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "Codeformer"); configRoot["gfpgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "GFPGAN"); configRoot["bsrgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "BSRGAN"); configRoot["esrgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "ESRGAN"); - configRoot["realesrgan_models_path"] = Path.Combine( - SettingsManager.ModelsDirectory, - "RealESRGAN" - ); + configRoot["realesrgan_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "RealESRGAN"); configRoot["scunet_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "ScuNET"); configRoot["swinir_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "SwinIR"); configRoot["ldsr_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "LDSR"); configRoot["clip_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "CLIP"); - configRoot["control_net_models_path"] = Path.Combine( - SettingsManager.ModelsDirectory, - "ControlNet" - ); + configRoot["control_net_models_path"] = Path.Combine(SettingsManager.ModelsDirectory, "ControlNet"); - var configJsonStr = JsonSerializer.Serialize( - configRoot, - new JsonSerializerOptions { WriteIndented = true } - ); + var configJsonStr = JsonSerializer.Serialize(configRoot, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(configJsonPath, configJsonStr); return Task.CompletedTask; } - public override Task UpdateModelFolders( - DirectoryPath installDirectory, - SharedFolderMethod sharedFolderMethod - ) => + public override Task UpdateModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod) => sharedFolderMethod switch { - SharedFolderMethod.Symlink - => base.UpdateModelFolders(installDirectory, sharedFolderMethod), + SharedFolderMethod.Symlink => base.UpdateModelFolders(installDirectory, sharedFolderMethod), SharedFolderMethod.None => Task.CompletedTask, - SharedFolderMethod.Configuration - => SetupModelFolders(installDirectory, sharedFolderMethod), + SharedFolderMethod.Configuration => SetupModelFolders(installDirectory, sharedFolderMethod), _ => Task.CompletedTask }; @@ -471,8 +425,7 @@ public class VladAutomatic : BaseGitPackage ) => sharedFolderMethod switch { - SharedFolderMethod.Symlink - => base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod), + SharedFolderMethod.Symlink => base.RemoveModelFolderLinks(installDirectory, sharedFolderMethod), SharedFolderMethod.None => Task.CompletedTask, SharedFolderMethod.Configuration => RemoveConfigSettings(installDirectory), _ => Task.CompletedTask @@ -523,10 +476,7 @@ public class VladAutomatic : BaseGitPackage configRoot.Remove("clip_models_path"); configRoot.Remove("control_net_models_path"); - var configJsonStr = JsonSerializer.Serialize( - configRoot, - new JsonSerializerOptions { WriteIndented = true } - ); + var configJsonStr = JsonSerializer.Serialize(configRoot, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(configJsonPath, configJsonStr); return Task.CompletedTask; diff --git a/StabilityMatrix.Core/Models/Packages/VoltaML.cs b/StabilityMatrix.Core/Models/Packages/VoltaML.cs index 933e4fb3..95acb970 100644 --- a/StabilityMatrix.Core/Models/Packages/VoltaML.cs +++ b/StabilityMatrix.Core/Models/Packages/VoltaML.cs @@ -10,21 +10,23 @@ using StabilityMatrix.Core.Services; namespace StabilityMatrix.Core.Models.Packages; [Singleton(typeof(BasePackage))] -public class VoltaML : BaseGitPackage +public class VoltaML( + IGithubApiCache githubApi, + ISettingsManager settingsManager, + IDownloadService downloadService, + IPrerequisiteHelper prerequisiteHelper +) : BaseGitPackage(githubApi, settingsManager, downloadService, prerequisiteHelper) { public override string Name => "voltaML-fast-stable-diffusion"; public override string DisplayName { get; set; } = "VoltaML"; public override string Author => "VoltaML"; public override string LicenseType => "GPL-3.0"; - public override string LicenseUrl => - "https://github.com/VoltaML/voltaML-fast-stable-diffusion/blob/main/License"; + public override string LicenseUrl => "https://github.com/VoltaML/voltaML-fast-stable-diffusion/blob/main/License"; public override string Blurb => "Fast Stable Diffusion with support for AITemplate"; public override string LaunchCommand => "main.py"; public override Uri PreviewImageUri => - new( - "https://github.com/LykosAI/StabilityMatrix/assets/13956642/d9a908ed-5665-41a5-a380-98458f4679a8" - ); + new("https://github.com/LykosAI/StabilityMatrix/assets/13956642/d9a908ed-5665-41a5-a380-98458f4679a8"); public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Simple; @@ -32,14 +34,6 @@ public class VoltaML : BaseGitPackage // so we'll just limit to commit mode to be more consistent public override bool ShouldIgnoreReleases => true; - public VoltaML( - IGithubApiCache githubApi, - ISettingsManager settingsManager, - IDownloadService downloadService, - IPrerequisiteHelper prerequisiteHelper - ) - : base(githubApi, settingsManager, downloadService, prerequisiteHelper) { } - // https://github.com/VoltaML/voltaML-fast-stable-diffusion/blob/main/main.py#L86 public override Dictionary> SharedFolders => new() @@ -165,16 +159,10 @@ public class VoltaML : BaseGitPackage await venvRunner.Setup(true, onConsoleOutput).ConfigureAwait(false); // Install requirements - progress?.Report( - new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true) - ); - await venvRunner - .PipInstall("rich packaging python-dotenv", onConsoleOutput) - .ConfigureAwait(false); - - progress?.Report( - new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false) - ); + progress?.Report(new ProgressReport(-1, "Installing Package Requirements", isIndeterminate: true)); + await venvRunner.PipInstall("rich packaging python-dotenv", onConsoleOutput).ConfigureAwait(false); + + progress?.Report(new ProgressReport(1, "Installing Package Requirements", isIndeterminate: false)); } public override async Task RunPackage( diff --git a/StabilityMatrix.Core/Models/Settings/HolidayMode.cs b/StabilityMatrix.Core/Models/Settings/HolidayMode.cs new file mode 100644 index 00000000..b03cba1e --- /dev/null +++ b/StabilityMatrix.Core/Models/Settings/HolidayMode.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace StabilityMatrix.Core.Models.Settings; + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum HolidayMode +{ + Automatic, + Enabled, + Disabled +} diff --git a/StabilityMatrix.Core/Models/Settings/Settings.cs b/StabilityMatrix.Core/Models/Settings/Settings.cs index 897b2fdd..d9b8d7c0 100644 --- a/StabilityMatrix.Core/Models/Settings/Settings.cs +++ b/StabilityMatrix.Core/Models/Settings/Settings.cs @@ -6,7 +6,6 @@ using StabilityMatrix.Core.Models.Update; namespace StabilityMatrix.Core.Models.Settings; -[JsonSerializable(typeof(Settings))] public class Settings { public int? Version { get; set; } = 1; @@ -59,6 +58,8 @@ public class Settings public bool IsNavExpanded { get; set; } public bool IsImportAsConnected { get; set; } public bool ShowConnectedModelImages { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] public SharedFolderType? SharedFolderVisibleCategories { get; set; } = SharedFolderType.StableDiffusion | SharedFolderType.Lora | SharedFolderType.LyCORIS; @@ -108,6 +109,13 @@ public class Settings public Size InferenceImageSize { get; set; } = new(150, 190); public Size OutputsImageSize { get; set; } = new(300, 300); + public HolidayMode HolidayModeSetting { get; set; } = HolidayMode.Automatic; + + [JsonIgnore] + public bool IsHolidayModeActive => + HolidayModeSetting == HolidayMode.Automatic + ? DateTimeOffset.Now.Month == 12 + : HolidayModeSetting == HolidayMode.Enabled; public void RemoveInstalledPackageAndUpdateActive(InstalledPackage package) { @@ -158,8 +166,13 @@ public class Settings return new CultureInfo("zh-Hant"); } - return supportedCultures.Contains(systemCulture.Name) - ? systemCulture - : new CultureInfo("en-US"); + return supportedCultures.Contains(systemCulture.Name) ? systemCulture : new CultureInfo("en-US"); } } + +[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] +[JsonSerializable(typeof(Settings))] +[JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(int))] +[JsonSerializable(typeof(string))] +internal partial class SettingsSerializerContext : JsonSerializerContext; diff --git a/StabilityMatrix.Core/Models/SharedFolderType.cs b/StabilityMatrix.Core/Models/SharedFolderType.cs index 4cc66439..63599311 100644 --- a/StabilityMatrix.Core/Models/SharedFolderType.cs +++ b/StabilityMatrix.Core/Models/SharedFolderType.cs @@ -8,6 +8,7 @@ namespace StabilityMatrix.Core.Models; [Flags] public enum SharedFolderType { + [Description("Base Models")] StableDiffusion = 1 << 0, Lora = 1 << 1, LyCORIS = 1 << 2, @@ -32,5 +33,10 @@ public enum SharedFolderType ScuNET = 1 << 19, GLIGEN = 1 << 20, AfterDetailer = 1 << 21, - IpAdapter = 1 << 22 + IpAdapter = 1 << 22, + T2IAdapter = 1 << 23, + + InvokeIpAdapters15 = 1 << 24, + InvokeIpAdaptersXl = 1 << 25, + InvokeClipVision = 1 << 26, } diff --git a/StabilityMatrix.Core/Processes/ProcessArgsBuilder.cs b/StabilityMatrix.Core/Processes/ProcessArgsBuilder.cs index 00a0e812..1b0dbfa7 100644 --- a/StabilityMatrix.Core/Processes/ProcessArgsBuilder.cs +++ b/StabilityMatrix.Core/Processes/ProcessArgsBuilder.cs @@ -1,6 +1,5 @@ -using System.Diagnostics; +using System.Collections.Immutable; using System.Diagnostics.Contracts; -using OneOf; namespace StabilityMatrix.Core.Processes; @@ -9,14 +8,7 @@ namespace StabilityMatrix.Core.Processes; /// public record ProcessArgsBuilder { - protected ProcessArgsBuilder() { } - - public ProcessArgsBuilder(params Argument[] arguments) - { - Arguments = arguments.ToList(); - } - - public List Arguments { get; init; } = new(); + public IImmutableList Arguments { get; init; } = ImmutableArray.Empty; private IEnumerable ToStringArgs() { @@ -34,6 +26,11 @@ public record ProcessArgsBuilder } } + public ProcessArgsBuilder(params Argument[] arguments) + { + Arguments = arguments.ToImmutableArray(); + } + /// public override string ToString() { @@ -45,8 +42,7 @@ public record ProcessArgsBuilder return ToStringArgs().ToArray(); } - public static implicit operator ProcessArgs(ProcessArgsBuilder builder) => - builder.ToProcessArgs(); + public static implicit operator ProcessArgs(ProcessArgsBuilder builder) => builder.ToProcessArgs(); } public static class ProcessArgBuilderExtensions @@ -55,7 +51,33 @@ public static class ProcessArgBuilderExtensions public static T AddArg(this T builder, Argument argument) where T : ProcessArgsBuilder { - return builder with { Arguments = builder.Arguments.Append(argument).ToList() }; + return builder with { Arguments = builder.Arguments.Add(argument) }; + } + + [Pure] + public static T AddArgs(this T builder, params Argument[] argument) + where T : ProcessArgsBuilder + { + return builder with { Arguments = builder.Arguments.AddRange(argument) }; + } + + [Pure] + public static T UpdateArg(this T builder, string key, Argument argument) + where T : ProcessArgsBuilder + { + var oldArg = builder + .Arguments + .FirstOrDefault(x => x.Match(stringArg => stringArg == key, tupleArg => tupleArg.Item1 == key)); + + if (oldArg is null) + { + return builder.AddArg(argument); + } + + return builder with + { + Arguments = builder.Arguments.Replace(oldArg, argument) + }; } [Pure] @@ -64,15 +86,10 @@ public static class ProcessArgBuilderExtensions { return builder with { - Arguments = builder.Arguments - .Where( - x => - x.Match( - stringArg => stringArg != argumentKey, - tupleArg => tupleArg.Item1 != argumentKey - ) - ) - .ToList() + Arguments = builder + .Arguments + .Where(x => x.Match(stringArg => stringArg != argumentKey, tupleArg => tupleArg.Item1 != argumentKey)) + .ToImmutableArray() }; } } diff --git a/StabilityMatrix.Core/Processes/ProcessRunner.cs b/StabilityMatrix.Core/Processes/ProcessRunner.cs index f29fc72c..868eb247 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); @@ -331,19 +331,10 @@ public static class ProcessRunner { // Quote arguments containing spaces var args = string.Join(" ", arguments.Where(s => !string.IsNullOrEmpty(s)).Select(Quote)); - return StartAnsiProcess( - fileName, - args, - workingDirectory, - outputDataReceived, - environmentVariables - ); + return StartAnsiProcess(fileName, args, workingDirectory, outputDataReceived, environmentVariables); } - public static async Task RunBashCommand( - string command, - string workingDirectory = "" - ) + public static async Task RunBashCommand(string command, string workingDirectory = "") { // Escape any single quotes in the command var escapedCommand = command.Replace("\"", "\\\""); @@ -381,10 +372,7 @@ public static class ProcessRunner }; } - public static Task RunBashCommand( - IEnumerable commands, - string workingDirectory = "" - ) + public static Task RunBashCommand(IEnumerable commands, string workingDirectory = "") { // Quote arguments containing spaces var args = string.Join(" ", commands.Select(Quote)); @@ -433,9 +421,7 @@ public static class ProcessRunner catch (SystemException) { } throw new ProcessException( - "Process " - + (processName == null ? "" : processName + " ") - + $"failed with exit-code {process.ExitCode}." + "Process " + (processName == null ? "" : processName + " ") + $"failed with exit-code {process.ExitCode}." ); } } diff --git a/StabilityMatrix.Core/Python/PipInstallArgs.cs b/StabilityMatrix.Core/Python/PipInstallArgs.cs index d16aedf7..68e371f7 100644 --- a/StabilityMatrix.Core/Python/PipInstallArgs.cs +++ b/StabilityMatrix.Core/Python/PipInstallArgs.cs @@ -1,4 +1,7 @@ -using StabilityMatrix.Core.Processes; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using StabilityMatrix.Core.Extensions; +using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Python; @@ -9,20 +12,36 @@ public record PipInstallArgs : ProcessArgsBuilder public PipInstallArgs WithTorch(string version = "") => this.AddArg($"torch{version}"); - public PipInstallArgs WithTorchDirectML(string version = "") => - this.AddArg($"torch-directml{version}"); + public PipInstallArgs WithTorchDirectML(string version = "") => this.AddArg($"torch-directml{version}"); - public PipInstallArgs WithTorchVision(string version = "") => - this.AddArg($"torchvision{version}"); + public PipInstallArgs WithTorchVision(string version = "") => this.AddArg($"torchvision{version}"); public PipInstallArgs WithXFormers(string version = "") => this.AddArg($"xformers{version}"); - public PipInstallArgs WithExtraIndex(string indexUrl) => - this.AddArg(("--extra-index-url", indexUrl)); + public PipInstallArgs WithExtraIndex(string indexUrl) => this.AddArg(("--extra-index-url", indexUrl)); public PipInstallArgs WithTorchExtraIndex(string index) => this.AddArg(("--extra-index-url", $"https://download.pytorch.org/whl/{index}")); + public PipInstallArgs WithParsedFromRequirementsTxt( + string requirements, + [StringSyntax(StringSyntaxAttribute.Regex)] string? excludePattern = null + ) + { + var requirementsEntries = requirements + .SplitLines(StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .AsEnumerable(); + + if (excludePattern is not null) + { + var excludeRegex = new Regex($"^{excludePattern}$"); + + requirementsEntries = requirementsEntries.Where(s => !excludeRegex.IsMatch(s)); + } + + return this.AddArgs(requirementsEntries.Select(s => (Argument)s).ToArray()); + } + /// public override string ToString() { diff --git a/StabilityMatrix.Core/Python/PipPackageSpecifier.cs b/StabilityMatrix.Core/Python/PipPackageSpecifier.cs new file mode 100644 index 00000000..5dc58356 --- /dev/null +++ b/StabilityMatrix.Core/Python/PipPackageSpecifier.cs @@ -0,0 +1,87 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using StabilityMatrix.Core.Processes; + +namespace StabilityMatrix.Core.Python; + +public partial record PipPackageSpecifier +{ + public required string Name { get; init; } + + public string? Constraint { get; init; } + + public string? Version { get; init; } + + public string? VersionConstraint => Constraint is null || Version is null ? null : Constraint + Name; + + public static PipPackageSpecifier Parse(string value) + { + var result = TryParse(value, true, out var packageSpecifier); + + Debug.Assert(result); + + return packageSpecifier!; + } + + public static bool TryParse(string value, [NotNullWhen(true)] out PipPackageSpecifier? packageSpecifier) + { + return TryParse(value, false, out packageSpecifier); + } + + private static bool TryParse( + string value, + bool throwOnFailure, + [NotNullWhen(true)] out PipPackageSpecifier? packageSpecifier + ) + { + var match = PackageSpecifierRegex().Match(value); + if (!match.Success) + { + if (throwOnFailure) + { + throw new ArgumentException($"Invalid package specifier: {value}"); + } + + packageSpecifier = null; + return false; + } + + packageSpecifier = new PipPackageSpecifier + { + Name = match.Groups["package_name"].Value, + Constraint = match.Groups["version_constraint"].Value, + Version = match.Groups["version"].Value + }; + + return true; + } + + /// + public override string ToString() + { + return Name + VersionConstraint; + } + + public static implicit operator Argument(PipPackageSpecifier specifier) + { + return specifier.VersionConstraint is null + ? new Argument(specifier.Name) + : new Argument((specifier.Name, specifier.VersionConstraint)); + } + + public static implicit operator PipPackageSpecifier(string specifier) + { + return Parse(specifier); + } + + /// + /// Regex to match a pip package specifier. + /// + [GeneratedRegex( + "(?[a-zA-Z0-9_]+)(?(?==|>=|<=|>|<|~=|!=)([a-zA-Z0-9_.]+))?", + RegexOptions.CultureInvariant, + 1000 + )] + private static partial Regex PackageSpecifierRegex(); +} diff --git a/StabilityMatrix.Core/Services/IMetadataImportService.cs b/StabilityMatrix.Core/Services/IMetadataImportService.cs new file mode 100644 index 00000000..3832d7aa --- /dev/null +++ b/StabilityMatrix.Core/Services/IMetadataImportService.cs @@ -0,0 +1,18 @@ +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.Progress; + +namespace StabilityMatrix.Core.Services; + +public interface IMetadataImportService +{ + Task ScanDirectoryForMissingInfo(DirectoryPath directory, IProgress? progress = null); + + Task GetMetadataForFile( + FilePath filePath, + IProgress? progress = null, + bool forceReimport = false + ); + + Task UpdateExistingMetadata(DirectoryPath directory, IProgress? progress = null); +} diff --git a/StabilityMatrix.Core/Services/MetadataImportService.cs b/StabilityMatrix.Core/Services/MetadataImportService.cs new file mode 100644 index 00000000..8a333dd0 --- /dev/null +++ b/StabilityMatrix.Core/Services/MetadataImportService.cs @@ -0,0 +1,266 @@ +using System.Diagnostics; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.Api; +using StabilityMatrix.Core.Models.Database; +using StabilityMatrix.Core.Models.FileInterfaces; +using StabilityMatrix.Core.Models.Progress; + +namespace StabilityMatrix.Core.Services; + +[Transient(typeof(IMetadataImportService))] +public class MetadataImportService( + ILogger logger, + IDownloadService downloadService, + ModelFinder modelFinder +) : IMetadataImportService +{ + public async Task ScanDirectoryForMissingInfo(DirectoryPath directory, IProgress? progress = null) + { + progress?.Report(new ProgressReport(-1f, "Scanning directory...", isIndeterminate: true)); + + var checkpointsWithoutMetadata = directory + .EnumerateFiles(searchOption: SearchOption.AllDirectories) + .Where(FileHasNoCmInfo) + .ToList(); + + var scanned = 0; + var success = 0; + + foreach (var checkpointFilePath in checkpointsWithoutMetadata) + { + if (scanned == 0) + { + progress?.Report( + new ProgressReport( + current: scanned, + total: checkpointsWithoutMetadata.Count, + $"Scanning directory..." + ) + ); + } + else + { + progress?.Report( + new ProgressReport( + current: scanned, + total: checkpointsWithoutMetadata.Count, + $"{success} files imported successfully" + ) + ); + } + + var fileNameWithoutExtension = checkpointFilePath.NameWithoutExtension; + var cmInfoPath = checkpointFilePath.Directory?.JoinFile($"{fileNameWithoutExtension}.cm-info.json"); + var cmInfoExists = File.Exists(cmInfoPath); + if (cmInfoExists) + continue; + + var hashProgress = new Progress(report => + { + progress?.Report( + new ProgressReport( + current: report.Current ?? 0, + total: report.Total ?? 0, + $"Scanning file {scanned}/{checkpointsWithoutMetadata.Count} ... {report.Percentage}%" + ) + ); + }); + + var blake3 = await GetBlake3Hash(cmInfoPath, checkpointFilePath, hashProgress).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(blake3)) + { + logger.LogWarning($"Blake3 hash was null for {checkpointFilePath}"); + scanned++; + continue; + } + + var modelInfo = await modelFinder.RemoteFindModel(blake3).ConfigureAwait(false); + if (modelInfo == null) + { + logger.LogWarning($"Could not find model for {blake3}"); + scanned++; + continue; + } + + var (model, modelVersion, modelFile) = modelInfo.Value; + + var updatedCmInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTimeOffset.UtcNow); + await updatedCmInfo + .SaveJsonToDirectory(checkpointFilePath.Directory, fileNameWithoutExtension) + .ConfigureAwait(false); + + var image = modelVersion + .Images + ?.FirstOrDefault(img => LocalModelFile.SupportedImageExtensions.Contains(Path.GetExtension(img.Url))); + if (image == null) + { + scanned++; + success++; + continue; + } + + await DownloadImage(image, checkpointFilePath, progress).ConfigureAwait(false); + + scanned++; + success++; + } + + progress?.Report( + new ProgressReport( + current: scanned, + total: checkpointsWithoutMetadata.Count, + $"Metadata found for {success}/{checkpointsWithoutMetadata.Count} files" + ) + ); + } + + private static bool FileHasNoCmInfo(FilePath file) + { + return LocalModelFile.SupportedCheckpointExtensions.Contains(file.Extension) + && !File.Exists(file.Directory?.JoinFile($"{file.NameWithoutExtension}.cm-info.json")); + } + + public async Task UpdateExistingMetadata(DirectoryPath directory, IProgress? progress = null) + { + progress?.Report(new ProgressReport(-1f, "Scanning directory...", isIndeterminate: true)); + + var cmInfoList = new Dictionary(); + foreach (var cmInfoPath in directory.EnumerateFiles("*.cm-info.json", SearchOption.AllDirectories)) + { + var cmInfo = JsonSerializer.Deserialize( + await cmInfoPath.ReadAllTextAsync().ConfigureAwait(false) + ); + if (cmInfo == null) + continue; + + cmInfoList.Add(cmInfoPath, cmInfo); + } + + var success = 1; + foreach (var (filePath, cmInfoValue) in cmInfoList) + { + progress?.Report( + new ProgressReport( + current: success, + total: cmInfoList.Count, + $"Updating metadata {success}/{cmInfoList.Count}" + ) + ); + + var hash = cmInfoValue.Hashes.BLAKE3; + if (string.IsNullOrWhiteSpace(hash)) + continue; + + var modelInfo = await modelFinder.RemoteFindModel(hash).ConfigureAwait(false); + if (modelInfo == null) + { + logger.LogWarning($"Could not find model for {hash}"); + continue; + } + + var (model, modelVersion, modelFile) = modelInfo.Value; + + var updatedCmInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTimeOffset.UtcNow); + + var nameWithoutCmInfo = filePath.NameWithoutExtension.Replace(".cm-info", string.Empty); + await updatedCmInfo.SaveJsonToDirectory(filePath.Directory, nameWithoutCmInfo).ConfigureAwait(false); + + var image = modelVersion + .Images + ?.FirstOrDefault(img => LocalModelFile.SupportedImageExtensions.Contains(Path.GetExtension(img.Url))); + if (image == null) + continue; + + await DownloadImage(image, filePath, progress).ConfigureAwait(false); + + success++; + } + } + + public async Task GetMetadataForFile( + FilePath filePath, + IProgress? progress = null, + bool forceReimport = false + ) + { + progress?.Report(new ProgressReport(-1f, "Getting metadata...", isIndeterminate: true)); + + var fileNameWithoutExtension = filePath.NameWithoutExtension; + var cmInfoPath = filePath.Directory?.JoinFile($"{fileNameWithoutExtension}.cm-info.json"); + var cmInfoExists = File.Exists(cmInfoPath); + if (cmInfoExists && !forceReimport) + return null; + + var hashProgress = new Progress(report => + { + progress?.Report( + new ProgressReport( + current: report.Current ?? 0, + total: report.Total ?? 0, + $"Getting metadata for {filePath} ... {report.Percentage}%" + ) + ); + }); + var blake3 = await GetBlake3Hash(cmInfoPath, filePath, hashProgress).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(blake3)) + { + logger.LogWarning($"Blake3 hash was null for {filePath}"); + return null; + } + + var modelInfo = await modelFinder.RemoteFindModel(blake3).ConfigureAwait(false); + if (modelInfo == null) + { + logger.LogWarning($"Could not find model for {blake3}"); + return null; + } + + var (model, modelVersion, modelFile) = modelInfo.Value; + + var updatedCmInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTimeOffset.UtcNow); + await updatedCmInfo.SaveJsonToDirectory(filePath.Directory, fileNameWithoutExtension).ConfigureAwait(false); + + var image = modelVersion + .Images + ?.FirstOrDefault(img => LocalModelFile.SupportedImageExtensions.Contains(Path.GetExtension(img.Url))); + + if (image == null) + return updatedCmInfo; + + await DownloadImage(image, filePath, progress).ConfigureAwait(false); + + return updatedCmInfo; + } + + private static async Task GetBlake3Hash( + FilePath? cmInfoPath, + FilePath checkpointFilePath, + IProgress hashProgress + ) + { + if (string.IsNullOrWhiteSpace(cmInfoPath?.ToString()) || !File.Exists(cmInfoPath)) + { + return await FileHash.GetBlake3Async(checkpointFilePath, hashProgress).ConfigureAwait(false); + } + + var cmInfo = JsonSerializer.Deserialize( + await cmInfoPath.ReadAllTextAsync().ConfigureAwait(false) + ); + return cmInfo?.Hashes.BLAKE3; + } + + private Task DownloadImage(CivitImage image, FilePath modelFilePath, IProgress? progress) + { + var imageExt = Path.GetExtension(image.Url).TrimStart('.'); + var nameWithoutCmInfo = modelFilePath.NameWithoutExtension.Replace(".cm-info", string.Empty); + var imageDownloadPath = Path.GetFullPath( + Path.Combine(modelFilePath.Directory, $"{nameWithoutCmInfo}.preview.{imageExt}") + ); + return downloadService.DownloadToFileAsync(image.Url, imageDownloadPath, progress); + } +} diff --git a/StabilityMatrix.Core/Services/SettingsManager.cs b/StabilityMatrix.Core/Services/SettingsManager.cs index 0d96f8e8..b70dc13d 100644 --- a/StabilityMatrix.Core/Services/SettingsManager.cs +++ b/StabilityMatrix.Core/Services/SettingsManager.cs @@ -6,7 +6,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using AsyncAwaitBestPractices; using NLog; -using Refit; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; @@ -21,6 +20,7 @@ public class SettingsManager : ISettingsManager { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly ReaderWriterLockSlim FileLock = new(); + private bool isLoaded; private static string GlobalSettingsPath => Path.Combine(Compat.AppDataHome, "global.json"); @@ -108,9 +108,7 @@ public class SettingsManager : ISettingsManager { if (!IsLibraryDirSet) { - throw new InvalidOperationException( - "LibraryDir not set when BeginTransaction was called" - ); + throw new InvalidOperationException("LibraryDir not set when BeginTransaction was called"); } return new SettingsTransaction(this, SaveSettingsAsync); } @@ -136,9 +134,7 @@ public class SettingsManager : ISettingsManager { if (expression.Body is not MemberExpression memberExpression) { - throw new ArgumentException( - $"Expression must be a member expression, not {expression.Body.NodeType}" - ); + throw new ArgumentException($"Expression must be a member expression, not {expression.Body.NodeType}"); } var propertyInfo = memberExpression.Member as PropertyInfo; @@ -189,8 +185,7 @@ public class SettingsManager : ISettingsManager if (args.IsRelay && ReferenceEquals(sender, source)) return; Logger.Trace( - "[RelayPropertyFor] " - + "Settings.{TargetProperty:l} -> {SourceType:l}.{SourceProperty:l}", + "[RelayPropertyFor] " + "Settings.{TargetProperty:l} -> {SourceType:l}.{SourceProperty:l}", targetPropertyName, sourceTypeName, propertyName @@ -206,8 +201,7 @@ public class SettingsManager : ISettingsManager return; Logger.Trace( - "[RelayPropertyFor] " - + "{SourceType:l}.{SourceProperty:l} -> Settings.{TargetProperty:l}", + "[RelayPropertyFor] " + "{SourceType:l}.{SourceProperty:l} -> Settings.{TargetProperty:l}", sourceTypeName, propertyName, targetPropertyName @@ -232,10 +226,7 @@ public class SettingsManager : ISettingsManager } // Invoke property changed event, passing along sender - SettingsPropertyChanged?.Invoke( - sender, - new RelayPropertyChangedEventArgs(targetPropertyName, true) - ); + SettingsPropertyChanged?.Invoke(sender, new RelayPropertyChangedEventArgs(targetPropertyName, true)); }; // Set initial value if requested @@ -276,8 +267,9 @@ public class SettingsManager : ISettingsManager // 0. Check Override if (!string.IsNullOrEmpty(LibraryDirOverride)) { - Logger.Info("Using library override path: {Path}", LibraryDirOverride); - LibraryDir = LibraryDirOverride; + var fullOverridePath = Path.GetFullPath(LibraryDirOverride); + Logger.Info("Using library override path: {Path}", fullOverridePath); + LibraryDir = fullOverridePath; SetStaticLibraryPaths(); LoadSettings(); return true; @@ -339,10 +331,7 @@ public class SettingsManager : ISettingsManager var libraryJsonFile = Compat.AppDataHome.JoinFile("library.json"); var library = new LibrarySettings { LibraryPath = path }; - var libraryJson = JsonSerializer.Serialize( - library, - new JsonSerializerOptions { WriteIndented = true } - ); + var libraryJson = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true }); libraryJsonFile.WriteAllText(libraryJson); // actually create the LibraryPath directory @@ -464,9 +453,7 @@ public class SettingsManager : ISettingsManager public void SetLastUpdateCheck(InstalledPackage package) { - var installedPackage = Settings.InstalledPackages.First( - p => p.DisplayName == package.DisplayName - ); + var installedPackage = Settings.InstalledPackages.First(p => p.DisplayName == package.DisplayName); installedPackage.LastUpdateCheck = package.LastUpdateCheck; installedPackage.UpdateAvailable = package.UpdateAvailable; SaveSettings(); @@ -494,14 +481,10 @@ public class SettingsManager : ISettingsManager public string? GetActivePackageHost() { - var package = Settings.InstalledPackages.FirstOrDefault( - x => x.Id == Settings.ActiveInstalledPackageId - ); + var package = Settings.InstalledPackages.FirstOrDefault(x => x.Id == Settings.ActiveInstalledPackageId); if (package == null) return null; - var hostOption = package.LaunchArgs?.FirstOrDefault( - x => x.Name.ToLowerInvariant() == "host" - ); + var hostOption = package.LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "host"); if (hostOption?.OptionValue != null) { return hostOption.OptionValue as string; @@ -511,14 +494,10 @@ public class SettingsManager : ISettingsManager public string? GetActivePackagePort() { - var package = Settings.InstalledPackages.FirstOrDefault( - x => x.Id == Settings.ActiveInstalledPackageId - ); + var package = Settings.InstalledPackages.FirstOrDefault(x => x.Id == Settings.ActiveInstalledPackageId); if (package == null) return null; - var portOption = package.LaunchArgs?.FirstOrDefault( - x => x.Name.ToLowerInvariant() == "port" - ); + var portOption = package.LaunchArgs?.FirstOrDefault(x => x.Name.ToLowerInvariant() == "port"); if (portOption?.OptionValue != null) { return portOption.OptionValue as string; @@ -616,23 +595,36 @@ public class SettingsManager : ISettingsManager FileLock.EnterReadLock(); try { - if (!File.Exists(SettingsPath)) + var settingsFile = new FilePath(SettingsPath); + + if (!settingsFile.Exists) { - File.Create(SettingsPath).Close(); - Settings.Theme = "Dark"; - var defaultSettingsJson = JsonSerializer.Serialize(Settings); - File.WriteAllText(SettingsPath, defaultSettingsJson); + settingsFile.Directory?.Create(); + settingsFile.Create(); + + var settingsJson = JsonSerializer.Serialize(Settings); + settingsFile.WriteAllText(settingsJson); + + Loaded?.Invoke(this, EventArgs.Empty); + isLoaded = true; return; } - var settingsContent = File.ReadAllText(SettingsPath); - var modifiedDefaultSerializerOptions = - SystemTextJsonContentSerializer.GetDefaultJsonSerializerOptions(); - modifiedDefaultSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - Settings = JsonSerializer.Deserialize( - settingsContent, - modifiedDefaultSerializerOptions - )!; + using var fileStream = settingsFile.Info.OpenRead(); + + if (fileStream.Length == 0) + { + Logger.Warn("Settings file is empty, using default settings"); + return; + } + + if ( + JsonSerializer.Deserialize(fileStream, SettingsSerializerContext.Default.Settings) is { } loadedSettings + ) + { + Settings = loadedSettings; + isLoaded = true; + } Loaded?.Invoke(this, EventArgs.Empty); } @@ -644,24 +636,23 @@ public class SettingsManager : ISettingsManager protected virtual void SaveSettings() { - FileLock.TryEnterWriteLock(100000); + FileLock.TryEnterWriteLock(TimeSpan.FromSeconds(30)); try { - if (!File.Exists(SettingsPath)) + var settingsFile = new FilePath(SettingsPath); + + if (!settingsFile.Exists) { - File.Create(SettingsPath).Close(); + settingsFile.Directory?.Create(); + settingsFile.Create(); } - var json = JsonSerializer.Serialize( - Settings, - new JsonSerializerOptions - { - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter() } - } - ); - File.WriteAllText(SettingsPath, json); + if (!isLoaded) + return; + + var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(Settings, SettingsSerializerContext.Default.Settings); + + File.WriteAllBytes(SettingsPath, jsonBytes); } finally { @@ -693,9 +684,9 @@ public class SettingsManager : ISettingsManager { try { - await Task.Delay(delay, cts.Token); + await Task.Delay(delay, cts.Token).ConfigureAwait(false); - await SaveSettingsAsync(); + await SaveSettingsAsync().ConfigureAwait(false); } catch (TaskCanceledException) { } finally diff --git a/StabilityMatrix.Core/StabilityMatrix.Core.csproj b/StabilityMatrix.Core/StabilityMatrix.Core.csproj index 65fa8f17..446f7d9b 100644 --- a/StabilityMatrix.Core/StabilityMatrix.Core.csproj +++ b/StabilityMatrix.Core/StabilityMatrix.Core.csproj @@ -7,6 +7,7 @@ enable true true + true @@ -24,7 +25,10 @@ + + + diff --git a/StabilityMatrix.Tests/Avalonia/Converters/NullableDefaultNumericConverterTests.cs b/StabilityMatrix.Tests/Avalonia/Converters/NullableDefaultNumericConverterTests.cs new file mode 100644 index 00000000..44580393 --- /dev/null +++ b/StabilityMatrix.Tests/Avalonia/Converters/NullableDefaultNumericConverterTests.cs @@ -0,0 +1,44 @@ +using System.Globalization; +using StabilityMatrix.Avalonia.Converters; + +namespace StabilityMatrix.Tests.Avalonia.Converters; + +[TestClass] +public class NullableDefaultNumericConverterTests +{ + [TestMethod] + public void Convert_IntToDecimal_ValueReturnsNullable() + { + const int value = 123; + + var converter = NullableDefaultNumericConverters.IntToDecimal; + + var result = converter.Convert(value, typeof(decimal?), null, CultureInfo.InvariantCulture); + + Assert.AreEqual((decimal?)123, result); + } + + [TestMethod] + public void ConvertBack_IntToDecimal_NullableReturnsDefault() + { + decimal? value = null; + + var converter = NullableDefaultNumericConverters.IntToDecimal; + + var result = converter.ConvertBack(value, typeof(int), null, CultureInfo.InvariantCulture); + + Assert.AreEqual(0, result); + } + + [TestMethod] + public void ConvertBack_IntToDouble_NanReturnsDefault() + { + const double value = double.NaN; + + var converter = new NullableDefaultNumericConverter(); + + var result = converter.ConvertBack(value, typeof(int), null, CultureInfo.InvariantCulture); + + Assert.AreEqual(0, result); + } +} diff --git a/StabilityMatrix.Tests/Core/DefaultUnknownEnumConverterTests.cs b/StabilityMatrix.Tests/Core/DefaultUnknownEnumConverterTests.cs index b0930376..ff0bffc3 100644 --- a/StabilityMatrix.Tests/Core/DefaultUnknownEnumConverterTests.cs +++ b/StabilityMatrix.Tests/Core/DefaultUnknownEnumConverterTests.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Runtime.Serialization; +using System.Text.Json; using System.Text.Json.Serialization; using StabilityMatrix.Core.Converters.Json; @@ -46,6 +47,16 @@ public class DefaultUnknownEnumConverterTests Assert.AreEqual(expected, result); } + [TestMethod] + public void TestDeserialize_UnknownEnum_ShouldUseEnumMemberValue() + { + const string json = "\"Value 2\""; + + var result = JsonSerializer.Deserialize(json); + + Assert.AreEqual(UnknownEnum.Value2, result); + } + [TestMethod] public void TestSerialize_DefaultEnum_ShouldConvert() { @@ -56,6 +67,26 @@ public class DefaultUnknownEnumConverterTests Assert.AreEqual(expected, result); } + [TestMethod] + public void TestSerialize_UnknownEnum_ShouldUseEnumMemberValue() + { + const string json = "\"Value 2\""; + + var result = JsonSerializer.Deserialize(json); + + Assert.AreEqual(UnknownEnum.Value2, result); + } + + [TestMethod] + public void TestSerialize_ComplexObject_ShouldUseEnumMemberValue() + { + const string expected = "{\"Key\":\"Value 2\"}"; + + var result = JsonSerializer.Serialize(new { Key = UnknownEnum.Value2 }); + + Assert.AreEqual(expected, result); + } + private enum NormalEnum { Unknown, @@ -68,6 +99,8 @@ public class DefaultUnknownEnumConverterTests { Unknown, Value1, + + [EnumMember(Value = "Value 2")] Value2 } 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 @@ +