Browse Source

Merge branch 'main' into inference

pull/165/head
Ionite 1 year ago committed by GitHub
parent
commit
2ecd0a7936
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      CHANGELOG.md
  2. 9
      StabilityMatrix.Avalonia/Assets.cs
  3. BIN
      StabilityMatrix.Avalonia/Assets/macos-arm64/7zz
  4. 88
      StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt
  5. 63
      StabilityMatrix.Avalonia/Controls/BetterImage.cs
  6. 6
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  7. 4
      StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs
  8. 3
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  9. 238
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
  10. 15
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  11. 19
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs
  12. 59
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
  13. 23
      StabilityMatrix.Avalonia/ViewModels/ProgressManagerViewModel.cs
  14. 8
      StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml
  15. 94
      StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml
  16. 31
      StabilityMatrix.Core/Database/LiteDbContext.cs
  17. 6
      StabilityMatrix.Core/Helper/ArchiveHelper.cs
  18. 2
      StabilityMatrix.Core/Helper/Compat.cs
  19. 22
      StabilityMatrix.Core/Helper/HardwareHelper.cs
  20. 4
      StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs
  21. 27
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  22. 10
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  23. 2
      StabilityMatrix.Core/Models/Packages/InvokeAI.cs
  24. 15
      StabilityMatrix.Core/Python/PyRunner.cs
  25. 7
      StabilityMatrix.Core/Python/PyVenvRunner.cs
  26. 4
      StabilityMatrix.Core/Services/SettingsManager.cs

17
CHANGELOG.md

@ -5,6 +5,23 @@ 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.3.3
### Fixed
- Fixed GPU recognition for Nvidia Tesla GPUs
- Fix checkpoint file index extension identification with some path names
- Fixed issue where config file may be overwritten during Automatic1111 package updates
- Fixed "Directory Not Found" error on startup when previously selected Data directory does not exist
## v2.3.2
### Added
- Added warning for exFAT / FAT32 drives when selecting a data directory
### Fixed
- Automatic1111 and ComfyUI should now install the correct version of pytorch for AMD GPUs
- Fixed "Call from invalid thread" exceptions preventing download completion notifications from showing
- Fixed model preview image downloading with incorrect name
### Changed
- Redesigned "Select Model Version" dialog to include model description and all preview images
## v2.3.1
### Fixed
- Fixed Auto update not appearing in some regions due to date formatting issues

9
StabilityMatrix.Avalonia/Assets.cs

