Browse Source

Merge branch 'dev' of https://github.com/ionite34/StabilityMatrix into image-to-video

pull/438/head
JT 11 months ago
parent
commit
1bd293d8cd
  1. 6
      CHANGELOG.md
  2. 21
      StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs
  3. 64
      StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs
  4. 106
      StabilityMatrix.Core/Helper/ImageMetadata.cs
  5. 8
      StabilityMatrix.Core/Python/PipPackageInfo.cs
  6. 62
      StabilityMatrix.Core/Python/PyVenvRunner.cs
  7. 75
      StabilityMatrix.Core/Services/MetadataImportService.cs

6
CHANGELOG.md

@ -5,6 +5,12 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.7.5
### Fixed
- Fixed Python Packages manager crash when pip list returns warnings in json
- Fixed slowdown when loading PNGs with large amounts of metadata
- Fixed crash when scanning directories for missing metadata
## v2.7.4
### Changed
- Improved low disk space handling

21
StabilityMatrix.Avalonia/FallbackRamCachedWebImageLoader.cs

@ -1,9 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using AsyncImageLoader.Loaders;
@ -45,12 +43,11 @@ public class FallbackRamCachedWebImageLoader : RamCachedWebImageLoader
{
try
{
if (url.EndsWith("png", StringComparison.OrdinalIgnoreCase))
{
return await LoadPngAsync(url);
}
if (!url.EndsWith("png", StringComparison.OrdinalIgnoreCase))
return new Bitmap(url);
using var stream = ImageMetadata.BuildImageWithoutMetadata(url);
return stream == null ? new Bitmap(url) : new Bitmap(stream);
}
catch (Exception e)
{
@ -106,14 +103,4 @@ public class FallbackRamCachedWebImageLoader : RamCachedWebImageLoader
}
}
}
private async Task<Bitmap> LoadPngAsync(string url)
{
using var fileStream = new BinaryReader(File.OpenRead(url));
var imageBytes = ImageMetadata.BuildImageWithoutMetadata(fileStream).ToArray();
using var memoryStream = new MemoryStream();
await memoryStream.WriteAsync(imageBytes, 0, imageBytes.Length);
memoryStream.Position = 0;
return new Bitmap(memoryStream);
}
}

64
StabilityMatrix.Avalonia/ViewModels/OutputsPageViewModel.cs