@ -33,17 +33,22 @@ internal static class Assets
[SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
public static AvaloniaResource SevenZipExecutable => Compat.Switch(
(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", (UnixFileMode) 0x777)));
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs", (UnixFileMode) 0777)),
(PlatformKind.MacOS | PlatformKind.Arm,
new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/macos-arm64/7zz", (UnixFileMode) 0x777)));
[SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
public static AvaloniaResource SevenZipLicense => Compat.Switch(
(PlatformKind.Windows, 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")));
(PlatformKind.Linux | PlatformKind.X64, 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")));
public static AvaloniaResource PyScriptSiteCustomize => new(
"avares://StabilityMatrix.Avalonia/Assets/sitecustomize.py");

BIN
StabilityMatrix.Avalonia/Assets/macos-arm64/7zz

Binary file not shown.

88
StabilityMatrix.Avalonia/Assets/macos-arm64/7zz - LICENSE.txt

@ -0,0 +1,88 @@
7-Zip
~~~~~
License for use and distribution
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7-Zip Copyright (C) 1999-2023 Igor Pavlov.
The licenses for 7zz and 7zzs files are:
- The "GNU LGPL" as main license for most of the code
- The "GNU LGPL" with "unRAR license restriction" for some code
- The "BSD 3-clause License" for some code
Redistributions in binary form must reproduce related license information from this file.
Note:
You can use 7-Zip on any computer, including a computer in a commercial
organization. You don't need to register or pay for 7-Zip.
GNU LGPL information
--------------------
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You can receive a copy of the GNU Lesser General Public License from
http://www.gnu.org/
BSD 3-clause License
--------------------
The "BSD 3-clause License" is used for the code in 7z.dll that implements LZFSE data decompression.
That code was derived from the code in the "LZFSE compression library" developed by Apple Inc,
that also uses the "BSD 3-clause License":
----
Copyright (c) 2015-2016, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
----
unRAR license restriction
-------------------------
The decompression engine for RAR archives was developed using source
code of unRAR program.
All copyrights to original unRAR code are owned by Alexander Roshal.
The license for original unRAR code has the following restriction:
The unRAR sources cannot be used to re-create the RAR compression algorithm,
which is proprietary. Distribution of modified unRAR sources in separate form
or as a part of other software is permitted, provided that it is clearly
stated in the documentation and source comments that the code may
not be used to develop a RAR (WinRAR) compatible archiver.
--
Igor Pavlov

63
StabilityMatrix.Avalonia/Controls/BetterImage.cs

@ -1,8 +1,10 @@
using Avalonia;
using System;
using Avalonia;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Controls;
using Avalonia.Controls.Automation.Peers;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Metadata;
@ -77,25 +79,48 @@ public class BetterImage : Control
{
var source = Source;
if (source != null && Bounds.Width > 0 && Bounds.Height > 0)
if (source == null || Bounds is not {Width: > 0, Height: > 0}) return;
var viewPort = new Rect(Bounds.Size);
var sourceSize = source.Size;
var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection);
var scaledSize = sourceSize * scale;
// Calculate starting points for dest
var destX = HorizontalAlignment switch
{
Rect viewPort = new Rect(Bounds.Size);
Size sourceSize = source.Size;
Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection);
Size scaledSize = sourceSize * scale;
Rect destRect = viewPort
.CenterRect(new Rect(scaledSize))
.WithX(0)
.WithY(0)
.Intersect(viewPort);
Rect sourceRect = new Rect(sourceSize)
.CenterRect(new Rect(destRect.Size / scale))
.WithX(0)
.WithY(0);
context.DrawImage(source, sourceRect, destRect);
}
HorizontalAlignment.Left => 0,
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,
_ => throw new ArgumentException(nameof(HorizontalAlignment))
};
var destRect = viewPort
.CenterRect(new Rect(scaledSize))
.WithX(destX)
.WithY(0)
.Intersect(viewPort);
var destRectUnscaledSize = destRect.Size / scale;
var sourceX = HorizontalAlignment switch
{
HorizontalAlignment.Left => 0,
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,
_ => throw new ArgumentException(nameof(HorizontalAlignment))
};
var sourceRect = new Rect(sourceSize)
.CenterRect(new Rect(destRect.Size / scale))
.WithX(sourceX)
.WithY(0);
context.DrawImage(source, sourceRect, destRect);
}
/// <summary>

6
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -340,7 +340,9 @@ public static class DesignData
new()
{
Name = "BB95 Furry Mix",
Description = "v1.0.0",
Description = @"Introducing SnoutMix
A Mix of non-Furry and Furry models such as Furtastic and BB95Furry to create a great variety of anthro AI generation options, but bringing out more detail, still giving a lot of freedom to customise the human aspects, and having great backgrounds, with a focus on something more realistic. Works well with realistic character loras.
The gallery images are often inpainted, but you will get something very similar if copying their data directly. They are inpainted using the same model, therefore all results are possible without anything custom/hidden-away. Controlnet Tiled is applied to enhance them further afterwards. Gallery images were made with same model but before it was renamed",
BaseModel = "SD 1.5",
Files = new List<CivitFile>
{
@ -378,6 +380,8 @@ public static class DesignData
// Sample data for dialogs
vm.Versions = new List<ModelVersionViewModel> {sampleViewModel};
vm.Title = sampleCivitVersions[0].Name;
vm.Description = sampleCivitVersions[0].Description;
vm.SelectedVersionViewModel = sampleViewModel;
});

4
StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs

@ -173,8 +173,8 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
progress?.Report(new ProgressReport(0, "Installing Python", isIndeterminate: true));
await ArchiveHelper.Extract7ZAuto(downloadPath, PythonDir);
// For Linux, move the inner 'python' folder up to the root PythonDir
if (Compat.IsLinux)
// For Unix, move the inner 'python' folder up to the root PythonDir
if (Compat.IsUnix)
{
var innerPythonDir = PythonDir.JoinDir("python");
if (!innerPythonDir.Exists)

3
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -24,6 +24,7 @@
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.2" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.2" />
<PackageReference Include="Avalonia.HtmlRenderer" Version="11.0.0" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.0.1" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
@ -89,6 +90,8 @@
<AvaloniaResource Include="Assets\win-x64\**" Condition="'$(RuntimeIdentifier)' == 'win-x64'" />
<!-- Only for linux-64 -->
<AvaloniaResource Include="Assets\linux-x64\**" Condition="'$(RuntimeIdentifier)' == 'linux-x64'" />
<!-- Only for osx-arm64 -->
<AvaloniaResource Include="Assets\macos-arm64\**" Condition="'$(RuntimeIdentifier)' == 'osx-arm64'" />
</ItemGroup>
<ItemGroup>

238
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs

@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
@ -13,21 +12,18 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using NLog;
using Octokit;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
using Notification = Avalonia.Controls.Notifications.Notification;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
@ -39,7 +35,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly INotificationService notificationService;
public Action<CheckpointBrowserCardViewModel>? OnDownloadStart { get; set; }
public CivitModel CivitModel
@ -54,11 +50,18 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
}
public override bool IsTextVisible => Value > 0;
[ObservableProperty] private Uri? cardImage;
[ObservableProperty] private bool isImporting;
[ObservableProperty] private string updateCardText = string.Empty;
[ObservableProperty] private bool showUpdateCard;
[ObservableProperty]
private Uri? cardImage;
[ObservableProperty]
private bool isImporting;
[ObservableProperty]
private string updateCardText = string.Empty;
[ObservableProperty]
private bool showUpdateCard;
private CivitModel civitModel;
public CheckpointBrowserCardViewModel(
@ -66,7 +69,8 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
ITrackedDownloadService trackedDownloadService,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> dialogFactory,
INotificationService notificationService)
INotificationService notificationService
)
{
this.downloadService = downloadService;
this.trackedDownloadService = trackedDownloadService;
@ -77,7 +81,8 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
// Update image when nsfw setting changes
settingsManager.RegisterPropertyChangedHandler(
s => s.ModelBrowserNsfwEnabled,
_ => Dispatcher.UIThread.Post(UpdateImage));
_ => Dispatcher.UIThread.Post(UpdateImage)
);
}
private void CheckIfInstalled()
@ -88,28 +93,45 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
ShowUpdateCard = true;
return;
}
if (CivitModel.ModelVersions == null) return;
if (CivitModel.ModelVersions == null)
return;
var installedModels = settingsManager.Settings.InstalledModelHashes;
if (!installedModels.Any()) return;
if (!installedModels.Any())
return;
// check if latest version is installed
var latestVersion = CivitModel.ModelVersions.FirstOrDefault();
if (latestVersion == null) return;
var latestVersionInstalled = latestVersion.Files != null && latestVersion.Files.Any(file =>
file is {Type: CivitFileType.Model, Hashes.BLAKE3: not null} &&
installedModels.Contains(file.Hashes.BLAKE3));
if (latestVersion == null)
return;
var latestVersionInstalled =
latestVersion.Files != null
&& latestVersion.Files.Any(
file =>
file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null }
&& installedModels.Contains(file.Hashes.BLAKE3)
);
// check if any of the ModelVersion.Files.Hashes.BLAKE3 hashes are in the installedModels list
var anyVersionInstalled = latestVersionInstalled || CivitModel.ModelVersions.Any(version =>
version.Files != null && version.Files.Any(file =>
file is {Type: CivitFileType.Model, Hashes.BLAKE3: not null} &&
installedModels.Contains(file.Hashes.BLAKE3)));
var anyVersionInstalled =
latestVersionInstalled
|| CivitModel.ModelVersions.Any(
version =>
version.Files != null
&& version.Files.Any(
file =>
file is { Type: CivitFileType.Model, Hashes.BLAKE3: not null }
&& installedModels.Contains(file.Hashes.BLAKE3)
)
);
UpdateCardText = latestVersionInstalled ? "Installed" :
anyVersionInstalled ? "Update Available" : string.Empty;
UpdateCardText = latestVersionInstalled
? "Installed"
: anyVersionInstalled
? "Update Available"
: string.Empty;
ShowUpdateCard = anyVersionInstalled;
}
@ -130,7 +152,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
CardImage = new Uri(image.Url);
return;
}
// If no valid image found, use no image
CardImage = Assets.NoImage;
@ -158,11 +180,16 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
var versions = model.ModelVersions;
if (versions is null || versions.Count == 0)
{
notificationService.Show(new Notification("Model has no versions available",
"This model has no versions available for download", NotificationType.Warning));
notificationService.Show(
new Notification(
"Model has no versions available",
"This model has no versions available for download",
NotificationType.Warning
)
);
return;
}
var dialog = new BetterContentDialog
{
Title = model.Name,
@ -170,20 +197,27 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
IsSecondaryButtonEnabled = false,
IsFooterVisible = false,
MaxDialogWidth = 750,
MaxDialogHeight = 850,
};
var prunedDescription = PruneDescription(model);
var viewModel = dialogFactory.Get<SelectModelVersionViewModel>();
viewModel.Dialog = dialog;
viewModel.Versions = versions.Select(version =>
new ModelVersionViewModel(
settingsManager.Settings.InstalledModelHashes ?? new HashSet<string>(), version))
viewModel.Title = model.Name;
viewModel.Description = prunedDescription;
viewModel.Versions = versions
.Select(
version =>
new ModelVersionViewModel(
settingsManager.Settings.InstalledModelHashes ?? new HashSet<string>(),
version
)
)
.ToImmutableArray();
viewModel.SelectedVersionViewModel = viewModel.Versions[0];
dialog.Content = new SelectModelVersionDialog
{
DataContext = viewModel
};
dialog.Content = new SelectModelVersionDialog { DataContext = viewModel };
var result = await dialog.ShowAsync();
@ -199,18 +233,34 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
await DoImport(model, selectedVersion, selectedFile);
}
private static string PruneDescription(CivitModel model)
{
var prunedDescription = model.Description
.Replace("<br/>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("<br />", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</p>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h1>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h2>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h3>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h4>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h5>", $"{Environment.NewLine}{Environment.NewLine}")
.Replace("</h6>", $"{Environment.NewLine}{Environment.NewLine}");
prunedDescription = HtmlRegex().Replace(prunedDescription, string.Empty);
return prunedDescription;
}
private static async Task<FilePath> SaveCmInfo(
CivitModel model,
CivitModel model,
CivitModelVersion modelVersion,
CivitFile modelFile,
DirectoryPath downloadDirectory)
DirectoryPath downloadDirectory
)
{
var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Name);
var modelInfo =
new ConnectedModelInfo(model, modelVersion, modelFile, DateTime.UtcNow);
var modelInfo = new ConnectedModelInfo(model, modelVersion, modelFile, DateTime.UtcNow);
await modelInfo.SaveJsonToDirectory(downloadDirectory, modelFileName);
var jsonName = $"{modelFileName}.cm-info.json";
return downloadDirectory.JoinFile(jsonName);
}
@ -221,87 +271,103 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
/// <param name="modelVersion"></param>
/// <param name="modelFilePath"></param>
/// <returns>The file path of the saved preview image</returns>
private async Task<FilePath?> SavePreviewImage(CivitModelVersion modelVersion, FilePath modelFilePath)
private async Task<FilePath?> SavePreviewImage(
CivitModelVersion modelVersion,
FilePath modelFilePath
)
{
// Skip if model has no images
if (modelVersion.Images == null || modelVersion.Images.Count == 0)
{
return null;
}
var image = modelVersion.Images[0];
var imageExtension = Path.GetExtension(image.Url).TrimStart('.');
if (imageExtension is "jpg" or "jpeg" or "png")
{
var imageDownloadPath =
modelFilePath.Directory!.JoinFile($"{modelFilePath.Name}.preview.{imageExtension}");
var imageDownloadPath = modelFilePath.Directory!.JoinFile(
$"{modelFilePath.NameWithoutExtension}.preview.{imageExtension}"
);
var imageTask =
downloadService.DownloadToFileAsync(image.Url, imageDownloadPath);
var imageTask = downloadService.DownloadToFileAsync(image.Url, imageDownloadPath);
await notificationService.TryAsync(imageTask, "Could not download preview image");
return imageDownloadPath;
}
return null;
}
private async Task DoImport(CivitModel model, CivitModelVersion? selectedVersion = null,
CivitFile? selectedFile = null)
private async Task DoImport(
CivitModel model,
CivitModelVersion? selectedVersion = null,
CivitFile? selectedFile = null
)
{
IsImporting = true;
Text = "Downloading...";
OnDownloadStart?.Invoke(this);
// Get latest version
var modelVersion = selectedVersion ?? model.ModelVersions?.FirstOrDefault();
if (modelVersion is null)
{
notificationService.Show(new Notification("Model has no versions available",
"This model has no versions available for download", NotificationType.Warning));
notificationService.Show(
new Notification(
"Model has no versions available",
"This model has no versions available for download",
NotificationType.Warning
)
);
Text = "Unable to Download";
return;
}
// Get latest version file
var modelFile = selectedFile ??
modelVersion.Files?.FirstOrDefault(x => x.Type == CivitFileType.Model);
var modelFile =
selectedFile ?? modelVersion.Files?.FirstOrDefault(x => x.Type == CivitFileType.Model);
if (modelFile is null)
{
notificationService.Show(new Notification("Model has no files available",
"This model has no files available for download", NotificationType.Warning));
notificationService.Show(
new Notification(
"Model has no files available",
"This model has no files available for download",
NotificationType.Warning
)
);
Text = "Unable to Download";
return;
}
var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory);
var downloadDirectory =
rootModelsDirectory.JoinDir(model.Type.ConvertTo<SharedFolderType>()
.GetStringValue());
var downloadDirectory = rootModelsDirectory.JoinDir(
model.Type.ConvertTo<SharedFolderType>().GetStringValue()
);
// Folders might be missing if user didn't install any packages yet
downloadDirectory.Create();
var downloadPath = downloadDirectory.JoinFile(modelFile.Name);
// Download model info and preview first
var cmInfoPath = await SaveCmInfo(model, modelVersion, modelFile, downloadDirectory);
var previewImagePath = await SavePreviewImage(modelVersion, downloadPath);
// Create tracked download
var download = trackedDownloadService.NewDownload(modelFile.DownloadUrl, downloadPath);
// Add hash info
download.ExpectedHashSha256 = modelFile.Hashes.SHA256;
// Add files to cleanup list
download.ExtraCleanupFileNames.Add(cmInfoPath);
if (previewImagePath is not null)
{
download.ExtraCleanupFileNames.Add(previewImagePath);
}
// Attach for progress updates
download.ProgressUpdate += (s, e) =>
{
@ -321,7 +387,7 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
if (e == ProgressState.Success)
{
Text = "Import Complete";
IsIndeterminate = false;
Value = 100;
CheckIfInstalled();
@ -338,20 +404,24 @@ public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
DelayedClearProgress(TimeSpan.FromMilliseconds(800));
}
};
// Add hash context action
download.ContextAction = CivitPostDownloadContextAction.FromCivitFile(modelFile);
download.Start();
}
private void DelayedClearProgress(TimeSpan delay)
{
Task.Delay(delay).ContinueWith(_ =>
{
Text = string.Empty;
Value = 0;
IsImporting = false;
});
Task.Delay(delay)
.ContinueWith(_ =>
{
Text = string.Empty;
Value = 0;
IsImporting = false;
});
}
[GeneratedRegex("<[^>]+>")]
private static partial Regex HtmlRegex();
}

15
StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@ -49,11 +48,11 @@ public partial class CheckpointFile : ViewModelBase
[ObservableProperty] private bool isLoading;
[ObservableProperty] private CivitModelType modelType;
public string FileName => Path.GetFileName((string?) FilePath);
public string FileName => Path.GetFileName(FilePath);
public ObservableCollection<string> Badges { get; set; } = new();
private static readonly string[] SupportedCheckpointExtensions = { ".safetensors", ".pt", ".ckpt", ".pth", "bin" };
private static readonly string[] SupportedCheckpointExtensions = { ".safetensors", ".pt", ".ckpt", ".pth", ".bin" };
private static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg" };
private static readonly string[] SupportedMetadataExtensions = { ".json" };
@ -200,7 +199,8 @@ public partial class CheckpointFile : ViewModelBase
{
foreach (var file in Directory.EnumerateFiles(directory, "*.*", searchOption))
{
if (!SupportedCheckpointExtensions.Any(ext => file.Contains(ext)))
if (!SupportedCheckpointExtensions.Any(ext =>
Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase)))
continue;
var checkpointFile = new CheckpointFile
@ -230,7 +230,8 @@ public partial class CheckpointFile : ViewModelBase
{
foreach (var file in Directory.EnumerateFiles(modelsDirectory, "*.*", SearchOption.AllDirectories))
{
if (!SupportedCheckpointExtensions.Any(ext => file.Contains(ext)))
if (!SupportedCheckpointExtensions.Any(ext =>
Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase)))
continue;
var checkpointFile = new CheckpointFile
@ -239,7 +240,7 @@ public partial class CheckpointFile : ViewModelBase
FilePath = file,
};
var jsonPath = Path.Combine(Path.GetDirectoryName(file),
var jsonPath = Path.Combine(Path.GetDirectoryName(file) ?? "",
Path.GetFileNameWithoutExtension(file) + ".cm-info.json");
if (File.Exists(jsonPath))
@ -251,7 +252,7 @@ public partial class CheckpointFile : ViewModelBase
}
checkpointFile.PreviewImagePath = SupportedImageExtensions
.Select(ext => Path.Combine(Path.GetDirectoryName(file),
.Select(ext => Path.Combine(Path.GetDirectoryName(file) ?? "",
$"{Path.GetFileNameWithoutExtension(file)}.preview{ext}")).Where(File.Exists)
.FirstOrDefault();

19
StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectDataDirectoryViewModel.cs

@ -29,6 +29,8 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
private const string InvalidDirectoryText =
"Directory must be empty or have a valid settings.json file";
private const string NotEnoughFreeSpaceText = "Not enough free space on the selected drive";
private const string FatWarningText =
"FAT32 / exFAT drives are not supported at this time";
[ObservableProperty] private string dataDirectory = DefaultInstallLocation;
[ObservableProperty] private bool isPortableMode;
@ -36,6 +38,7 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
[ObservableProperty] private string directoryStatusText = string.Empty;
[ObservableProperty] private bool isStatusBadgeVisible;
[ObservableProperty] private bool isDirectoryValid;
[ObservableProperty] private bool showFatWarning;
public RefreshBadgeViewModel ValidatorRefreshBadge { get; } = new()
{
@ -66,6 +69,8 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
private async Task<bool> ValidateDataDirectory()
{
await using var delay = new MinimumDelay(100, 200);
ShowFatWarning = IsDriveFat(DataDirectory);
// Doesn't exist, this is fine as a new install, hide badge
if (!Directory.Exists(DataDirectory))
@ -151,4 +156,18 @@ public partial class SelectDataDirectoryViewModel : ContentDialogViewModelBase
{
DataDirectory = value ? Compat.AppCurrentDir + "Data" : DefaultInstallLocation;
}
private bool IsDriveFat(string path)
{
try
{
var drive = new DriveInfo(Path.GetPathRoot(path));
return drive.DriveFormat.Contains("FAT", StringComparison.OrdinalIgnoreCase);
}
catch (Exception e)
{
Logger.Warn(e, "Error checking drive FATness");
return false;
}
}
}

59
StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
@ -7,6 +8,7 @@ using Avalonia.Platform;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
@ -18,11 +20,22 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
public required ContentDialog Dialog { get; set; }
public required IReadOnlyList<ModelVersionViewModel> Versions { get; set; }
public required string Description { get; set; }
public required string Title { get; set; }
[ObservableProperty] private Bitmap? previewImage;
[ObservableProperty] private ModelVersionViewModel? selectedVersionViewModel;
[ObservableProperty] private CivitFileViewModel? selectedFile;
[ObservableProperty] private bool isImportEnabled;
[ObservableProperty] private ObservableCollection<ImageSource> imageUrls = new();
[ObservableProperty] private bool canGoToNextImage;
[ObservableProperty] private bool canGoToPreviousImage;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DisplayedPageNumber))]
private int selectedImageIndex;
public int DisplayedPageNumber => SelectedImageIndex + 1;
public SelectModelVersionViewModel(ISettingsManager settingsManager,
IDownloadService downloadService)
@ -34,18 +47,31 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
public override void OnLoaded()
{
SelectedVersionViewModel = Versions[0];
CanGoToNextImage = true;
}
partial void OnSelectedVersionViewModelChanged(ModelVersionViewModel? value)
{
var nsfwEnabled = settingsManager.Settings.ModelBrowserNsfwEnabled;
var firstImageUrl = value?.ModelVersion?.Images?.FirstOrDefault(
img => nsfwEnabled || img.Nsfw == "None")?.Url;
var allImages = value?.ModelVersion?.Images?.Where(
img => nsfwEnabled || img.Nsfw == "None")?.Select(x => new ImageSource(x.Url)).ToList();
if (allImages == null || !allImages.Any())
{
allImages = new List<ImageSource> {new(Assets.NoImage)};
CanGoToNextImage = false;
}
else
{
CanGoToNextImage = allImages.Count > 1;
}
Dispatcher.UIThread.InvokeAsync(async () =>
Dispatcher.UIThread.Post(() =>
{
SelectedFile = value?.CivitFileViewModels.FirstOrDefault();
await UpdateImage(firstImageUrl);
CanGoToPreviousImage = false;
SelectedFile = SelectedVersionViewModel?.CivitFileViewModels.FirstOrDefault();
ImageUrls = new ObservableCollection<ImageSource>(allImages);
SelectedImageIndex = 0;
});
}
@ -54,15 +80,6 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
IsImportEnabled = value?.CivitFile != null;
}
private async Task UpdateImage(string? url = null)
{
var assetStream = string.IsNullOrWhiteSpace(url)
? AssetLoader.Open(new Uri("avares://StabilityMatrix.Avalonia/Assets/noimage.png"))
: await downloadService.GetImageStreamFromUrl(url);
PreviewImage = new Bitmap(assetStream);
}
public void Cancel()
{
Dialog.Hide(ContentDialogResult.Secondary);
@ -72,4 +89,18 @@ public partial class SelectModelVersionViewModel : ContentDialogViewModelBase
{
Dialog.Hide(ContentDialogResult.Primary);
}
public void PreviousImage()
{
if (SelectedImageIndex > 0) SelectedImageIndex--;
CanGoToPreviousImage = SelectedImageIndex > 0;
CanGoToNextImage = SelectedImageIndex < ImageUrls.Count - 1;
}
public void NextImage()
{
if (SelectedImageIndex < ImageUrls.Count - 1) SelectedImageIndex++;
CanGoToPreviousImage = SelectedImageIndex > 0;
CanGoToNextImage = SelectedImageIndex < ImageUrls.Count - 1;
}
}

23
StabilityMatrix.Avalonia/ViewModels/ProgressManagerViewModel.cs

@ -1,13 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using Polly;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views;
@ -53,7 +49,10 @@ public partial class ProgressManagerViewModel : PageViewModelBase
switch (state)
{
case ProgressState.Success:
notificationService.Show("Download Completed", $"Download of {e.FileName} completed successfully.", NotificationType.Success);
Dispatcher.UIThread.Post(() =>
{
notificationService.Show("Download Completed", $"Download of {e.FileName} completed successfully.", NotificationType.Success);
});
break;
case ProgressState.Failed:
var msg = "";
@ -61,10 +60,16 @@ public partial class ProgressManagerViewModel : PageViewModelBase
{
msg = $"({exception.GetType().Name}) {exception.Message}";
}
notificationService.ShowPersistent("Download Failed", $"Download of {e.FileName} failed: {msg}", NotificationType.Error);
Dispatcher.UIThread.Post(() =>
{
notificationService.ShowPersistent("Download Failed", $"Download of {e.FileName} failed: {msg}", NotificationType.Error);
});
break;
case ProgressState.Cancelled:
notificationService.Show("Download Cancelled", $"Download of {e.FileName} was cancelled.", NotificationType.Warning);
Dispatcher.UIThread.Post(() =>
{
notificationService.Show("Download Cancelled", $"Download of {e.FileName} was cancelled.", NotificationType.Warning);
});
break;
}
};