@ -135,7 +135,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
delay: TimeSpan.FromMilliseconds(250)
);
RefreshCategories();
RefreshCategories(false);
}
public override void OnLoaded()
@ -211,12 +211,14 @@ public partial class OutputsPageViewModel : PageViewModelBase
var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item.ImageFile };
using var onNext = Observable
.FromEventPattern<DirectionalNavigationEventArgs>(vm, nameof(ImageViewerViewModel.NavigationRequested))
.FromEventPattern<DirectionalNavigationEventArgs>(
vm,
nameof(ImageViewerViewModel.NavigationRequested)
)
.Subscribe(ctx =>
{
Dispatcher
.UIThread
.InvokeAsync(async () =>
.UIThread.InvokeAsync(async () =>
{
var sender = (ImageViewerViewModel)ctx.Sender!;
var newIndex = currentIndex + (ctx.EventArgs.IsNext ? 1 : -1);
@ -224,7 +226,9 @@ public partial class OutputsPageViewModel : PageViewModelBase
if (newIndex >= 0 && newIndex < Outputs.Count)
{
var newImage = Outputs[newIndex];
var newImageSource = new ImageSource(new FilePath(newImage.ImageFile.AbsolutePath));
var newImageSource = new ImageSource(
new FilePath(newImage.ImageFile.AbsolutePath)
);
// Preload
await newImageSource.GetBitmapAsync();
@ -251,7 +255,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
public void Refresh()
{
Dispatcher.UIThread.Post(RefreshCategories);
Dispatcher.UIThread.Post(() => RefreshCategories());
Dispatcher.UIThread.Post(OnLoaded);
}
@ -376,9 +380,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
public async Task ConsolidateImages()
{
var stackPanel = new StackPanel();
stackPanel
.Children
.Add(
stackPanel.Children.Add(
new TextBlock
{
Text = Resources.Label_ConsolidateExplanation,
@ -393,9 +395,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
continue;
}
stackPanel
.Children
.Add(
stackPanel.Children.Add(
new CheckBox
{
Content = $"{category.Name} ({category.Path})",
@ -426,7 +426,10 @@ public partial class OutputsPageViewModel : PageViewModelBase
foreach (var category in stackPanel.Children.OfType<CheckBox>().Where(c => c.IsChecked == true))
{
if (string.IsNullOrWhiteSpace(category.Tag?.ToString()) || !Directory.Exists(category.Tag?.ToString()))
if (
string.IsNullOrWhiteSpace(category.Tag?.ToString())
|| !Directory.Exists(category.Tag?.ToString())
)
continue;
var directory = category.Tag.ToString();
@ -482,7 +485,10 @@ public partial class OutputsPageViewModel : PageViewModelBase
if (
!Directory.Exists(directory)
&& (SelectedCategory.Path != settingsManager.ImagesDirectory || SelectedOutputType != SharedOutputType.All)
&& (
SelectedCategory.Path != settingsManager.ImagesDirectory
|| SelectedOutputType != SharedOutputType.All
)
)
{
Directory.CreateDirectory(directory);
@ -504,7 +510,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
}
}
private void RefreshCategories()
private void RefreshCategories(bool updateProperty = true)
{
if (Design.IsDesignMode)
return;
@ -515,9 +521,7 @@ public partial class OutputsPageViewModel : PageViewModelBase
var previouslySelectedCategory = SelectedCategory;
var packageCategories = settingsManager
.Settings
.InstalledPackages
.Where(x => !x.UseSharedOutputFolder)
.Settings.InstalledPackages.Where(x => !x.UseSharedOutputFolder)
.Select(packageFactory.GetPackagePair)
.WhereNotNull()
.Where(p => p.BasePackage.SharedOutputFolders != null && p.BasePackage.SharedOutputFolders.Any())
@ -525,7 +529,10 @@ public partial class OutputsPageViewModel : PageViewModelBase
pair =>
new PackageOutputCategory
{
Path = Path.Combine(pair.InstalledPackage.FullPath!, pair.BasePackage.OutputFolderName),
Path = Path.Combine(
pair.InstalledPackage.FullPath!,
pair.BasePackage.OutputFolderName
),
Name = pair.InstalledPackage.DisplayName ?? ""
}
)
@ -533,7 +540,11 @@ public partial class OutputsPageViewModel : PageViewModelBase
packageCategories.Insert(
0,
new PackageOutputCategory { Path = settingsManager.ImagesDirectory, Name = "Shared Output Folder" }
new PackageOutputCategory
{
Path = settingsManager.ImagesDirectory,
Name = "Shared Output Folder"
}
);
packageCategories.Insert(
@ -542,7 +553,18 @@ public partial class OutputsPageViewModel : PageViewModelBase
);
Categories = new ObservableCollection<PackageOutputCategory>(packageCategories);
if (updateProperty)
{
SelectedCategory =
Categories.FirstOrDefault(x => x.Name == previouslySelectedCategory?.Name) ?? Categories.First();
Categories.FirstOrDefault(x => x.Name == previouslySelectedCategory?.Name)
?? Categories.First();
}
else
{
selectedCategory =
Categories.FirstOrDefault(x => x.Name == previouslySelectedCategory?.Name)
?? Categories.First();
}
}
}

106
StabilityMatrix.Core/Helper/ImageMetadata.cs

@ -1,10 +1,7 @@
using System.Diagnostics;
using System.Text;
using System.Text;
using System.Text.Json;
using ExifLibrary;
using MetadataExtractor;
using MetadataExtractor.Formats.Png;
using Microsoft.VisualBasic;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
@ -16,13 +13,10 @@ public class ImageMetadata
{
private IReadOnlyList<Directory>? Directories { get; set; }
private static readonly byte[] PngHeader = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
private static readonly byte[] PngHeader = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
private static readonly byte[] Idat = "IDAT"u8.ToArray();
private static readonly byte[] Text = "tEXt"u8.ToArray();
private static readonly byte[] Riff = "RIFF"u8.ToArray();
private static readonly byte[] Webp = "WEBP"u8.ToArray();
public static ImageMetadata ParseFile(FilePath path)
{
return new ImageMetadata { Directories = ImageMetadataReader.ReadMetadata(path) };
@ -186,21 +180,24 @@ public class ImageMetadata
return string.Empty;
}
public static IEnumerable<byte> BuildImageWithoutMetadata(BinaryReader byteStream)
public static MemoryStream? BuildImageWithoutMetadata(FilePath imagePath)
{
var bytes = new List<byte>();
using var byteStream = new BinaryReader(File.OpenRead(imagePath));
byteStream.BaseStream.Position = 0;
// Read first 8 bytes and make sure they match the png header
if (!byteStream.ReadBytes(8).SequenceEqual(PngHeader))
{
return Array.Empty<byte>();
return null;
}
bytes.AddRange(PngHeader);
var memoryStream = new MemoryStream();
memoryStream.Write(PngHeader);
// add the IHDR chunk
var ihdrStuff = byteStream.ReadBytes(25);
bytes.AddRange(ihdrStuff);
memoryStream.Write(ihdrStuff);
// find IDATs
while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4)
{
var chunkSizeBytes = byteStream.ReadBytes(4);
@ -217,84 +214,17 @@ public class ImageMetadata
continue;
}
bytes.AddRange(chunkSizeBytes);
bytes.AddRange(chunkTypeBytes);
memoryStream.Write(chunkSizeBytes);
memoryStream.Write(chunkTypeBytes);
var idatBytes = byteStream.ReadBytes(chunkSize);
bytes.AddRange(idatBytes);
memoryStream.Write(idatBytes);
var crcBytes = byteStream.ReadBytes(4);
bytes.AddRange(crcBytes);
memoryStream.Write(crcBytes);
}
// Add IEND chunk
bytes.AddRange([0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82]);
return bytes;
}
public static async Task<string> ReadTextChunkFromWebp(FilePath filePath, ExifTag exifTag)
{
var sw = Stopwatch.StartNew();
try
{
await using var memoryStream = Utilities.GetMemoryStreamFromFile(filePath);
if (memoryStream is null)
return string.Empty;
var exifChunks = GetExifChunks(memoryStream);
if (exifChunks.Length == 0)
return string.Empty;
// write exifChunks to new memoryStream but skip first 6 bytes
using var newMemoryStream = new MemoryStream(exifChunks[6..]);
newMemoryStream.Seek(0, SeekOrigin.Begin);
var img = new MyTiffFile(newMemoryStream, Encoding.UTF8);
return img.Properties[exifTag]?.Value?.ToString() ?? string.Empty;
}
finally
{
sw.Stop();
Console.WriteLine($"ReadTextChunkFromWebp took {sw.ElapsedMilliseconds}ms");
}
}
private static byte[] GetExifChunks(MemoryStream memoryStream)
{
using var byteStream = new BinaryReader(memoryStream);
byteStream.BaseStream.Position = 0;
// Read first 8 bytes and make sure they match the RIFF header
if (!byteStream.ReadBytes(4).SequenceEqual(Riff))
{
return Array.Empty<byte>();
}
// skip 4 bytes then read next 4 for webp header
byteStream.BaseStream.Position += 4;
if (!byteStream.ReadBytes(4).SequenceEqual(Webp))
{
return Array.Empty<byte>();
}
while (byteStream.BaseStream.Position < byteStream.BaseStream.Length - 4)
{
var chunkType = Encoding.UTF8.GetString(byteStream.ReadBytes(4));
var chunkSize = BitConverter.ToInt32(byteStream.ReadBytes(4).ToArray());
if (chunkType != "EXIF")
{
// skip chunk data
byteStream.BaseStream.Position += chunkSize;
continue;
}
var exifStart = byteStream.BaseStream.Position;
var exifBytes = byteStream.ReadBytes(chunkSize);
var exif = Encoding.UTF8.GetString(exifBytes);
Debug.WriteLine($"Found exif chunk of size {chunkSize}");
return exifBytes;
}
return Array.Empty<byte>();
memoryStream.Write([0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82]);
memoryStream.Position = 0;
return memoryStream;
}
}

8
StabilityMatrix.Core/Python/PipPackageInfo.cs

@ -1,7 +1,13 @@
namespace StabilityMatrix.Core.Python;
using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Python;
public readonly record struct PipPackageInfo(
string Name,
string Version,
string? EditableProjectLocation = null
);
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)]
[JsonSerializable(typeof(PipPackageInfo))]
internal partial class PipPackageInfoSerializerContext : JsonSerializerContext;

62
StabilityMatrix.Core/Python/PyVenvRunner.cs

@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
@ -119,13 +120,7 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
RootPath.Create();
// Create venv (copy mode if windows)
var args = new string[]
{
"-m",
"virtualenv",
Compat.IsWindows ? "--always-copy" : "",
RootPath
};
var args = new string[] { "-m", "virtualenv", Compat.IsWindows ? "--always-copy" : "", RootPath };
var venvProc = ProcessRunner.StartAnsiProcess(
PyRunner.PythonExePath,
@ -323,24 +318,28 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
);
}
// Use only first line, since there might be pip update messages later
if (
result.StandardOutput
?.SplitLines(StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault()
is not { } firstLine
// There may be warning lines before the Json line, or update messages after
// Filter to find the first line that starts with [
var jsonLine = result
.StandardOutput?.SplitLines(
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries
)
.Select(line => line.Trim())
.FirstOrDefault(
line =>
line.StartsWith("[", StringComparison.OrdinalIgnoreCase)
&& line.EndsWith("]", StringComparison.OrdinalIgnoreCase)
);
if (jsonLine is null)
{
return new List<PipPackageInfo>();
return [];
}
return JsonSerializer.Deserialize<List<PipPackageInfo>>(
firstLine,
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicies.SnakeCaseLower
}
) ?? new List<PipPackageInfo>();
jsonLine,
PipPackageInfoSerializerContext.Default.Options
) ?? [];
}
/// <summary>
@ -408,12 +407,7 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
}
var result = await ProcessRunner
.GetProcessResultAsync(
PythonPath,
args,
WorkingDirectory?.FullPath,
EnvironmentVariables
)
.GetProcessResultAsync(PythonPath, args, WorkingDirectory?.FullPath, EnvironmentVariables)
.ConfigureAwait(false);
// Check return code
@ -426,8 +420,8 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
if (
string.IsNullOrEmpty(result.StandardOutput)
|| result.StandardOutput!
.SplitLines()
|| result
.StandardOutput!.SplitLines()
.Any(l => l.StartsWith("ERROR: No matching distribution found"))
)
{
@ -498,11 +492,7 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
);
await process.WaitForExitAsync().ConfigureAwait(false);
return new ProcessResult
{
ExitCode = process.ExitCode,
StandardOutput = output.ToString()
};
return new ProcessResult { ExitCode = process.ExitCode, StandardOutput = output.ToString() };
}
[MemberNotNull(nameof(Process))]
@ -642,9 +632,7 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
Process.Kill(true);
try
{
await Process
.WaitForExitAsync(new CancellationTokenSource(5000).Token)
.ConfigureAwait(false);
await Process.WaitForExitAsync(new CancellationTokenSource(5000).Token).ConfigureAwait(false);
}
catch (OperationCanceledException e)
{

75
StabilityMatrix.Core/Services/MetadataImportService.cs

@ -1,5 +1,4 @@
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
@ -18,7 +17,10 @@ public class MetadataImportService(
ModelFinder modelFinder
) : IMetadataImportService
{
public async Task ScanDirectoryForMissingInfo(DirectoryPath directory, IProgress<ProgressReport>? progress = null)
public async Task ScanDirectoryForMissingInfo(
DirectoryPath directory,
IProgress<ProgressReport>? progress = null
)
{
progress?.Report(new ProgressReport(-1f, "Scanning directory...", isIndeterminate: true));
@ -54,7 +56,9 @@ public class MetadataImportService(
}
var fileNameWithoutExtension = checkpointFilePath.NameWithoutExtension;
var cmInfoPath = checkpointFilePath.Directory?.JoinFile($"{fileNameWithoutExtension}.cm-info.json");
var cmInfoPath = checkpointFilePath.Directory?.JoinFile(
$"{fileNameWithoutExtension}.cm-info.json"
);
var cmInfoExists = File.Exists(cmInfoPath);
if (cmInfoExists)
continue;
@ -70,7 +74,10 @@ public class MetadataImportService(
);
});
var blake3 = await GetBlake3Hash(cmInfoPath, checkpointFilePath, hashProgress).ConfigureAwait(false);
try
{
var blake3 = await GetBlake3Hash(cmInfoPath, checkpointFilePath, hashProgress)
.ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(blake3))
{
logger.LogWarning($"Blake3 hash was null for {checkpointFilePath}");
@ -88,14 +95,19 @@ public class MetadataImportService(
var (model, modelVersion, modelFile) = modelInfo.Value;
var updatedCmInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTimeOffset.UtcNow);
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)));
var image = modelVersion.Images?.FirstOrDefault(
img => LocalModelFile.SupportedImageExtensions.Contains(Path.GetExtension(img.Url))
);
if (image == null)
{
scanned++;
@ -108,6 +120,12 @@ public class MetadataImportService(
scanned++;
success++;
}
catch (Exception e)
{
logger.LogError(e, "Error while scanning {checkpointFilePath}", checkpointFilePath);
scanned++;
}
}
progress?.Report(
new ProgressReport(
@ -124,7 +142,10 @@ public class MetadataImportService(
&& !File.Exists(file.Directory?.JoinFile($"{file.NameWithoutExtension}.cm-info.json"));
}
public async Task UpdateExistingMetadata(DirectoryPath directory, IProgress<ProgressReport>? progress = null)
public async Task UpdateExistingMetadata(
DirectoryPath directory,
IProgress<ProgressReport>? progress = null
)
{
progress?.Report(new ProgressReport(-1f, "Scanning directory...", isIndeterminate: true));
@ -151,6 +172,8 @@ public class MetadataImportService(
)
);
try
{
var hash = cmInfoValue.Hashes.BLAKE3;
if (string.IsNullOrWhiteSpace(hash))
continue;
@ -164,14 +187,21 @@ public class MetadataImportService(
var (model, modelVersion, modelFile) = modelInfo.Value;
var updatedCmInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTimeOffset.UtcNow);
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);
await updatedCmInfo
.SaveJsonToDirectory(filePath.Directory, nameWithoutCmInfo)
.ConfigureAwait(false);
var image = modelVersion
.Images
?.FirstOrDefault(img => LocalModelFile.SupportedImageExtensions.Contains(Path.GetExtension(img.Url)));
var image = modelVersion.Images?.FirstOrDefault(
img => LocalModelFile.SupportedImageExtensions.Contains(Path.GetExtension(img.Url))
);
if (image == null)
continue;
@ -179,6 +209,11 @@ public class MetadataImportService(
success++;
}
catch (Exception e)
{
logger.LogError(e, "Error while updating {filePath}", filePath);
}
}
}
public async Task<ConnectedModelInfo?> GetMetadataForFile(
@ -223,11 +258,13 @@ public class MetadataImportService(
var (model, modelVersion, modelFile) = modelInfo.Value;
var updatedCmInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTimeOffset.UtcNow);
await updatedCmInfo.SaveJsonToDirectory(filePath.Directory, fileNameWithoutExtension).ConfigureAwait(false);
await updatedCmInfo
.SaveJsonToDirectory(filePath.Directory, fileNameWithoutExtension)
.ConfigureAwait(false);
var image = modelVersion
.Images
?.FirstOrDefault(img => LocalModelFile.SupportedImageExtensions.Contains(Path.GetExtension(img.Url)));
var image = modelVersion.Images?.FirstOrDefault(
img => LocalModelFile.SupportedImageExtensions.Contains(Path.GetExtension(img.Url))
);
if (image == null)
return updatedCmInfo;

Loading…
Cancel
Save