8
StabilityMatrix.Avalonia/Views/Dialogs/SelectDataDirectoryDialog.axaml

@ -55,6 +55,14 @@
Foreground="LightGray"
FontSize="12"
Margin="0,8,0,0" />
<TextBlock
Text="You may encounter errors when using a FAT32 or exFAT drive. Select a different drive for a smoother experience."
TextWrapping="Wrap"
Foreground="Yellow"
IsVisible="{Binding ShowFatWarning}"
FontSize="14"
Margin="0,8,0,0" />
<CheckBox
Content="Portable Mode"

94
StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml

@ -3,32 +3,84 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:api="clr-namespace:StabilityMatrix.Core.Models.Api;assembly=StabilityMatrix.Core"
xmlns:dialogs="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Dialogs"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="450"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="850"
x:DataType="dialogs:SelectModelVersionViewModel"
xmlns:icons="https://github.com/projektanker/icons.avalonia"
xmlns:fluentAvalonia="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia"
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
d:DataContext="{x:Static designData:DesignData.SelectModelVersionViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.SelectModelVersionDialog">
<Grid
Margin="8"
MinHeight="450"
MinWidth="700"
RowDefinitions="*,Auto"
RowDefinitions="Auto, *, Auto"
ColumnDefinitions="*,Auto,*">
<controls:BetterImage Grid.Column="0" Grid.Row="0" Margin="8"
VerticalAlignment="Top"
MaxHeight="400"
HorizontalAlignment="Left"
Source="{Binding PreviewImage}" />
<Grid Grid.Row="0" Grid.Column="0" RowDefinitions="Auto, Auto, Auto">
<Carousel Grid.Row="0"
ItemsSource="{Binding ImageUrls}"
SelectedIndex="{Binding SelectedImageIndex}">
<Carousel.PageTransition>
<CompositePageTransition>
<PageSlide Orientation="Horizontal" Duration="0.167">
<PageSlide.SlideInEasing>
<SineEaseIn />
</PageSlide.SlideInEasing>
<PageSlide.SlideOutEasing>
<SineEaseOut />
</PageSlide.SlideOutEasing>
</PageSlide>
<CrossFade Duration="0.167" />
</CompositePageTransition>
</Carousel.PageTransition>
<Carousel.ItemTemplate>
<DataTemplate DataType="{x:Type models:ImageSource}">
<controls:BetterImage Margin="8"
VerticalAlignment="Top"
HorizontalAlignment="Center"
Height="300"
Stretch="UniformToFill"
Source="{Binding BitmapAsync^}" />
</DataTemplate>
</Carousel.ItemTemplate>
</Carousel>
<StackPanel Grid.Row="1" Orientation="Horizontal"
Margin="0,0,0,8"
VerticalAlignment="Bottom"
HorizontalAlignment="Center">
<TextBlock Text="{Binding DisplayedPageNumber}"/>
<TextBlock Text="/"/>
<TextBlock Text="{Binding ImageUrls.Count}"/>
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Bottom">
<Button Command="{Binding PreviousImage}"
Margin="0,0,8,0"
IsEnabled="{Binding CanGoToPreviousImage}"
ToolTip.Tip="Previous Image"
ToolTip.Placement="Top">
<avalonia:Icon Value="fa-solid fa-caret-left"/>
</Button>
<Button Command="{Binding NextImage}"
IsEnabled="{Binding CanGoToNextImage}"
ToolTip.Tip="Next Image"
ToolTip.Placement="Top">
<avalonia:Icon Value="fa-solid fa-caret-right"/>
</Button>
</StackPanel>
</Grid>
<ListBox Grid.Row="0" Grid.Column="1"
Margin="8"
MaxHeight="450"
MaxHeight="360"
VerticalAlignment="Top"
ItemsSource="{Binding Versions}"
SelectedItem="{Binding SelectedVersionViewModel}">
<ListBox.ItemTemplate>
@ -66,6 +118,8 @@
</ListBox>
<ListBox Grid.Row="0" Grid.Column="2"
MaxHeight="360"
VerticalAlignment="Top"
ItemsSource="{Binding SelectedVersionViewModel.CivitFileViewModels}"
SelectedItem="{Binding SelectedFile}">
<ListBox.Template>
@ -78,7 +132,7 @@
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type dialogs:CivitFileViewModel}">
<Grid RowDefinitions="Auto, Auto" ColumnDefinitions="Auto, *"
Margin="-4,0,0,0">
Margin="0,4,0,4">
<fluentAvalonia:SymbolIcon Grid.Row="0" Grid.RowSpan="2"
Grid.Column="0"
Margin="0, 0, 8, 0"
@ -106,9 +160,23 @@
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Expander Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="1"
Header="Model Description"
ExpandDirection="Down"
Margin="8, 16">
<ScrollViewer MaxHeight="200">
<TextBlock Text="{Binding Description}"
TextWrapping="Wrap"
Margin="4"/>
</ScrollViewer>
</Expander>
<StackPanel Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="1"
<StackPanel Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="2"
Orientation="Horizontal"
Margin="0,8,0,0"
HorizontalAlignment="Center">
<Button
Content="Cancel"

31
StabilityMatrix.Core/Database/LiteDbContext.cs

@ -46,20 +46,22 @@ public class LiteDbContext : ILiteDbContext
{
db = new LiteDatabaseAsync(":temp:");
}
// Attempt to create connection, might be in use
try
else
{
var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db");
db = new LiteDatabaseAsync(new ConnectionString()
// Attempt to create connection, might be in use
try
{
Filename = dbPath,
Connection = ConnectionType.Shared,
});
}
catch (IOException e)
{
logger.LogWarning("Database in use or not accessible ({Message}), using temporary database", e.Message);
var dbPath = Path.Combine(settingsManager.LibraryDir, "StabilityMatrix.db");
db = new LiteDatabaseAsync(new ConnectionString()
{
Filename = dbPath,
Connection = ConnectionType.Shared,
});
}
catch (IOException e)
{
logger.LogWarning("Database in use or not accessible ({Message}), using temporary database", e.Message);
}
}
// Fallback to temporary database
@ -136,12 +138,15 @@ public class LiteDbContext : ILiteDbContext
return changed;
}
public async Task<GithubCacheEntry?> GetGithubCacheEntry(string cacheKey)
public async Task<GithubCacheEntry?> GetGithubCacheEntry(string? cacheKey)
{
if (string.IsNullOrEmpty(cacheKey)) return null;
if (await GithubCache.FindByIdAsync(cacheKey).ConfigureAwait(false) is { } result)
{
return result;
}
return null;
}

6
StabilityMatrix.Core/Helper/ArchiveHelper.cs

@ -31,10 +31,14 @@ public static partial class ArchiveHelper
{
return "7za.exe";
}
if (Compat.Platform.HasFlag(PlatformKind.Linux))
if (Compat.IsLinux)
{
return "7zzs";
}
if (Compat.IsMacOS)
{
return "7zz";
}
throw new PlatformNotSupportedException("7z is not supported on this platform.");
}
}

2
StabilityMatrix.Core/Helper/Compat.cs

@ -114,7 +114,7 @@ public static class Compat
throw new PlatformNotSupportedException();
}
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm)
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
Platform |= PlatformKind.Arm;
}

22
StabilityMatrix.Core/Helper/HardwareHelper.cs

@ -125,13 +125,13 @@ public static partial class HardwareHelper
}
// Set ROCm for default if AMD and Linux
public static bool PreferRocm() => !HardwareHelper.HasNvidiaGpu()
&& HardwareHelper.HasAmdGpu()
public static bool PreferRocm() => !HasNvidiaGpu()
&& HasAmdGpu()
&& Compat.IsLinux;
// Set DirectML for default if AMD and Windows
public static bool PreferDirectML() => !HardwareHelper.HasNvidiaGpu()
&& HardwareHelper.HasAmdGpu()
public static bool PreferDirectML() => !HasNvidiaGpu()
&& HasAmdGpu()
&& Compat.IsWindows;
}
@ -155,6 +155,18 @@ public record GpuInfo
_ => Level.High
};
public bool IsNvidia => Name?.ToLowerInvariant().Contains("nvidia") ?? false;
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;
}

4
StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs

@ -30,6 +30,10 @@ public class FilePath : FileSystemPath, IPathObject
[JsonIgnore]
public string Name => Info.Name;
[JsonIgnore]
public string NameWithoutExtension
=> Path.GetFileNameWithoutExtension(Info.Name);
/// <summary>
/// Get the directory of the file.
/// </summary>

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

@ -111,6 +111,14 @@ public class A3WebUI : BaseGitPackage
InitialValue = true,
Options = new() {"--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"}
},
LaunchOptionDefinition.Extras
};
@ -146,6 +154,16 @@ public class A3WebUI : BaseGitPackage
Logger.Info("Installing xformers...");
await venvRunner.PipInstall("xformers", OnConsoleOutput).ConfigureAwait(false);
}
else if (HardwareHelper.PreferRocm())
{
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for ROCm", isIndeterminate: true));
await venvRunner.PipInstall("--upgrade pip wheel", OnConsoleOutput)
.ConfigureAwait(false);
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm511, OnConsoleOutput)
.ConfigureAwait(false);
}
else
{
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CPU", isIndeterminate: true));
@ -161,10 +179,15 @@ public class A3WebUI : BaseGitPackage
progress?.Report(new ProgressReport(1f, "Installing Package Requirements", isIndeterminate: false));
progress?.Report(new ProgressReport(-1f, "Updating configuration", isIndeterminate: true));
// Create and add {"show_progress_type": "TAESD"} to config.json
// Only add if the file doesn't exist
var configPath = Path.Combine(InstallLocation, "config.json");
var config = new JsonObject {{"show_progress_type", "TAESD"}};
await File.WriteAllTextAsync(configPath, config.ToString()).ConfigureAwait(false);
if (!File.Exists(configPath))
{
var config = new JsonObject {{"show_progress_type", "TAESD"}};
await File.WriteAllTextAsync(configPath, config.ToString()).ConfigureAwait(false);
}
progress?.Report(new ProgressReport(1f, "Install complete", isIndeterminate: false));
}

10
StabilityMatrix.Core/Models/Packages/ComfyUI.cs

@ -126,11 +126,19 @@ public class ComfyUI : BaseGitPackage
Logger.Info("Installing xformers...");
await venvRunner.PipInstall("xformers", OnConsoleOutput).ConfigureAwait(false);
}
else if (HardwareHelper.PreferRocm())
{
progress?.Report(new ProgressReport(-1, "Installing PyTorch for ROCm", isIndeterminate: true));
await venvRunner
.PipInstall(PyVenvRunner.TorchPipInstallArgsRocm542, OnConsoleOutput)
.ConfigureAwait(false);
}
else
{
progress?.Report(new ProgressReport(-1, "Installing PyTorch for CPU", isIndeterminate: true));
Logger.Info("Starting torch install (CPU)...");
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, OnConsoleOutput).ConfigureAwait(false);
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, OnConsoleOutput)
.ConfigureAwait(false);
}
// Install requirements file

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

@ -148,7 +148,7 @@ public class InvokeAI : BaseGitPackage
"InvokeAI[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu117";
}
// For AMD, Install ROCm version
else if (gpus.Any(g => g.IsAmd))
else if (HardwareHelper.PreferRocm())
{
Logger.Info("Starting InvokeAI install (ROCm)...");
pipCommandArgs =

15
StabilityMatrix.Core/Python/PyRunner.cs

@ -31,17 +31,18 @@ public class PyRunner : IPyRunner
/// </summary>
public static string RelativePythonDllPath => Compat.Switch(
(PlatformKind.Windows, "python310.dll"),
(PlatformKind.Linux, Path.Combine("lib", "libpython3.10.so")));
public static string PythonDllPath => Compat.Switch(
(PlatformKind.Windows, Path.Combine(PythonDir, "python310.dll")),
(PlatformKind.Linux, Path.Combine(PythonDir, "lib", "libpython3.10.so")));
(PlatformKind.Linux, Path.Combine("lib", "libpython3.10.so")),
(PlatformKind.MacOS, Path.Combine("lib", "libpython3.10.dylib")));
public static string PythonDllPath => Path.Combine(PythonDir, RelativePythonDllPath);
public static string PythonExePath => Compat.Switch(
(PlatformKind.Windows, Path.Combine(PythonDir, "python.exe")),
(PlatformKind.Linux, Path.Combine(PythonDir, "bin", "python3")));
(PlatformKind.Linux, Path.Combine(PythonDir, "bin", "python3")),
(PlatformKind.MacOS, Path.Combine(PythonDir, "bin", "python3")));
public static string PipExePath => Compat.Switch(
(PlatformKind.Windows, Path.Combine(PythonDir, "Scripts", "pip.exe")),
(PlatformKind.Linux, Path.Combine(PythonDir, "bin", "pip3")));
(PlatformKind.Linux, Path.Combine(PythonDir, "bin", "pip3")),
(PlatformKind.MacOS, Path.Combine(PythonDir, "bin", "pip3")));
public static string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc");

7
StabilityMatrix.Core/Python/PyVenvRunner.cs

@ -24,6 +24,13 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
public const string TorchPipInstallArgsCpu = "torch torchvision torchaudio";
public const string TorchPipInstallArgsDirectML = "torch-directml";
public const string TorchPipInstallArgsRocm511 =
"torch torchvision --extra-index-url https://download.pytorch.org/whl/rocm5.1.1";
public const string TorchPipInstallArgsRocm542 =
"torch torchvision --extra-index-url https://download.pytorch.org/whl/rocm5.4.2";
public const string TorchPipInstallArgsRocmNightly56 =
"--pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/rocm5.6";
/// <summary>
/// Relative path to the site-packages folder from the venv root.
/// This is platform specific.

4
StabilityMatrix.Core/Services/SettingsManager.cs

@ -247,7 +247,9 @@ public class SettingsManager : ISettingsManager
{
var libraryJson = libraryJsonFile.ReadAllText();
var librarySettings = JsonSerializer.Deserialize<LibrarySettings>(libraryJson);
if (!string.IsNullOrWhiteSpace(librarySettings?.LibraryPath))
if (!string.IsNullOrWhiteSpace(librarySettings?.LibraryPath)
&& Directory.Exists(librarySettings?.LibraryPath))
{
LibraryDir = librarySettings.LibraryPath;
SetStaticLibraryPaths();

Loading…
Cancel
Save