Browse Source

Merge branch 'dev' into inference-modules

# Conflicts:
#	StabilityMatrix.Avalonia/App.axaml.cs
#	StabilityMatrix.Avalonia/Controls/StackExpander.axaml
#	StabilityMatrix.Avalonia/DesignData/DesignData.cs
#	StabilityMatrix.Avalonia/Languages/Resources.resx
#	StabilityMatrix.Core/Helper/FileHash.cs
#	StabilityMatrix.Core/Models/Packages/A3WebUI.cs
#	StabilityMatrix.Core/Models/Packages/ComfyUI.cs
#	StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs
#	StabilityMatrix.Core/Models/Packages/VladAutomatic.cs
#	StabilityMatrix.Core/Services/SettingsManager.cs
pull/333/head
Ionite 12 months ago
parent
commit
6cbf721a0b
No known key found for this signature in database
  1. 13
      CHANGELOG.md
  2. 1
      StabilityMatrix.Avalonia/App.axaml.cs
  3. 49
      StabilityMatrix.Avalonia/Assets.cs
  4. 544
      StabilityMatrix.Avalonia/Assets/hf-packages.json
  5. 2
      StabilityMatrix.Avalonia/Controls/StackExpander.axaml
  6. 29
      StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs
  7. 18
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  8. 30
      StabilityMatrix.Avalonia/DesignData/MockMetadataImportService.cs
  9. 38
      StabilityMatrix.Avalonia/Languages/Resources.Designer.cs
  10. 3
      StabilityMatrix.Avalonia/Languages/Resources.es.resx
  11. 3
      StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx
  12. 3
      StabilityMatrix.Avalonia/Languages/Resources.it-it.resx
  13. 3
      StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx
  14. 14
      StabilityMatrix.Avalonia/Languages/Resources.resx
  15. 3
      StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx
  16. 3
      StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx
  17. 3
      StabilityMatrix.Avalonia/Languages/Resources.zh-Hant.resx
  18. 46
      StabilityMatrix.Avalonia/Models/HuggingFace/HuggingFaceModelType.cs
  19. 12
      StabilityMatrix.Avalonia/Models/HuggingFace/HuggingfaceItem.cs
  20. 1
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  21. 63
      StabilityMatrix.Avalonia/Styles/SplitButtonStyles.axaml
  22. 6
      StabilityMatrix.Avalonia/ViewModels/Base/TabViewModelBase.cs
  23. 612
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs
  24. 193
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/HuggingFacePageViewModel.cs
  25. 654
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  26. 100
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  27. 123
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  28. 86
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  29. 52
      StabilityMatrix.Avalonia/ViewModels/HuggingFacePage/CategoryViewModel.cs
  30. 25
      StabilityMatrix.Avalonia/ViewModels/HuggingFacePage/HuggingfaceItemViewModel.cs
  31. 527
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
  32. 29
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml.cs
  33. 89
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  34. 33
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs
  35. 531
      StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml
  36. 41
      StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml.cs
  37. 2
      StabilityMatrix.Avalonia/Views/Dialogs/InferenceConnectionHelpDialog.axaml
  38. 188
      StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml
  39. 13
      StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml.cs
  40. 43
      StabilityMatrix.Core/Helper/FileHash.cs
  41. 46
      StabilityMatrix.Core/Helper/ModelFinder.cs
  42. 12
      StabilityMatrix.Core/Models/ConnectedModelInfo.cs
  43. 23
      StabilityMatrix.Core/Models/Database/LocalModelFile.cs
  44. 46
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  45. 84
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  46. 15
      StabilityMatrix.Core/Models/Packages/FocusControlNet.cs
  47. 9
      StabilityMatrix.Core/Models/Packages/Fooocus.cs
  48. 19
      StabilityMatrix.Core/Models/Packages/FooocusMre.cs
  49. 115
      StabilityMatrix.Core/Models/Packages/InvokeAI.cs
  50. 37
      StabilityMatrix.Core/Models/Packages/KohyaSs.cs
  51. 20
      StabilityMatrix.Core/Models/Packages/RuinedFooocus.cs
  52. 29
      StabilityMatrix.Core/Models/Packages/StableDiffusionDirectMl.cs
  53. 44
      StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs
  54. 42
      StabilityMatrix.Core/Models/Packages/VladAutomatic.cs
  55. 36
      StabilityMatrix.Core/Models/Packages/VoltaML.cs
  56. 8
      StabilityMatrix.Core/Models/SharedFolderType.cs
  57. 18
      StabilityMatrix.Core/Services/IMetadataImportService.cs
  58. 266
      StabilityMatrix.Core/Services/MetadataImportService.cs
  59. 6
      StabilityMatrix.Core/Services/SettingsManager.cs

13
CHANGELOG.md

@ -5,6 +5,19 @@ 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.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

1
StabilityMatrix.Avalonia/App.axaml.cs

@ -65,7 +65,6 @@ using StabilityMatrix.Avalonia.Diagnostics.LogViewer;
using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions;
#endif
namespace StabilityMatrix.Avalonia;
public sealed class App : Application

49
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");
/// <summary>
/// Fixed image for models with no images.
/// </summary>
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
/// <summary>
/// Yield AvaloniaResources given a relative directory path within the 'Assets' folder.
/// </summary>
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);

544
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"
}
]

2
StabilityMatrix.Avalonia/Controls/StackExpander.axaml

@ -25,6 +25,8 @@
<Setter Property="Template">
<ControlTemplate>
<Expander
ExpandDirection="{TemplateBinding ExpandDirection}"
IsExpanded="{TemplateBinding IsExpanded}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
HorizontalContentAlignment="Stretch"

29
StabilityMatrix.Avalonia/Controls/StackExpander.axaml.cs

@ -1,4 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using StabilityMatrix.Core.Attributes;
@ -7,10 +8,30 @@ namespace StabilityMatrix.Avalonia.Controls;
[Transient]
public class StackExpander : TemplatedControl
{
public static readonly StyledProperty<int> SpacingProperty = AvaloniaProperty.Register<
StackCard,
int
>("Spacing", 8);
public static readonly StyledProperty<bool> IsExpandedProperty = Expander
.IsExpandedProperty
.AddOwner<StackExpander>();
public static readonly StyledProperty<ExpandDirection> ExpandDirectionProperty = Expander
.ExpandDirectionProperty
.AddOwner<StackExpander>();
public static readonly StyledProperty<int> SpacingProperty = AvaloniaProperty.Register<StackCard, int>(
"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
{

18
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -44,6 +44,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;
@ -123,7 +125,8 @@ public static class DesignData
.AddSingleton<IInferenceClientManager, MockInferenceClientManager>()
.AddSingleton<ICompletionProvider, MockCompletionProvider>()
.AddSingleton<IModelIndexService, MockModelIndexService>()
.AddSingleton<IImageIndexService, MockImageIndexService>();
.AddSingleton<IImageIndexService, MockImageIndexService>()
.AddSingleton<IMetadataImportService, MetadataImportService>();
// Placeholder services that nobody should need during design time
services
@ -146,6 +149,7 @@ public static class DesignData
var modelFinder = Services.GetRequiredService<ModelFinder>();
var packageFactory = Services.GetRequiredService<IPackageFactory>();
var notificationService = Services.GetRequiredService<INotificationService>();
var modelImportService = Services.GetRequiredService<IMetadataImportService>();
LaunchOptionsViewModel = Services.GetRequiredService<LaunchOptionsViewModel>();
LaunchOptionsViewModel.Cards = new[]
@ -182,7 +186,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<CheckpointFile>()
@ -212,7 +216,7 @@ public static class DesignData
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",
@ -293,7 +297,7 @@ public static class DesignData
);
}*/
CheckpointBrowserViewModel.ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>
CivitAiBrowserViewModel.ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>
{
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
@ -416,6 +420,9 @@ public static class DesignData
public static LaunchPageViewModel LaunchPageViewModel => Services.GetRequiredService<LaunchPageViewModel>();
public static HuggingFacePageViewModel HuggingFacePageViewModel =>
Services.GetRequiredService<HuggingFacePageViewModel>();
public static OutputsPageViewModel OutputsPageViewModel
{
get
@ -501,6 +508,9 @@ public static class DesignData
}
}
public static CivitAiBrowserViewModel CivitAiBrowserViewModel =>
Services.GetRequiredService<CivitAiBrowserViewModel>();
public static CheckpointBrowserViewModel CheckpointBrowserViewModel =>
Services.GetRequiredService<CheckpointBrowserViewModel>();

30
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<ProgressReport>? progress = null)
{
return Task.CompletedTask;
}
public Task<ConnectedModelInfo?> GetMetadataForFile(
FilePath filePath,
IProgress<ProgressReport>? progress = null,
bool forceReimport = false
)
{
return null;
}
public Task UpdateExistingMetadata(DirectoryPath directory, IProgress<ProgressReport>? progress = null)
{
return Task.CompletedTask;
}
}

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

@ -338,6 +338,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Open on Hugging Face.
/// </summary>
public static string Action_OpenOnHuggingFace {
get {
return ResourceManager.GetString("Action_OpenOnHuggingFace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open Project....
/// </summary>
@ -563,6 +572,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Update Existing Metadata.
/// </summary>
public static string Action_UpdateExistingMetadata {
get {
return ResourceManager.GetString("Action_UpdateExistingMetadata", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Upgrade.
/// </summary>
@ -779,6 +797,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to CivitAI.
/// </summary>
public static string Label_CivitAi {
get {
return ResourceManager.GetString("Label_CivitAi", resourceCulture);
}
}
/// <summary>
/// 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..
/// </summary>
@ -1050,7 +1077,7 @@ namespace StabilityMatrix.Avalonia.Languages {
}
/// <summary>
/// Looks up a localized string similar to Emebeddings / Textual Inversion.
/// Looks up a localized string similar to Embeddings / Textual Inversion.
/// </summary>
public static string Label_EmbeddingsOrTextualInversion {
get {
@ -1157,6 +1184,15 @@ namespace StabilityMatrix.Avalonia.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Hugging Face.
/// </summary>
public static string Label_HuggingFace {
get {
return ResourceManager.GetString("Label_HuggingFace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Image to Image.
/// </summary>

3
StabilityMatrix.Avalonia/Languages/Resources.es.resx

@ -680,4 +680,7 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Restablecer Diseño Predeterminado</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

3
StabilityMatrix.Avalonia/Languages/Resources.fr-FR.resx

@ -678,4 +678,7 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Rétablir la présentation par défaut</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

3
StabilityMatrix.Avalonia/Languages/Resources.it-it.resx

@ -680,4 +680,7 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value></value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

3
StabilityMatrix.Avalonia/Languages/Resources.ja-JP.resx

@ -680,4 +680,7 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>レイアウトを初期状態に戻す</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

14
StabilityMatrix.Avalonia/Languages/Resources.resx

@ -175,7 +175,7 @@
<value>Deemphasis</value>
</data>
<data name="Label_EmbeddingsOrTextualInversion" xml:space="preserve">
<value>Emebeddings / Textual Inversion</value>
<value>Embeddings / Textual Inversion</value>
</data>
<data name="Label_NetworksLoraOrLycoris" xml:space="preserve">
<value>Networks (Lora / LyCORIS)</value>
@ -825,6 +825,12 @@
<data name="TeachingTip_MoreCheckpointCategories" xml:space="preserve">
<value>Additional folders such as IPAdapters and TextualInversions (embeddings) can be enabled here</value>
</data>
<data name="Action_OpenOnHuggingFace" xml:space="preserve">
<value>Open on Hugging Face</value>
</data>
<data name="Action_UpdateExistingMetadata" xml:space="preserve">
<value>Update Existing Metadata</value>
</data>
<data name="Label_General" xml:space="preserve">
<value>General</value><comment>A general settings category</comment>
</data>
@ -858,6 +864,12 @@
<data name="Label_SystemInformation" xml:space="preserve">
<value>System Information</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
<data name="Label_HuggingFace" xml:space="preserve">
<value>Hugging Face</value>
</data>
<data name="Label_Addons" xml:space="preserve">
<value>Addons</value><comment>Inference Sampler Addons</comment>
</data>

3
StabilityMatrix.Avalonia/Languages/Resources.ru-ru.resx

@ -678,4 +678,7 @@
<data name="Action_RestoreDefaultLayout" xml:space="preserve">
<value>Восстановить вид по умолчанию</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

3
StabilityMatrix.Avalonia/Languages/Resources.zh-Hans.resx

@ -647,4 +647,7 @@
<data name="Label_Branch" xml:space="preserve">
<value>分支</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

3
StabilityMatrix.Avalonia/Languages/Resources.zh-Hant.resx

@ -639,4 +639,7 @@
<data name="Label_Branch" xml:space="preserve">
<value>分支</value>
</data>
<data name="Label_CivitAi" xml:space="preserve">
<value>CivitAI</value>
</data>
</root>

46
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<HuggingFaceModelType>))]
public enum HuggingFaceModelType
{
[Description("Base Models")]
[ConvertTo<SharedFolderType>(SharedFolderType.StableDiffusion)]
BaseModel,
[Description("ControlNets")]
[ConvertTo<SharedFolderType>(SharedFolderType.ControlNet)]
ControlNet,
[Description("ControlNets (Diffusers)")]
[ConvertTo<SharedFolderType>(SharedFolderType.ControlNet)]
DiffusersControlNet,
[Description("IP Adapters")]
[ConvertTo<SharedFolderType>(SharedFolderType.IpAdapter)]
IpAdapter,
[Description("IP Adapters (Diffusers SD1.5)")]
[ConvertTo<SharedFolderType>(SharedFolderType.InvokeIpAdapters15)]
DiffusersIpAdapter,
[Description("IP Adapters (Diffusers SDXL)")]
[ConvertTo<SharedFolderType>(SharedFolderType.InvokeIpAdaptersXl)]
DiffusersIpAdapterXl,
[Description("CLIP Vision (Diffusers)")]
[ConvertTo<SharedFolderType>(SharedFolderType.InvokeClipVision)]
DiffusersClipVision,
[Description("T2I Adapters")]
[ConvertTo<SharedFolderType>(SharedFolderType.T2IAdapter)]
T2IAdapter,
[Description("T2I Adapters (Diffusers)")]
[ConvertTo<SharedFolderType>(SharedFolderType.T2IAdapter)]
DiffusersT2IAdapter,
}

12
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; }
}

1
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -81,6 +81,7 @@
<ItemGroup>
<AvaloniaResource Include="Assets\Icon.ico" />
<AvaloniaResource Include="Assets\Icon.png" />
<AvaloniaResource Include="Assets\hf-packages.json" />
</ItemGroup>
<ItemGroup>

63
StabilityMatrix.Avalonia/Styles/SplitButtonStyles.axaml

@ -11,6 +11,7 @@
<SplitButton Classes="info" Content="Info Button" Margin="8" HorizontalAlignment="Center" />
<SplitButton Classes="transparent-info" Content="Semi-Transparent Info Button" Margin="8" HorizontalAlignment="Center" />
<SplitButton Classes="transparent" Content="Transparent Button" Margin="8" HorizontalAlignment="Center" />
<SplitButton Classes="transparent-full" Content="Full Transparent Button" Margin="8" HorizontalAlignment="Center" />
<SplitButton Content="Disabled Button" Margin="8" IsEnabled="False" HorizontalAlignment="Center" />
</StackPanel>
</Border>
@ -417,6 +418,68 @@
</Style>
</Style>
<!-- Transparent-Full -->
<Style Selector="SplitButton.transparent-full">
<Style Selector="^ /template/ Button /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource SystemControlTransparentBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlTransparentBrush}" />
</Style>
<Style Selector="^ /template/ Button /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
</Style>
<!-- Pseudo-classes -->
<Style Selector="^ /template/ Button#PART_PrimaryButton:pointerover">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^ /template/ Button#PART_SecondaryButton:pointerover">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundDisabled}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
</Style>
<Style Selector="^ /template/ Button#PART_PrimaryButton:pressed">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^ /template/ Button#PART_SecondaryButton:pressed">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBackgroundPressed}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ Button /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
</Style>
</Style>
<!-- Semi-Transparent Info -->
<Style Selector="SplitButton.transparent-info">
<Style Selector="^ /template/ Button /template/ ui|FABorder#Root">

6
StabilityMatrix.Avalonia/ViewModels/Base/TabViewModelBase.cs

@ -0,0 +1,6 @@
namespace StabilityMatrix.Avalonia.ViewModels.Base;
public abstract class TabViewModelBase : ViewModelBase
{
public abstract string Header { get; }
}

612
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CivitAiBrowserViewModel.cs

@ -0,0 +1,612 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LiteDB;
using LiteDB.Async;
using NLog;
using Refit;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
using Notification = Avalonia.Controls.Notifications.Notification;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
[View(typeof(CivitAiBrowserPage))]
[Singleton]
public partial class CivitAiBrowserViewModel : TabViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ICivitApi civitApi;
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly ILiteDbContext liteDbContext;
private readonly INotificationService notificationService;
private const int MaxModelsPerPage = 20;
private LRUCache<
int /* model id */
,
CheckpointBrowserCardViewModel
> cache = new(50);
[ObservableProperty]
private ObservableCollection<CheckpointBrowserCardViewModel>? modelCards;
[ObservableProperty]
private DataGridCollectionView? modelCardsView;
[ObservableProperty]
private string searchQuery = string.Empty;
[ObservableProperty]
private bool showNsfw;
[ObservableProperty]
private bool showMainLoadingSpinner;
[ObservableProperty]
private CivitPeriod selectedPeriod = CivitPeriod.Month;
[ObservableProperty]
private CivitSortMode sortMode = CivitSortMode.HighestRated;
[ObservableProperty]
private CivitModelType selectedModelType = CivitModelType.Checkpoint;
[ObservableProperty]
private int currentPageNumber;
[ObservableProperty]
private int totalPages;
[ObservableProperty]
private bool hasSearched;
[ObservableProperty]
private bool canGoToNextPage;
[ObservableProperty]
private bool canGoToPreviousPage;
[ObservableProperty]
private bool canGoToFirstPage;
[ObservableProperty]
private bool canGoToLastPage;
[ObservableProperty]
private bool isIndeterminate;
[ObservableProperty]
private bool noResultsFound;
[ObservableProperty]
private string noResultsText = string.Empty;
[ObservableProperty]
private string selectedBaseModelType = "All";
private List<CheckpointBrowserCardViewModel> allModelCards = new();
public IEnumerable<CivitPeriod> AllCivitPeriods => Enum.GetValues(typeof(CivitPeriod)).Cast<CivitPeriod>();
public IEnumerable<CivitSortMode> AllSortModes => Enum.GetValues(typeof(CivitSortMode)).Cast<CivitSortMode>();
public IEnumerable<CivitModelType> AllModelTypes =>
Enum.GetValues(typeof(CivitModelType))
.Cast<CivitModelType>()
.Where(t => t == CivitModelType.All || t.ConvertTo<SharedFolderType>() > 0)
.OrderBy(t => t.ToString());
public List<string> BaseModelOptions => new() { "All", "SD 1.5", "SD 2.1", "SDXL 0.9", "SDXL 1.0" };
public CivitAiBrowserViewModel(
ICivitApi civitApi,
IDownloadService downloadService,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> dialogFactory,
ILiteDbContext liteDbContext,
INotificationService notificationService
)
{
this.civitApi = civitApi;
this.downloadService = downloadService;
this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory;
this.liteDbContext = liteDbContext;
this.notificationService = notificationService;
CurrentPageNumber = 1;
CanGoToNextPage = true;
CanGoToLastPage = true;
Observable
.FromEventPattern<PropertyChangedEventArgs>(this, nameof(PropertyChanged))
.Where(x => x.EventArgs.PropertyName == nameof(CurrentPageNumber))
.Throttle(TimeSpan.FromMilliseconds(250))
.Select<EventPattern<PropertyChangedEventArgs>, int>(_ => CurrentPageNumber)
.Where(page => page <= TotalPages && page > 0)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err));
}
public override void OnLoaded()
{
if (Design.IsDesignMode)
return;
var searchOptions = settingsManager.Settings.ModelSearchOptions;
// Fix SelectedModelType if someone had selected the obsolete "Model" option
if (searchOptions is { SelectedModelType: CivitModelType.Model })
{
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
SortMode,
CivitModelType.Checkpoint,
SelectedBaseModelType
)
);
searchOptions = settingsManager.Settings.ModelSearchOptions;
}
SelectedPeriod = searchOptions?.SelectedPeriod ?? CivitPeriod.Month;
SortMode = searchOptions?.SortMode ?? CivitSortMode.HighestRated;
SelectedModelType = searchOptions?.SelectedModelType ?? CivitModelType.Checkpoint;
SelectedBaseModelType = searchOptions?.SelectedBaseModelType ?? "All";
ShowNsfw = settingsManager.Settings.ModelBrowserNsfwEnabled;
settingsManager.RelayPropertyFor(this, model => model.ShowNsfw, settings => settings.ModelBrowserNsfwEnabled);
}
/// <summary>
/// Filter predicate for model cards
/// </summary>
private bool FilterModelCardsPredicate(object? item)
{
if (item is not CheckpointBrowserCardViewModel card)
return false;
return !card.CivitModel.Nsfw || ShowNsfw;
}
/// <summary>
/// Background update task
/// </summary>
private async Task CivitModelQuery(CivitModelsRequest request)
{
var timer = Stopwatch.StartNew();
var queryText = request.Query;
try
{
var modelsResponse = await civitApi.GetModels(request);
var models = modelsResponse.Items;
if (models is null)
{
Logger.Debug(
"CivitAI Query {Text} returned no results (in {Elapsed:F1} s)",
queryText,
timer.Elapsed.TotalSeconds
);
return;
}
Logger.Debug(
"CivitAI Query {Text} returned {Results} results (in {Elapsed:F1} s)",
queryText,
models.Count,
timer.Elapsed.TotalSeconds
);
var unknown = models.Where(m => m.Type == CivitModelType.Unknown).ToList();
if (unknown.Any())
{
var names = unknown.Select(m => m.Name).ToList();
Logger.Warn("Excluded {Unknown} unknown model types: {Models}", unknown.Count, names);
}
// Filter out unknown model types and archived/taken-down models
models = models.Where(m => m.Type.ConvertTo<SharedFolderType>() > 0).Where(m => m.Mode == null).ToList();
// Database update calls will invoke `OnModelsUpdated`
// Add to database
await liteDbContext.UpsertCivitModelAsync(models);
// Add as cache entry
var cacheNew = await liteDbContext.UpsertCivitModelQueryCacheEntryAsync(
new()
{
Id = ObjectHash.GetMd5Guid(request),
InsertedAt = DateTimeOffset.UtcNow,
Request = request,
Items = models,
Metadata = modelsResponse.Metadata
}
);
if (cacheNew)
{
Logger.Debug("New cache entry, updating model cards");
UpdateModelCards(models, modelsResponse.Metadata);
}
else
{
Logger.Debug("Cache entry already exists, not updating model cards");
}
}
catch (OperationCanceledException)
{
notificationService.Show(
new Notification("Request to CivitAI timed out", "Please try again in a few minutes")
);
Logger.Warn($"CivitAI query timed out ({request})");
}
catch (HttpRequestException e)
{
notificationService.Show(
new Notification("CivitAI can't be reached right now", "Please try again in a few minutes")
);
Logger.Warn(e, $"CivitAI query HttpRequestException ({request})");
}
catch (ApiException e)
{
notificationService.Show(
new Notification("CivitAI can't be reached right now", "Please try again in a few minutes")
);
Logger.Warn(e, $"CivitAI query ApiException ({request})");
}
catch (Exception e)
{
notificationService.Show(
new Notification(
"CivitAI can't be reached right now",
$"Unknown exception during CivitAI query: {e.GetType().Name}"
)
);
Logger.Error(e, $"CivitAI query unknown exception ({request})");
}
finally
{
ShowMainLoadingSpinner = false;
UpdateResultsText();
}
}
/// <summary>
/// Updates model cards using api response object.
/// </summary>
private void UpdateModelCards(IEnumerable<CivitModel>? models, CivitMetadata? metadata)
{
if (models is null)
{
ModelCards?.Clear();
}
else
{
var updateCards = models
.Select(model =>
{
var cachedViewModel = cache.Get(model.Id);
if (cachedViewModel != null)
{
if (!cachedViewModel.IsImporting)
{
cache.Remove(model.Id);
}
return cachedViewModel;
}
var newCard = dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
vm.CivitModel = model;
vm.OnDownloadStart = viewModel =>
{
if (cache.Get(viewModel.CivitModel.Id) != null)
return;
cache.Add(viewModel.CivitModel.Id, viewModel);
};
return vm;
});
return newCard;
})
.ToList();
allModelCards = updateCards;
var filteredCards = updateCards.Where(FilterModelCardsPredicate);
if (SortMode == CivitSortMode.Installed)
{
filteredCards = filteredCards.OrderByDescending(x => x.UpdateCardText == "Update Available");
}
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(filteredCards);
}
TotalPages = metadata?.TotalPages ?? 1;
CanGoToFirstPage = CurrentPageNumber != 1;
CanGoToPreviousPage = CurrentPageNumber > 1;
CanGoToNextPage = CurrentPageNumber < TotalPages;
CanGoToLastPage = CurrentPageNumber != TotalPages;
// Status update
ShowMainLoadingSpinner = false;
IsIndeterminate = false;
HasSearched = true;
}
private string previousSearchQuery = string.Empty;
[RelayCommand]
private async Task SearchModels()
{
var timer = Stopwatch.StartNew();
if (SearchQuery != previousSearchQuery)
{
// Reset page number
CurrentPageNumber = 1;
previousSearchQuery = SearchQuery;
}
// Build request
var modelRequest = new CivitModelsRequest
{
Limit = MaxModelsPerPage,
Nsfw = "true", // Handled by local view filter
Sort = SortMode,
Period = SelectedPeriod,
Page = CurrentPageNumber
};
if (SearchQuery.StartsWith("#"))
{
modelRequest.Tag = SearchQuery[1..];
}
else if (SearchQuery.StartsWith("@"))
{
modelRequest.Username = SearchQuery[1..];
}
else
{
modelRequest.Query = SearchQuery;
}
if (SelectedModelType != CivitModelType.All)
{
modelRequest.Types = new[] { SelectedModelType };
}
if (SelectedBaseModelType != "All")
{
modelRequest.BaseModel = SelectedBaseModelType;
}
if (SortMode == CivitSortMode.Installed)
{
var connectedModels = CheckpointFile
.GetAllCheckpointFiles(settingsManager.ModelsDirectory)
.Where(c => c.IsConnectedModel);
modelRequest.CommaSeparatedModelIds = string.Join(
",",
connectedModels.Select(c => c.ConnectedModel!.ModelId).GroupBy(m => m).Select(g => g.First())
);
modelRequest.Sort = null;
modelRequest.Period = null;
}
else if (SortMode == CivitSortMode.Favorites)
{
var favoriteModels = settingsManager.Settings.FavoriteModels;
if (!favoriteModels.Any())
{
notificationService.Show(
"No Favorites",
"You have not added any models to your Favorites.",
NotificationType.Error
);
return;
}
modelRequest.CommaSeparatedModelIds = string.Join(",", favoriteModels);
modelRequest.Sort = null;
modelRequest.Period = null;
}
// See if query is cached
CivitModelQueryCacheEntry? cachedQuery = null;
try
{
cachedQuery = await liteDbContext
.CivitModelQueryCache
.IncludeAll()
.FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest));
}
catch (Exception e)
{
// Suppress 'Training_Data' enum not found exceptions
// Caused by enum name change
// Ignore to do a new search to overwrite the cache
if (
!(
e is LiteException or LiteAsyncException
&& e.InnerException is ArgumentException inner
&& inner.Message.Contains("Training_Data")
)
)
{
// Otherwise log error
Logger.Error(e, "Error while querying CivitModelQueryCache");
}
}
// If cached, update model cards
if (cachedQuery is not null)
{
var elapsed = timer.Elapsed;
Logger.Debug(
"Using cached query for {Text} [{RequestHash}] (in {Elapsed:F1} s)",
SearchQuery,
modelRequest.GetHashCode(),
elapsed.TotalSeconds
);
UpdateModelCards(cachedQuery.Items, cachedQuery.Metadata);
// Start remote query (background mode)
// Skip when last query was less than 2 min ago
var timeSinceCache = DateTimeOffset.UtcNow - cachedQuery.InsertedAt;
if (timeSinceCache?.TotalMinutes >= 2)
{
CivitModelQuery(modelRequest).SafeFireAndForget();
Logger.Debug(
"Cached query was more than 2 minutes ago ({Seconds:F0} s), updating cache with remote query",
timeSinceCache.Value.TotalSeconds
);
}
}
else
{
// Not cached, wait for remote query
ShowMainLoadingSpinner = true;
await CivitModelQuery(modelRequest);
}
UpdateResultsText();
}
public void FirstPage()
{
CurrentPageNumber = 1;
}
public void PreviousPage()
{
if (CurrentPageNumber == 1)
return;
CurrentPageNumber--;
}
public void NextPage()
{
if (CurrentPageNumber == TotalPages)
return;
CurrentPageNumber++;
}
public void LastPage()
{
CurrentPageNumber = TotalPages;
}
public void ClearSearchQuery()
{
SearchQuery = string.Empty;
}
partial void OnShowNsfwChanged(bool value)
{
settingsManager.Transaction(s => s.ModelBrowserNsfwEnabled, value);
// ModelCardsView?.Refresh();
var updateCards = allModelCards.Where(FilterModelCardsPredicate);
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(updateCards);
if (!HasSearched)
return;
UpdateResultsText();
}
partial void OnSelectedPeriodChanged(CivitPeriod value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(value, SortMode, SelectedModelType, SelectedBaseModelType)
);
}
partial void OnSortModeChanged(CivitSortMode value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
value,
SelectedModelType,
SelectedBaseModelType
)
);
}
partial void OnSelectedModelTypeChanged(CivitModelType value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s => s.ModelSearchOptions = new ModelSearchOptions(SelectedPeriod, SortMode, value, SelectedBaseModelType)
);
}
partial void OnSelectedBaseModelTypeChanged(string value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s => s.ModelSearchOptions = new ModelSearchOptions(SelectedPeriod, SortMode, SelectedModelType, value)
);
}
private async Task TrySearchAgain(bool shouldUpdatePageNumber = true)
{
if (!HasSearched)
return;
ModelCards?.Clear();
if (shouldUpdatePageNumber)
{
CurrentPageNumber = 1;
}
// execute command instead of calling method directly so that the IsRunning property gets updated
await SearchModelsCommand.ExecuteAsync(null);
}
private void UpdateResultsText()
{
NoResultsFound = ModelCards?.Count <= 0;
NoResultsText =
allModelCards.Count > 0 ? $"{allModelCards.Count} results hidden by filters" : "No results found";
}
public override string Header => Resources.Label_CivitAi;
}

193
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/HuggingFacePageViewModel.cs

@ -0,0 +1,193 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.Notifications;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Media;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Languages;
using StabilityMatrix.Avalonia.Models.HuggingFace;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.HuggingFacePage;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
[View(typeof(Views.HuggingFacePage))]
[Singleton]
public partial class HuggingFacePageViewModel : TabViewModelBase
{
private readonly ITrackedDownloadService trackedDownloadService;
private readonly ISettingsManager settingsManager;
private readonly INotificationService notificationService;
public SourceCache<HuggingfaceItem, string> ItemsCache { get; } = new(i => i.RepositoryPath + i.ModelName);
public IObservableCollection<CategoryViewModel> Categories { get; set; } =
new ObservableCollectionExtended<CategoryViewModel>();
public string DownloadPercentText =>
Math.Abs(TotalProgress.Percentage - 100f) < 0.001f
? "Download Complete"
: $"Downloading {TotalProgress.Percentage:0}%";
[ObservableProperty]
private int numSelected;
private ConcurrentDictionary<Guid, ProgressReport> progressReports = new();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DownloadPercentText))]
private ProgressReport totalProgress;
private readonly DispatcherTimer progressTimer = new() { Interval = TimeSpan.FromMilliseconds(100) };
public HuggingFacePageViewModel(
ITrackedDownloadService trackedDownloadService,
ISettingsManager settingsManager,
INotificationService notificationService
)
{
this.trackedDownloadService = trackedDownloadService;
this.settingsManager = settingsManager;
this.notificationService = notificationService;
ItemsCache
.Connect()
.DeferUntilLoaded()
.Group(i => i.ModelCategory)
.Transform(g => new CategoryViewModel(g.Cache.Items) { Title = g.Key.GetDescription() ?? g.Key.ToString() })
.SortBy(vm => vm.Title)
.Bind(Categories)
.WhenAnyPropertyChanged()
.Subscribe(_ => NumSelected = Categories.Sum(c => c.NumSelected));
progressTimer.Tick += (_, _) =>
{
var currentSum = 0ul;
var totalSum = 0ul;
foreach (var progress in progressReports.Values)
{
currentSum += progress.Current ?? 0;
totalSum += progress.Total ?? 0;
}
TotalProgress = new ProgressReport(current: currentSum, total: totalSum);
};
}
public override void OnLoaded()
{
if (ItemsCache.Count > 0)
return;
using var reader = new StreamReader(Assets.HfPackagesJson.Open());
var packages =
JsonSerializer.Deserialize<IReadOnlyList<HuggingfaceItem>>(
reader.ReadToEnd(),
new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }
) ?? throw new InvalidOperationException("Failed to read hf-packages.json");
ItemsCache.EditDiff(packages, (a, b) => a.RepositoryPath == b.RepositoryPath);
}
public void ClearSelection()
{
foreach (var category in Categories)
{
category.IsChecked = true;
category.IsChecked = false;
}
}
public void SelectAll()
{
foreach (var category in Categories)
{
category.IsChecked = true;
}
}
[RelayCommand]
private async Task ImportSelected()
{
var selected = Categories.SelectMany(c => c.Items).Where(i => i.IsSelected).ToArray();
foreach (var viewModel in selected)
{
foreach (var file in viewModel.Item.Files)
{
var url = $"https://huggingface.co/{viewModel.Item.RepositoryPath}/resolve/main/{file}?download=true";
var sharedFolderType = viewModel.Item.ModelCategory.ConvertTo<SharedFolderType>();
var downloadPath = new FilePath(
Path.Combine(
settingsManager.ModelsDirectory,
sharedFolderType.GetDescription() ?? sharedFolderType.ToString(),
viewModel.Item.Subfolder ?? string.Empty,
file
)
);
Directory.CreateDirectory(downloadPath.Directory);
var download = trackedDownloadService.NewDownload(url, downloadPath);
download.ProgressUpdate += DownloadOnProgressUpdate;
download.Start();
await Task.Delay(Random.Shared.Next(50, 100));
}
}
progressTimer.Start();
}
private void DownloadOnProgressUpdate(object? sender, ProgressReport e)
{
if (sender is not TrackedDownload trackedDownload)
return;
progressReports[trackedDownload.Id] = e;
}
partial void OnTotalProgressChanged(ProgressReport value)
{
if (Math.Abs(value.Percentage - 100) < 0.001f)
{
notificationService.Show(
"Download complete",
"All selected models have been downloaded.",
NotificationType.Success
);
progressTimer.Stop();
DelayedClearProgress(TimeSpan.FromSeconds(1.5));
}
}
private void DelayedClearProgress(TimeSpan delay)
{
Task.Delay(delay)
.ContinueWith(_ =>
{
TotalProgress = new ProgressReport(0, 0);
});
}
public override string Header => Resources.Label_HuggingFace;
}

654
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

@ -1,40 +1,13 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Collections;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using LiteDB;
using LiteDB.Async;
using NLog;
using Refit;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Database;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Services;
using Notification = Avalonia.Controls.Notifications.Notification;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -42,621 +15,18 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(CheckpointBrowserPage))]
[Singleton]
public partial class CheckpointBrowserViewModel : PageViewModelBase
public partial class CheckpointBrowserViewModel(
CivitAiBrowserViewModel civitAiBrowserViewModel,
HuggingFacePageViewModel huggingFaceViewModel
) : PageViewModelBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly ICivitApi civitApi;
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly ILiteDbContext liteDbContext;
private readonly INotificationService notificationService;
private const int MaxModelsPerPage = 20;
private LRUCache<
int /* model id */
,
CheckpointBrowserCardViewModel
> cache = new(50);
[ObservableProperty]
private ObservableCollection<CheckpointBrowserCardViewModel>? modelCards;
[ObservableProperty]
private DataGridCollectionView? modelCardsView;
[ObservableProperty]
private string searchQuery = string.Empty;
[ObservableProperty]
private bool showNsfw;
[ObservableProperty]
private bool showMainLoadingSpinner;
[ObservableProperty]
private CivitPeriod selectedPeriod = CivitPeriod.Month;
[ObservableProperty]
private CivitSortMode sortMode = CivitSortMode.HighestRated;
[ObservableProperty]
private CivitModelType selectedModelType = CivitModelType.Checkpoint;
[ObservableProperty]
private int currentPageNumber;
[ObservableProperty]
private int totalPages;
[ObservableProperty]
private bool hasSearched;
[ObservableProperty]
private bool canGoToNextPage;
[ObservableProperty]
private bool canGoToPreviousPage;
[ObservableProperty]
private bool canGoToFirstPage;
[ObservableProperty]
private bool canGoToLastPage;
[ObservableProperty]
private bool isIndeterminate;
[ObservableProperty]
private bool noResultsFound;
[ObservableProperty]
private string noResultsText = string.Empty;
[ObservableProperty]
private string selectedBaseModelType = "All";
private List<CheckpointBrowserCardViewModel> allModelCards = new();
public IEnumerable<CivitPeriod> AllCivitPeriods =>
Enum.GetValues(typeof(CivitPeriod)).Cast<CivitPeriod>();
public IEnumerable<CivitSortMode> AllSortModes =>
Enum.GetValues(typeof(CivitSortMode)).Cast<CivitSortMode>();
public IEnumerable<CivitModelType> AllModelTypes =>
Enum.GetValues(typeof(CivitModelType))
.Cast<CivitModelType>()
.Where(t => t == CivitModelType.All || t.ConvertTo<SharedFolderType>() > 0)
.OrderBy(t => t.ToString());
public List<string> BaseModelOptions =>
new() { "All", "SD 1.5", "SD 2.1", "SDXL 0.9", "SDXL 1.0" };
public CheckpointBrowserViewModel(
ICivitApi civitApi,
IDownloadService downloadService,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> dialogFactory,
ILiteDbContext liteDbContext,
INotificationService notificationService
)
{
this.civitApi = civitApi;
this.downloadService = downloadService;
this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory;
this.liteDbContext = liteDbContext;
this.notificationService = notificationService;
CurrentPageNumber = 1;
CanGoToNextPage = true;
CanGoToLastPage = true;
Observable
.FromEventPattern<PropertyChangedEventArgs>(this, nameof(PropertyChanged))
.Where(x => x.EventArgs.PropertyName == nameof(CurrentPageNumber))
.Throttle(TimeSpan.FromMilliseconds(250))
.Select(_ => CurrentPageNumber)
.Where(page => page <= TotalPages && page > 0)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(_ => TrySearchAgain(false).SafeFireAndForget(), err => Logger.Error(err));
}
public override void OnLoaded()
{
if (Design.IsDesignMode)
return;
var searchOptions = settingsManager.Settings.ModelSearchOptions;
// Fix SelectedModelType if someone had selected the obsolete "Model" option
if (searchOptions is { SelectedModelType: CivitModelType.Model })
{
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
SortMode,
CivitModelType.Checkpoint,
SelectedBaseModelType
)
);
searchOptions = settingsManager.Settings.ModelSearchOptions;
}
SelectedPeriod = searchOptions?.SelectedPeriod ?? CivitPeriod.Month;
SortMode = searchOptions?.SortMode ?? CivitSortMode.HighestRated;
SelectedModelType = searchOptions?.SelectedModelType ?? CivitModelType.Checkpoint;
SelectedBaseModelType = searchOptions?.SelectedBaseModelType ?? "All";
ShowNsfw = settingsManager.Settings.ModelBrowserNsfwEnabled;
settingsManager.RelayPropertyFor(
this,
model => model.ShowNsfw,
settings => settings.ModelBrowserNsfwEnabled
);
}
/// <summary>
/// Filter predicate for model cards
/// </summary>
private bool FilterModelCardsPredicate(object? item)
{
if (item is not CheckpointBrowserCardViewModel card)
return false;
return !card.CivitModel.Nsfw || ShowNsfw;
}
/// <summary>
/// Background update task
/// </summary>
private async Task CivitModelQuery(CivitModelsRequest request)
{
var timer = Stopwatch.StartNew();
var queryText = request.Query;
try
{
var modelsResponse = await civitApi.GetModels(request);
var models = modelsResponse.Items;
if (models is null)
{
Logger.Debug(
"CivitAI Query {Text} returned no results (in {Elapsed:F1} s)",
queryText,
timer.Elapsed.TotalSeconds
);
return;
}
Logger.Debug(
"CivitAI Query {Text} returned {Results} results (in {Elapsed:F1} s)",
queryText,
models.Count,
timer.Elapsed.TotalSeconds
);
var unknown = models.Where(m => m.Type == CivitModelType.Unknown).ToList();
if (unknown.Any())
{
var names = unknown.Select(m => m.Name).ToList();
Logger.Warn(
"Excluded {Unknown} unknown model types: {Models}",
unknown.Count,
names
);
}
// Filter out unknown model types and archived/taken-down models
models = models
.Where(m => m.Type.ConvertTo<SharedFolderType>() > 0)
.Where(m => m.Mode == null)
.ToList();
// Database update calls will invoke `OnModelsUpdated`
// Add to database
await liteDbContext.UpsertCivitModelAsync(models);
// Add as cache entry
var cacheNew = await liteDbContext.UpsertCivitModelQueryCacheEntryAsync(
new()
{
Id = ObjectHash.GetMd5Guid(request),
InsertedAt = DateTimeOffset.UtcNow,
Request = request,
Items = models,
Metadata = modelsResponse.Metadata
}
);
if (cacheNew)
{
Logger.Debug("New cache entry, updating model cards");
UpdateModelCards(models, modelsResponse.Metadata);
}
else
{
Logger.Debug("Cache entry already exists, not updating model cards");
}
}
catch (OperationCanceledException)
{
notificationService.Show(
new Notification(
"Request to CivitAI timed out",
"Please try again in a few minutes"
)
);
Logger.Warn($"CivitAI query timed out ({request})");
}
catch (HttpRequestException e)
{
notificationService.Show(
new Notification(
"CivitAI can't be reached right now",
"Please try again in a few minutes"
)
);
Logger.Warn(e, $"CivitAI query HttpRequestException ({request})");
}
catch (ApiException e)
{
notificationService.Show(
new Notification(
"CivitAI can't be reached right now",
"Please try again in a few minutes"
)
);
Logger.Warn(e, $"CivitAI query ApiException ({request})");
}
catch (Exception e)
{
notificationService.Show(
new Notification(
"CivitAI can't be reached right now",
$"Unknown exception during CivitAI query: {e.GetType().Name}"
)
);
Logger.Error(e, $"CivitAI query unknown exception ({request})");
}
finally
{
ShowMainLoadingSpinner = false;
UpdateResultsText();
}
}
/// <summary>
/// Updates model cards using api response object.
/// </summary>
private void UpdateModelCards(IEnumerable<CivitModel>? models, CivitMetadata? metadata)
{
if (models is null)
{
ModelCards?.Clear();
}
else
{
var updateCards = models
.Select(model =>
{
var cachedViewModel = cache.Get(model.Id);
if (cachedViewModel != null)
{
if (!cachedViewModel.IsImporting)
{
cache.Remove(model.Id);
}
return cachedViewModel;
}
var newCard = dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
vm.CivitModel = model;
vm.OnDownloadStart = viewModel =>
{
if (cache.Get(viewModel.CivitModel.Id) != null)
return;
cache.Add(viewModel.CivitModel.Id, viewModel);
};
return vm;
});
return newCard;
})
.ToList();
allModelCards = updateCards;
var filteredCards = updateCards.Where(FilterModelCardsPredicate);
if (SortMode == CivitSortMode.Installed)
{
filteredCards = filteredCards.OrderByDescending(
x => x.UpdateCardText == "Update Available"
);
}
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(filteredCards);
}
TotalPages = metadata?.TotalPages ?? 1;
CanGoToFirstPage = CurrentPageNumber != 1;
CanGoToPreviousPage = CurrentPageNumber > 1;
CanGoToNextPage = CurrentPageNumber < TotalPages;
CanGoToLastPage = CurrentPageNumber != TotalPages;
// Status update
ShowMainLoadingSpinner = false;
IsIndeterminate = false;
HasSearched = true;
}
private string previousSearchQuery = string.Empty;
[RelayCommand]
private async Task SearchModels()
{
var timer = Stopwatch.StartNew();
if (SearchQuery != previousSearchQuery)
{
// Reset page number
CurrentPageNumber = 1;
previousSearchQuery = SearchQuery;
}
// Build request
var modelRequest = new CivitModelsRequest
{
Limit = MaxModelsPerPage,
Nsfw = "true", // Handled by local view filter
Sort = SortMode,
Period = SelectedPeriod,
Page = CurrentPageNumber
};
if (SearchQuery.StartsWith("#"))
{
modelRequest.Tag = SearchQuery[1..];
}
else if (SearchQuery.StartsWith("@"))
{
modelRequest.Username = SearchQuery[1..];
}
else
{
modelRequest.Query = SearchQuery;
}
if (SelectedModelType != CivitModelType.All)
{
modelRequest.Types = new[] { SelectedModelType };
}
if (SelectedBaseModelType != "All")
{
modelRequest.BaseModel = SelectedBaseModelType;
}
if (SortMode == CivitSortMode.Installed)
{
var connectedModels = CheckpointFile
.GetAllCheckpointFiles(settingsManager.ModelsDirectory)
.Where(c => c.IsConnectedModel);
modelRequest.CommaSeparatedModelIds = string.Join(
",",
connectedModels
.Select(c => c.ConnectedModel!.ModelId)
.GroupBy(m => m)
.Select(g => g.First())
);
modelRequest.Sort = null;
modelRequest.Period = null;
}
else if (SortMode == CivitSortMode.Favorites)
{
var favoriteModels = settingsManager.Settings.FavoriteModels;
if (!favoriteModels.Any())
{
notificationService.Show(
"No Favorites",
"You have not added any models to your Favorites.",
NotificationType.Error
);
return;
}
modelRequest.CommaSeparatedModelIds = string.Join(",", favoriteModels);
modelRequest.Sort = null;
modelRequest.Period = null;
}
// See if query is cached
CivitModelQueryCacheEntry? cachedQuery = null;
try
{
cachedQuery = await liteDbContext.CivitModelQueryCache
.IncludeAll()
.FindByIdAsync(ObjectHash.GetMd5Guid(modelRequest));
}
catch (Exception e)
{
// Suppress 'Training_Data' enum not found exceptions
// Caused by enum name change
// Ignore to do a new search to overwrite the cache
if (
!(
e is LiteException or LiteAsyncException
&& e.InnerException is ArgumentException inner
&& inner.Message.Contains("Training_Data")
)
)
{
// Otherwise log error
Logger.Error(e, "Error while querying CivitModelQueryCache");
}
}
// If cached, update model cards
if (cachedQuery is not null)
{
var elapsed = timer.Elapsed;
Logger.Debug(
"Using cached query for {Text} [{RequestHash}] (in {Elapsed:F1} s)",
SearchQuery,
modelRequest.GetHashCode(),
elapsed.TotalSeconds
);
UpdateModelCards(cachedQuery.Items, cachedQuery.Metadata);
// Start remote query (background mode)
// Skip when last query was less than 2 min ago
var timeSinceCache = DateTimeOffset.UtcNow - cachedQuery.InsertedAt;
if (timeSinceCache?.TotalMinutes >= 2)
{
CivitModelQuery(modelRequest).SafeFireAndForget();
Logger.Debug(
"Cached query was more than 2 minutes ago ({Seconds:F0} s), updating cache with remote query",
timeSinceCache.Value.TotalSeconds
);
}
}
else
{
// Not cached, wait for remote query
ShowMainLoadingSpinner = true;
await CivitModelQuery(modelRequest);
}
UpdateResultsText();
}
public void FirstPage()
{
CurrentPageNumber = 1;
}
public void PreviousPage()
{
if (CurrentPageNumber == 1)
return;
CurrentPageNumber--;
}
public void NextPage()
{
if (CurrentPageNumber == TotalPages)
return;
CurrentPageNumber++;
}
public void LastPage()
{
CurrentPageNumber = TotalPages;
}
public void ClearSearchQuery()
{
SearchQuery = string.Empty;
}
partial void OnShowNsfwChanged(bool value)
{
settingsManager.Transaction(s => s.ModelBrowserNsfwEnabled, value);
// ModelCardsView?.Refresh();
var updateCards = allModelCards.Where(FilterModelCardsPredicate);
ModelCards = new ObservableCollection<CheckpointBrowserCardViewModel>(updateCards);
if (!HasSearched)
return;
UpdateResultsText();
}
partial void OnSelectedPeriodChanged(CivitPeriod value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
value,
SortMode,
SelectedModelType,
SelectedBaseModelType
)
);
}
partial void OnSortModeChanged(CivitSortMode value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
value,
SelectedModelType,
SelectedBaseModelType
)
);
}
partial void OnSelectedModelTypeChanged(CivitModelType value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
SortMode,
value,
SelectedBaseModelType
)
);
}
public override string Title => "Model Browser";
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true };
partial void OnSelectedBaseModelTypeChanged(string value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(
s =>
s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod,
SortMode,
SelectedModelType,
value
public IReadOnlyList<TabItem> Pages { get; } =
new List<TabItem>(
new List<TabViewModelBase>([civitAiBrowserViewModel, huggingFaceViewModel]).Select(
vm => new TabItem { Header = vm.Header, Content = vm }
)
);
}
private async Task TrySearchAgain(bool shouldUpdatePageNumber = true)
{
if (!HasSearched)
return;
ModelCards?.Clear();
if (shouldUpdatePageNumber)
{
CurrentPageNumber = 1;
}
// execute command instead of calling method directly so that the IsRunning property gets updated
await SearchModelsCommand.ExecuteAsync(null);
}
private void UpdateResultsText()
{
NoResultsFound = ModelCards?.Count <= 0;
NoResultsText =
allModelCards.Count > 0
? $"{allModelCards.Count} results hidden by filters"
: "No results found";
}
public override string Title => "Model Browser";
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.BrainCircuit, IsFilled = true };
}

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

@ -18,6 +18,7 @@ using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
@ -46,6 +47,7 @@ public partial class CheckpointFile : ViewModelBase
private string? previewImagePath;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsConnectedModel))]
private ConnectedModelInfo? connectedModel;
public bool IsConnectedModel => ConnectedModel != null;
@ -58,6 +60,9 @@ public partial class CheckpointFile : ViewModelBase
[ObservableProperty]
private CheckpointFolder parentFolder;
[ObservableProperty]
private ProgressReport? progress;
public string FileName => Path.GetFileName(FilePath);
public bool CanShowTriggerWords =>
@ -65,15 +70,8 @@ public partial class CheckpointFile : ViewModelBase
public ObservableCollection<string> Badges { get; set; } = new();
public static readonly string[] SupportedCheckpointExtensions =
{
".safetensors",
".pt",
".ckpt",
".pth",
".bin"
};
private static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg" };
public static readonly string[] SupportedCheckpointExtensions = { ".safetensors", ".pt", ".ckpt", ".pth", ".bin" };
private static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".gif" };
private static readonly string[] SupportedMetadataExtensions = { ".json" };
partial void OnConnectedModelChanged(ConnectedModelInfo? value)
@ -97,9 +95,7 @@ public partial class CheckpointFile : ViewModelBase
{
if (string.IsNullOrEmpty(FilePath))
{
throw new InvalidOperationException(
"Cannot get connected model info file path when FilePath is empty"
);
throw new InvalidOperationException("Cannot get connected model info file path when FilePath is empty");
}
var modelNameNoExt = Path.GetFileNameWithoutExtension((string?)FilePath);
var modelDir = Path.GetDirectoryName((string?)FilePath) ?? "";
@ -196,10 +192,7 @@ public partial class CheckpointFile : ViewModelBase
var cmInfoPath = Path.Combine(parentPath, $"{originalNameNoExt}.cm-info.json");
if (File.Exists(cmInfoPath))
{
File.Move(
cmInfoPath,
Path.Combine(parentPath, $"{nameNoExt}.cm-info.json")
);
File.Move(cmInfoPath, Path.Combine(parentPath, $"{nameNoExt}.cm-info.json"));
}
}
}
@ -231,6 +224,43 @@ public partial class CheckpointFile : ViewModelBase
return App.Clipboard.SetTextAsync(words);
}
[RelayCommand]
private async Task FindConnectedMetadata(bool forceReimport = false)
{
if (App.Services.GetService(typeof(IMetadataImportService)) is not IMetadataImportService importService)
return;
IsLoading = true;
try
{
var progressReport = new Progress<ProgressReport>(report =>
{
Progress = report;
});
var cmInfo = await importService.GetMetadataForFile(FilePath, progressReport, forceReimport);
if (cmInfo != null)
{
ConnectedModel = cmInfo;
PreviewImagePath = SupportedImageExtensions
.Select(
ext =>
Path.Combine(
ParentFolder.DirectoryPath,
$"{Path.GetFileNameWithoutExtension(FileName)}.preview{ext}"
)
)
.Where(File.Exists)
.FirstOrDefault();
}
}
finally
{
IsLoading = false;
}
}
/// <summary>
/// Indexes directory and yields all checkpoint files.
/// First we match all files with supported extensions.
@ -248,9 +278,7 @@ public partial class CheckpointFile : ViewModelBase
{
if (
!SupportedCheckpointExtensions.Any(
ext =>
Path.GetExtension(file)
.Equals(ext, StringComparison.InvariantCultureIgnoreCase)
ext => Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase)
)
)
continue;
@ -261,10 +289,7 @@ public partial class CheckpointFile : ViewModelBase
FilePath = Path.Combine(directory, file),
};
var jsonPath = Path.Combine(
directory,
$"{Path.GetFileNameWithoutExtension(file)}.cm-info.json"
);
var jsonPath = Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(file)}.cm-info.json");
if (File.Exists(jsonPath))
{
var json = File.ReadAllText(jsonPath);
@ -273,13 +298,7 @@ public partial class CheckpointFile : ViewModelBase
}
checkpointFile.PreviewImagePath = SupportedImageExtensions
.Select(
ext =>
Path.Combine(
directory,
$"{Path.GetFileNameWithoutExtension(file)}.preview{ext}"
)
)
.Select(ext => Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(file)}.preview{ext}"))
.Where(File.Exists)
.FirstOrDefault();
@ -296,28 +315,16 @@ public partial class CheckpointFile : ViewModelBase
public static IEnumerable<CheckpointFile> GetAllCheckpointFiles(string modelsDirectory)
{
foreach (
var file in Directory.EnumerateFiles(
modelsDirectory,
"*.*",
SearchOption.AllDirectories
)
)
foreach (var file in Directory.EnumerateFiles(modelsDirectory, "*.*", SearchOption.AllDirectories))
{
if (
!SupportedCheckpointExtensions.Any(
ext =>
Path.GetExtension(file)
.Equals(ext, StringComparison.InvariantCultureIgnoreCase)
ext => Path.GetExtension(file).Equals(ext, StringComparison.InvariantCultureIgnoreCase)
)
)
continue;
var checkpointFile = new CheckpointFile
{
Title = Path.GetFileNameWithoutExtension(file),
FilePath = file,
};
var checkpointFile = new CheckpointFile { Title = Path.GetFileNameWithoutExtension(file), FilePath = file };
var jsonPath = Path.Combine(
Path.GetDirectoryName(file) ?? "",
@ -425,6 +432,5 @@ public partial class CheckpointFile : ViewModelBase
}
}
public static IEqualityComparer<CheckpointFile> FilePathComparer { get; } =
new FilePathEqualityComparer();
public static IEqualityComparer<CheckpointFile> FilePathComparer { get; } = new FilePathEqualityComparer();
}

123
StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs

@ -37,12 +37,11 @@ public partial class CheckpointFolder : ViewModelBase
private readonly IDownloadService downloadService;
private readonly ModelFinder modelFinder;
private readonly INotificationService notificationService;
private readonly IMetadataImportService metadataImportService;
public SourceCache<CheckpointFolder, string> SubFoldersCache { get; } =
new(x => x.DirectoryPath);
public SourceCache<CheckpointFolder, string> SubFoldersCache { get; } = new(x => x.DirectoryPath);
private readonly SourceCache<CheckpointFile, string> checkpointFilesCache =
new(x => x.FilePath);
private readonly SourceCache<CheckpointFile, string> checkpointFilesCache = new(x => x.FilePath);
// ReSharper disable once FieldCanBeMadeReadOnly.Local
private bool useCategoryVisibility;
@ -111,6 +110,7 @@ public partial class CheckpointFolder : ViewModelBase
IDownloadService downloadService,
ModelFinder modelFinder,
INotificationService notificationService,
IMetadataImportService metadataImportService,
bool useCategoryVisibility = true
)
{
@ -118,6 +118,7 @@ public partial class CheckpointFolder : ViewModelBase
this.downloadService = downloadService;
this.modelFinder = modelFinder;
this.notificationService = notificationService;
this.metadataImportService = metadataImportService;
this.useCategoryVisibility = useCategoryVisibility;
checkpointFilesCache
@ -133,9 +134,7 @@ public partial class CheckpointFolder : ViewModelBase
.Sort(
SortExpressionComparer<CheckpointFile>
.Descending(f => f.IsConnectedModel)
.ThenByAscending(
f => f.IsConnectedModel ? f.ConnectedModel!.ModelName : f.FileName
)
.ThenByAscending(f => f.IsConnectedModel ? f.ConnectedModel!.ModelName : f.FileName)
)
.Filter(
f =>
@ -148,6 +147,12 @@ public partial class CheckpointFolder : ViewModelBase
SubFoldersCache
.Connect()
.DeferUntilLoaded()
.SubscribeMany(
folder =>
Observable
.FromEventPattern<EventArgs>(folder, nameof(ParentListRemoveRequested))
.Subscribe(_ => SubFoldersCache.Remove(folder))
)
.SortBy(x => x.Title)
.Bind(SubFolders)
.Subscribe();
@ -242,17 +247,10 @@ public partial class CheckpointFolder : ViewModelBase
return;
}
var dialog = DialogHelper.CreateTaskDialog(
"Are you sure you want to delete this folder?",
directory
);
var dialog = DialogHelper.CreateTaskDialog("Are you sure you want to delete this folder?", directory);
dialog.ShowProgressBar = false;
dialog.Buttons = new List<TaskDialogButton>
{
TaskDialogButton.YesButton,
TaskDialogButton.NoButton
};
dialog.Buttons = new List<TaskDialogButton> { TaskDialogButton.YesButton, TaskDialogButton.NoButton };
dialog.Closing += async (sender, e) =>
{
@ -306,12 +304,13 @@ public partial class CheckpointFolder : ViewModelBase
Directory.CreateDirectory(subFolderPath);
SubFolders.Add(
SubFoldersCache.AddOrUpdate(
new CheckpointFolder(
settingsManager,
downloadService,
modelFinder,
notificationService,
metadataImportService,
useCategoryVisibility: false
)
{
@ -333,23 +332,11 @@ public partial class CheckpointFolder : ViewModelBase
Progress.Value = 0;
var sourcePath = new FilePath(sourceFile.FilePath);
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(sourcePath);
var sourceCmInfoPath = Path.Combine(
sourcePath.Directory,
$"{fileNameWithoutExt}.cm-info.json"
);
var sourcePreviewPath = Path.Combine(
sourcePath.Directory,
$"{fileNameWithoutExt}.preview.jpeg"
);
var sourceCmInfoPath = Path.Combine(sourcePath.Directory, $"{fileNameWithoutExt}.cm-info.json");
var sourcePreviewPath = Path.Combine(sourcePath.Directory, $"{fileNameWithoutExt}.preview.jpeg");
var destinationFilePath = Path.Combine(DirectoryPath, sourcePath.Name);
var destinationCmInfoPath = Path.Combine(
DirectoryPath,
$"{fileNameWithoutExt}.cm-info.json"
);
var destinationPreviewPath = Path.Combine(
DirectoryPath,
$"{fileNameWithoutExt}.preview.jpeg"
);
var destinationCmInfoPath = Path.Combine(DirectoryPath, $"{fileNameWithoutExt}.cm-info.json");
var destinationPreviewPath = Path.Combine(DirectoryPath, $"{fileNameWithoutExt}.preview.jpeg");
// Move files
if (File.Exists(sourcePath))
@ -393,19 +380,12 @@ public partial class CheckpointFolder : ViewModelBase
/// <summary>
/// Imports files to the folder. Reports progress to instance properties.
/// </summary>
public async Task ImportFilesAsync(
IEnumerable<string> files,
bool convertToConnected = false,
bool copyFiles = true
)
public async Task ImportFilesAsync(IEnumerable<string> files, bool convertToConnected = false)
{
try
{
Progress.Value = 0;
var copyPaths = files.ToDictionary(
k => k,
v => Path.Combine(DirectoryPath, Path.GetFileName(v))
);
var copyPaths = files.ToDictionary(k => k, v => Path.Combine(DirectoryPath, Path.GetFileName(v)));
var progress = new Progress<ProgressReport>(report =>
{
@ -413,15 +393,10 @@ public partial class CheckpointFolder : ViewModelBase
Progress.Value = report.Percentage;
// For multiple files, add count
Progress.Text =
copyPaths.Count > 1
? $"Importing {report.Title} ({report.Message})"
: $"Importing {report.Title}";
copyPaths.Count > 1 ? $"Importing {report.Title} ({report.Message})" : $"Importing {report.Title}";
});
if (copyFiles)
{
await FileTransfers.CopyFiles(copyPaths, progress);
}
// Hash files and convert them to connected model if found
if (convertToConnected)
@ -459,12 +434,7 @@ public partial class CheckpointFolder : ViewModelBase
// Save connected model info json
var modelFileName = Path.GetFileNameWithoutExtension(modelFile.Info.Name);
var modelInfo = new ConnectedModelInfo(
model,
version,
file,
DateTimeOffset.UtcNow
);
var modelInfo = new ConnectedModelInfo(model, version, file, DateTimeOffset.UtcNow);
await modelInfo.SaveJsonToDirectory(DirectoryPath, modelFileName);
// If available, save thumbnail
@ -475,15 +445,9 @@ public partial class CheckpointFolder : ViewModelBase
if (imageExt is "jpg" or "jpeg" or "png")
{
var imageDownloadPath = Path.GetFullPath(
Path.Combine(
DirectoryPath,
$"{modelFileName}.preview.{imageExt}"
)
);
await downloadService.DownloadToFileAsync(
image.Url,
imageDownloadPath
Path.Combine(DirectoryPath, $"{modelFileName}.preview.{imageExt}")
);
await downloadService.DownloadToFileAsync(image.Url, imageDownloadPath);
}
}
@ -531,14 +495,21 @@ public partial class CheckpointFolder : ViewModelBase
public async Task FindConnectedMetadata()
{
IsImportInProgress = true;
var files = CheckpointFiles
.Where(f => !f.IsConnectedModel)
.Select(f => f.FilePath)
.ToList();
var files = CheckpointFiles.Where(f => !f.IsConnectedModel).Select(f => f.FilePath).ToList();
if (files.Any())
try
{
if (files.Count > 0)
{
await ImportFilesAsync(files, true, false);
var progress = new Progress<ProgressReport>(report =>
{
Progress.IsIndeterminate = report.IsIndeterminate;
Progress.Value = report.Percentage;
Progress.Text = report.Title;
});
await metadataImportService.ScanDirectoryForMissingInfo(DirectoryPath, progress);
BackgroundIndex();
notificationService.Show("Scan Complete", "Finished scanning for missing metadata");
}
else
{
@ -549,6 +520,11 @@ public partial class CheckpointFolder : ViewModelBase
);
}
}
finally
{
DelayedClearProgress(TimeSpan.FromSeconds(1.5));
}
}
/// <summary>
/// Clears progress after a delay.
@ -591,6 +567,7 @@ public partial class CheckpointFolder : ViewModelBase
/// </summary>
public void Index()
{
var updatedFolders = new List<CheckpointFolder>();
// Get subfolders
foreach (var folder in Directory.GetDirectories(DirectoryPath))
{
@ -598,6 +575,7 @@ public partial class CheckpointFolder : ViewModelBase
if (SubFoldersCache.Lookup(folder) is { HasValue: true } result)
{
result.Value.BackgroundIndex();
updatedFolders.Add(result.Value);
}
else
{
@ -607,6 +585,7 @@ public partial class CheckpointFolder : ViewModelBase
downloadService,
modelFinder,
notificationService,
metadataImportService,
useCategoryVisibility: false
)
{
@ -617,12 +596,16 @@ public partial class CheckpointFolder : ViewModelBase
IsExpanded = false, // Subfolders are collapsed by default
};
subFolder.BackgroundIndex();
SubFoldersCache.AddOrUpdate(subFolder);
updatedFolders.Add(subFolder);
}
}
SubFoldersCache.EditDiff(updatedFolders, (a, b) => a.Title == b.Title);
// Index files
Dispatcher.UIThread.Post(
Dispatcher
.UIThread
.Post(
() =>
{
var files = GetCheckpointFiles();

86
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
@ -16,6 +18,7 @@ using StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
@ -34,11 +37,11 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
private readonly ModelFinder modelFinder;
private readonly IDownloadService downloadService;
private readonly INotificationService notificationService;
private readonly IMetadataImportService metadataImportService;
public override string Title => "Checkpoints";
public override IconSource IconSource =>
new SymbolIconSource { Symbol = Symbol.Notebook, IsFilled = true };
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Notebook, IsFilled = true };
// Toggle button for auto hashing new drag-and-dropped files for connected upgrade
[ObservableProperty]
@ -59,18 +62,18 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
[ObservableProperty]
private bool isCategoryTipOpen;
[ObservableProperty]
private ProgressReport progress;
partial void OnIsImportAsConnectedChanged(bool value)
{
if (
settingsManager.IsLibraryDirSet && value != settingsManager.Settings.IsImportAsConnected
)
if (settingsManager.IsLibraryDirSet && value != settingsManager.Settings.IsImportAsConnected)
{
settingsManager.Transaction(s => s.IsImportAsConnected = value);
}
}
public SourceCache<CheckpointFolder, string> CheckpointFoldersCache { get; } =
new(x => x.DirectoryPath);
public SourceCache<CheckpointFolder, string> CheckpointFoldersCache { get; } = new(x => x.DirectoryPath);
public IObservableCollection<CheckpointFolder> CheckpointFolders { get; } =
new ObservableCollectionExtended<CheckpointFolder>();
@ -83,6 +86,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
ISettingsManager settingsManager,
IDownloadService downloadService,
INotificationService notificationService,
IMetadataImportService metadataImportService,
ModelFinder modelFinder
)
{
@ -90,6 +94,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
this.settingsManager = settingsManager;
this.downloadService = downloadService;
this.notificationService = notificationService;
this.metadataImportService = metadataImportService;
this.modelFinder = modelFinder;
CheckpointFoldersCache
@ -118,14 +123,10 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
if (Design.IsDesignMode)
return;
if (
!settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.CheckpointCategoriesTip)
)
if (!settingsManager.Settings.SeenTeachingTips.Contains(TeachingTip.CheckpointCategoriesTip))
{
IsCategoryTipOpen = true;
settingsManager.Transaction(
s => s.SeenTeachingTips.Add(TeachingTip.CheckpointCategoriesTip)
);
settingsManager.Transaction(s => s.SeenTeachingTips.Add(TeachingTip.CheckpointCategoriesTip));
}
IsLoading = CheckpointFolders.Count == 0;
@ -156,10 +157,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
partial void OnShowConnectedModelImagesChanged(bool value)
{
if (
settingsManager.IsLibraryDirSet
&& value != settingsManager.Settings.ShowConnectedModelImages
)
if (settingsManager.IsLibraryDirSet && value != settingsManager.Settings.ShowConnectedModelImages)
{
settingsManager.Transaction(s => s.ShowConnectedModelImages = value);
}
@ -176,9 +174,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
}
// Check files in the current folder
return folder.CheckpointFiles.Any(
x => x.FileName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase)
)
return folder.CheckpointFiles.Any(x => x.FileName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase))
||
// If no matching files were found in the current folder, check in all subfolders
folder.SubFolders.Any(ContainsSearchFilter);
@ -195,14 +191,16 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
var sw = Stopwatch.StartNew();
// Index all folders
var updatedFolders = new List<CheckpointFolder>();
// Index all folders
foreach (var folder in folders)
{
// Get from cache or create new
if (CheckpointFoldersCache.Lookup(folder) is { HasValue: true } result)
{
result.Value.Index();
updatedFolders.Add(result.Value);
}
else
{
@ -210,7 +208,8 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
settingsManager,
downloadService,
modelFinder,
notificationService
notificationService,
metadataImportService
)
{
Title = Path.GetFileName(folder),
@ -218,10 +217,12 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
IsExpanded = true // Top level folders expanded by default
};
checkpointFolder.Index();
CheckpointFoldersCache.AddOrUpdate(checkpointFolder);
updatedFolders.Add(checkpointFolder);
}
}
CheckpointFoldersCache.EditDiff(updatedFolders, (a, b) => a.Title == b.Title);
sw.Stop();
Logger.Info($"IndexFolders in {sw.Elapsed.TotalMilliseconds:F1}ms");
}
@ -231,4 +232,43 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
{
await ProcessRunner.OpenFolderBrowser(settingsManager.ModelsDirectory);
}
[RelayCommand]
private async Task FindConnectedMetadata()
{
var progressHandler = new Progress<ProgressReport>(report =>
{
Progress = report;
});
await metadataImportService.ScanDirectoryForMissingInfo(settingsManager.ModelsDirectory, progressHandler);
notificationService.Show("Scan Complete", "Finished scanning for missing metadata.", NotificationType.Success);
DelayedClearProgress(TimeSpan.FromSeconds(1.5));
}
[RelayCommand]
private async Task UpdateExistingMetadata()
{
var progressHandler = new Progress<ProgressReport>(report =>
{
Progress = report;
});
await metadataImportService.UpdateExistingMetadata(settingsManager.ModelsDirectory, progressHandler);
notificationService.Show("Scan Complete", "Finished updating metadata.", NotificationType.Success);
DelayedClearProgress(TimeSpan.FromSeconds(1.5));
}
private void DelayedClearProgress(TimeSpan delay)
{
Task.Delay(delay)
.ContinueWith(_ =>
{
IsLoading = false;
Progress = new ProgressReport(0, 0);
});
}
}

52
StabilityMatrix.Avalonia/ViewModels/HuggingFacePage/CategoryViewModel.cs

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using DynamicData.Binding;
using StabilityMatrix.Avalonia.Models.HuggingFace;
using StabilityMatrix.Avalonia.ViewModels.Base;
namespace StabilityMatrix.Avalonia.ViewModels.HuggingFacePage;
public partial class CategoryViewModel : ViewModelBase
{
[ObservableProperty]
private IObservableCollection<HuggingfaceItemViewModel> items =
new ObservableCollectionExtended<HuggingfaceItemViewModel>();
public SourceCache<HuggingfaceItem, string> ItemsCache { get; } = new(i => i.RepositoryPath + i.ModelName);
[ObservableProperty]
private string? title;
[ObservableProperty]
private bool isChecked;
[ObservableProperty]
private int numSelected;
public CategoryViewModel(IEnumerable<HuggingfaceItem> items)
{
ItemsCache
.Connect()
.DeferUntilLoaded()
.Transform(i => new HuggingfaceItemViewModel { Item = i })
.Bind(Items)
.WhenPropertyChanged(p => p.IsSelected)
.Subscribe(_ => NumSelected = Items.Count(i => i.IsSelected));
ItemsCache.EditDiff(items, (a, b) => a.RepositoryPath == b.RepositoryPath);
}
partial void OnIsCheckedChanged(bool value)
{
if (Items is null)
return;
foreach (var item in Items)
{
item.IsSelected = value;
}
}
}

25
StabilityMatrix.Avalonia/ViewModels/HuggingFacePage/HuggingfaceItemViewModel.cs

@ -0,0 +1,25 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.Models.HuggingFace;
using StabilityMatrix.Avalonia.ViewModels.Base;
namespace StabilityMatrix.Avalonia.ViewModels.HuggingFacePage;
public partial class HuggingfaceItemViewModel : ViewModelBase
{
[ObservableProperty]
private HuggingfaceItem item;
[ObservableProperty]
private bool isSelected;
public string LicenseUrl =>
$"https://huggingface.co/{Item.RepositoryPath}/blob/main/{Item.LicensePath ?? "README.md"}";
public string RepoUrl => $"https://huggingface.co/{Item.RepositoryPath}";
[RelayCommand]
private void ToggleSelected()
{
IsSelected = !IsSelected;
}
}

527
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml

@ -1,531 +1,18 @@
<UserControl
x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage"
xmlns="https://github.com/avaloniaui"
<controls:UserControlBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}"
d:DesignHeight="700"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="viewModels:CheckpointBrowserViewModel"
mc:Ignorable="d">
<UserControl.Styles>
<Style Selector="Border#HoverBorder">
<Setter Property="Transitions">
<Transitions>
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.237"/>
</Transitions>
</Setter>
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform"
Duration="0:0:0.237">
<TransformOperationsTransition.Easing>
<QuadraticEaseInOut/>
</TransformOperationsTransition.Easing>
</TransformOperationsTransition>
</Transitions>
</Setter>
</Style>
<Style Selector="^:pointerover">
<Setter Property="BoxShadow" Value="0 0 40 0 #60000000"/>
<Setter Property="Cursor" Value="Hand" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="RenderTransform" Value="scale(1.03, 1.03)"/>
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#CC000000" />
</Style>
</Style>
<Style Selector="^:not(:pointerover)">
<Setter Property="BoxShadow" Value="0 0 20 0 #60000000"/>
<Setter Property="Cursor" Value="Arrow" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="RenderTransform" Value="scale(1, 1)"/>
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#99000000" />
</Style>
</Style>
</Style>
</UserControl.Styles>
<UserControl.Resources>
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter"/>
<DataTemplate x:Key="CivitModelTemplate" DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}">
<Border
Name="HoverBorder"
Padding="0"
BorderThickness="0"
Margin="8"
ClipToBounds="True"
CornerRadius="8">
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="{x:Static lang:Resources.Action_OpenOnCivitAi}"
Command="{Binding OpenModelCommand}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Border.ContextFlyout>
<Button
Name="ModelCard"
Classes="transparent-full"
Padding="0"
BorderThickness="0"
VerticalContentAlignment="Top"
CornerRadius="8"
Command="{Binding ShowVersionDialogCommand}"
CommandParameter="{Binding CivitModel}"
IsEnabled="{Binding !IsImporting}">
<Grid RowDefinitions="*, Auto">
<controls:BetterAdvancedImage
Grid.RowSpan="2"
CornerRadius="8"
Width="330"
Height="400"
Source="{Binding CardImage}"
Stretch="UniformToFill"
StretchDirection="Both"/>
<StackPanel
Grid.Row="0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="0,8,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Classes="transparent-info"
Command="{Binding ToggleFavoriteCommand}"
FontSize="20"
IsVisible="{Binding !IsFavorite}">
<Grid>
<ui:SymbolIcon Symbol="StarAdd" />
</Grid>
</Button>
<Button
Margin="0,8,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Classes="success"
Command="{Binding ToggleFavoriteCommand}"
FontSize="20"
IsVisible="{Binding IsFavorite}">
<Grid>
<ui:SymbolIcon Symbol="StarFilled" />
</Grid>
</Button>
</StackPanel>
<!-- Username pill card -->
<Border
BoxShadow="inset 1.2 0 80 1.8 #66000000"
CornerRadius="16"
Margin="4"
Grid.Row="0"
HorizontalAlignment="Left"
VerticalAlignment="Bottom">
<Border.Resources>
<DropShadowEffect
x:Key="TextDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.9"/>
<DropShadowEffect
x:Key="ImageDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.2"/>
</Border.Resources>
<Button
Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding CivitModel.Creator.ProfileUrl}"
CornerRadius="16"
Classes="transparent"
Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<controls:BetterAdvancedImage
Width="22"
Height="22"
Effect="{StaticResource ImageDropShadowEffect}"
CornerRadius="11"
RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding CivitModel.Creator.Image, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Source="{Binding CivitModel.Creator.Image}"/>
<TextBlock
VerticalAlignment="Center"
Effect="{StaticResource TextDropShadowEffect}"
Text="{Binding CivitModel.Creator.Username}"/>
</StackPanel>
</Button>
</Border>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<controls:Card
Height="24"
Margin="8,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="info">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding CivitModel.Type}" />
</controls:Card>
<controls:Card
Height="24"
Margin="4,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="info">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding CivitModel.BaseModelType}" />
</controls:Card>
<controls:Card
Height="24"
Margin="4,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="success"
IsVisible="{Binding ShowUpdateCard}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding UpdateCardText}" />
</controls:Card>
</StackPanel>
<Border
Grid.Row="0"
Grid.RowSpan="2"
Margin="0,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="#DD000000"
CornerRadius="8"
IsVisible="{Binding IsImporting}"
ZIndex="1" />
<StackPanel
Grid.Row="0"
Grid.RowSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{Binding IsImporting}"
Orientation="Vertical"
ZIndex="2">
<controls:ProgressRing
Width="120"
Height="120"
HorizontalAlignment="Center"
VerticalAlignment="Center"
EndAngle="450"
IsIndeterminate="False"
StartAngle="90"
Value="{Binding Value}" />
<TextBlock
Margin="0,8,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Text, TargetNullValue=Importing...}" />
</StackPanel>
<Border
Name="ModelCardBottom"
Grid.Row="1">
<Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto, Auto, Auto">
<!--
TextTrimming causing issues with unicode chars until
https://github.com/AvaloniaUI/Avalonia/pull/13385 is released
-->
<TextBlock
Grid.ColumnSpan="2"
MaxWidth="250"
Margin="8,0,8,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource TextControlForeground}"
LetterSpacing="0.33"
Text="{Binding CivitModel.Name}"
TextWrapping="NoWrap"
ToolTip.Tip="{Binding CivitModel.Name}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="8,-4,0,0"
VerticalAlignment="Center"
FontSize="11"
Foreground="{DynamicResource TextControlForeground}"
Text="{Binding CivitModel.LatestModelVersionName, FallbackValue=''}" />
<StackPanel
Grid.Row="2"
Grid.Column="0"
Orientation="Horizontal">
<controls:StarsRating
Margin="8,8,0,8"
Background="#66000000"
FontSize="16"
Foreground="{DynamicResource ThemeEldenRingOrangeColor}"
Value="{Binding CivitModel.Stats.Rating}" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.RatingCount}"
TextAlignment="Center" />
</StackPanel>
<StackPanel
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal">
<avalonia:Icon Value="fa-solid fa-heart" />
<TextBlock
Margin="4,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.FavoriteCount, Converter={StaticResource KiloFormatterConverter}}" />
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" />
<TextBlock
Margin="0,0,4,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.DownloadCount, Converter={StaticResource KiloFormatterConverter}}" />
</StackPanel>
<Button
Grid.Row="0"
Grid.Column="1"
Width="32"
Margin="0,4,4,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
HorizontalContentAlignment="Right"
VerticalContentAlignment="Top"
BorderThickness="0"
Classes="transparent">
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
<Button.Flyout>
<MenuFlyout>
<MenuItem Command="{Binding OpenModelCommand}" Header="{x:Static lang:Resources.Action_OpenOnCivitAi}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</Grid>
</Border>
</Grid>
</Button>
</Border>
</DataTemplate>
</UserControl.Resources>
<Grid Margin="0,8,0,0" RowDefinitions="Auto,*,Auto">
<StackPanel Margin="8" Orientation="Vertical">
<Grid ColumnDefinitions="*,Auto">
<TextBox
Margin="8,0,0,0"
HorizontalAlignment="Stretch"
KeyDown="InputElement_OnKeyDown"
Text="{Binding SearchQuery, Mode=TwoWay}"
Watermark="{x:Static lang:Resources.Label_ModelSearchWatermark}">
<TextBox.InnerRightContent>
<Button
Classes="transparent-full"
Command="{Binding ClearSearchQuery}"
IsVisible="{Binding SearchQuery.Length}">
<ui:SymbolIcon Symbol="Cancel" />
</Button>
</TextBox.InnerRightContent>
</TextBox>
<Button
Grid.Column="1"
Width="80"
Margin="8,0,8,0"
VerticalAlignment="Stretch"
Classes="accent"
Command="{Binding SearchModelsCommand}"
IsDefault="True">
<Grid>
<controls:ProgressRing
MinWidth="16"
MinHeight="16"
VerticalAlignment="Center"
BorderThickness="4"
IsIndeterminate="True"
IsVisible="{Binding SearchModelsCommand.IsRunning}" />
<TextBlock
VerticalAlignment="Center"
IsVisible="{Binding !SearchModelsCommand.IsRunning}"
Text="{x:Static lang:Resources.Action_Search}" />
</Grid>
</Button>
</Grid>
<DockPanel>
<StackPanel Margin="8,8,4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_Sort}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllSortModes}"
SelectedItem="{Binding SortMode}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_TimePeriod}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllCivitPeriods}"
SelectedItem="{Binding SelectedPeriod}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_ModelType}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllModelTypes}"
SelectedItem="{Binding SelectedModelType}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_BaseModel}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding BaseModelOptions}"
SelectedItem="{Binding SelectedBaseModelType}" />
</StackPanel>
<CheckBox
Margin="8,8,8,0"
HorizontalAlignment="Right"
Content="{x:Static lang:Resources.Label_ShowNsfwContent}"
IsChecked="{Binding ShowNsfw, Mode=TwoWay}" />
</DockPanel>
</StackPanel>
<ScrollViewer
Grid.Row="1"
Margin="8,0,8,0"
ScrollChanged="ScrollViewer_OnScrollChanged">
<ItemsRepeater ItemTemplate="{StaticResource CivitModelTemplate}"
HorizontalAlignment="Center"
ItemsSource="{Binding ModelCards}">
<ItemsRepeater.Layout>
<UniformGridLayout Orientation="Horizontal" />
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>
<TextBlock
Grid.Row="2"
Margin="16,8"
VerticalAlignment="Bottom"
Text="{x:Static lang:Resources.Label_DataProvidedByCivitAi}" />
<StackPanel
Grid.Row="2"
Margin="8"
HorizontalAlignment="Center"
IsVisible="{Binding HasSearched}"
Orientation="Vertical">
<TextBlock Margin="0,0,4,4" TextAlignment="Center">
<Run Text="{x:Static lang:Resources.Label_Page}" />
<Run Text="{Binding CurrentPageNumber, FallbackValue=1}" />
<Run Text="/" />
<Run Text="{Binding TotalPages, FallbackValue=5}" />
</TextBlock>
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button
Margin="0,0,8,0"
Command="{Binding FirstPage}"
IsEnabled="{Binding CanGoToFirstPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}">
<avalonia:Icon Value="fa-solid fa-backward-fast" />
</Button>
<Button
Margin="0,0,8,0"
Command="{Binding PreviousPage}"
IsEnabled="{Binding CanGoToPreviousPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}">
<avalonia:Icon Value="fa-solid fa-caret-left" />
</Button>
<Button
Margin="0,0,8,0"
Command="{Binding NextPage}"
IsEnabled="{Binding CanGoToNextPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}">
<avalonia:Icon Value="fa-solid fa-caret-right" />
</Button>
<Button
Command="{Binding LastPage}"
IsEnabled="{Binding CanGoToLastPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}">
<avalonia:Icon Value="fa-solid fa-forward-fast" />
</Button>
</StackPanel>
</StackPanel>
<TextBlock
Grid.Row="0"
Grid.RowSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
IsVisible="{Binding NoResultsFound}"
Text="{Binding NoResultsText, FallbackValue=No results found}" />
<controls:ProgressRing
Grid.Row="1"
Width="128"
Height="128"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsIndeterminate="True"
IsVisible="{Binding ShowMainLoadingSpinner, FallbackValue=False}" />
</Grid>
</UserControl>
mc:Ignorable="d"
x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage">
<TabControl ItemsSource="{Binding Pages}"/>
</controls:UserControlBase>

29
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();
}
}
}

89
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -56,14 +56,13 @@
</Interaction.Behaviors>
<controls:Card MaxWidth="330" Width="330"
Padding="0"
CornerRadius="12">
<!-- Right click menu for a checkpoint file -->
<controls:Card.ContextFlyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem Command="{Binding RenameCommand}"
Text="{x:Static lang:Resources.Action_Rename}" IconSource="Rename" />
<ui:MenuFlyoutItem Command="{Binding DeleteCommand}"
Text="{x:Static lang:Resources.Action_Delete}" IconSource="Delete" />
<ui:MenuFlyoutItem Command="{Binding OpenOnCivitAiCommand}"
Text="{x:Static lang:Resources.Action_OpenOnCivitAi}" IconSource="Link"
IsVisible="{Binding IsConnectedModel}" />
@ -71,11 +70,31 @@
IsVisible="{Binding CanShowTriggerWords}"
Text="{x:Static lang:Resources.Action_CopyTriggerWords}"
IconSource="Copy" />
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Label_FindConnectedMetadata}"
IconSource="Find"
Command="{Binding FindConnectedMetadataCommand}"
IsVisible="{Binding !IsConnectedModel}">
<ui:MenuFlyoutItem.CommandParameter>
<system:Boolean>False</system:Boolean>
</ui:MenuFlyoutItem.CommandParameter>
</ui:MenuFlyoutItem>
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Action_UpdateExistingMetadata}"
IconSource="Sync"
Command="{Binding FindConnectedMetadataCommand}"
IsVisible="{Binding IsConnectedModel}">
<ui:MenuFlyoutItem.CommandParameter>
<system:Boolean>True</system:Boolean>
</ui:MenuFlyoutItem.CommandParameter>
</ui:MenuFlyoutItem>
<ui:MenuFlyoutItem Command="{Binding DeleteCommand}"
Text="{x:Static lang:Resources.Action_Delete}"
IconSource="Delete" />
</ui:FAMenuFlyout>
</controls:Card.ContextFlyout>
<Grid>
<Grid RowDefinitions="Auto, Auto">
<!-- Main contents, hidden when IsLoading is true -->
<StackPanel MinHeight="70">
<StackPanel Grid.Row="0" Grid.RowSpan="2"
MinHeight="70" Margin="14, 16, 14, 16">
<Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto,*,Auto,Auto"
IsVisible="{Binding !IsLoading}">
<StackPanel
@ -230,6 +249,19 @@
</Grid>
</StackPanel>
<Border Grid.Row="0"
Grid.RowSpan="2"
Background="#AA000000"
IsVisible="{Binding IsLoading}"
CornerRadius="12"/>
<TextBlock Grid.Row="1"
x:CompileBindings="False"
Text="{Binding Progress.Title}"
TextAlignment="Center"
IsVisible="{Binding IsLoading}"
VerticalAlignment="Center"/>
<!-- Progress ring -->
<controls:ProgressRing
Grid.Row="0"
@ -407,7 +439,7 @@
</controls:UserControlBase.Resources>
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,*" Margin="4, 0"
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,*" Margin="4, 0"
x:Name="ParentGrid">
<!-- Top settings bar -->
<StackPanel Orientation="Horizontal">
@ -463,6 +495,36 @@
HorizontalContentAlignment="Right"
DefaultLabelPosition="Right">
<ui:CommandBar.PrimaryCommands>
<ui:CommandBarElementContainer>
<SplitButton
Classes="transparent-full"
HorizontalAlignment="Left"
FontFamily="{DynamicResource ContentControlThemeFontFamily}"
FontWeight="Normal"
FontSize="12"
Command="{Binding FindConnectedMetadataCommand}"
Padding="8">
<SplitButton.Content>
<Grid ColumnDefinitions="Auto, Auto">
<ui:SymbolIcon Grid.Column="0"
Symbol="CloudDownload"
FontSize="18"/>
<TextBlock Grid.Column="1"
Text="{x:Static lang:Resources.Label_FindConnectedMetadata}"
Margin="8,0,0,0"
VerticalAlignment="Center"/>
</Grid>
</SplitButton.Content>
<SplitButton.Flyout>
<ui:FAMenuFlyout
Placement="Bottom">
<ui:MenuFlyoutItem Text="{x:Static lang:Resources.Action_UpdateExistingMetadata}"
Command="{Binding UpdateExistingMetadataCommand}"/>
</ui:FAMenuFlyout>
</SplitButton.Flyout>
</SplitButton>
</ui:CommandBarElementContainer>
<ui:CommandBarButton
IconSource="OpenFolder"
VerticalAlignment="Center"
@ -495,6 +557,19 @@
PreferredPlacement="Bottom"
IsOpen="{Binding IsCategoryTipOpen}" />
<!-- Metadata scan progress -->
<StackPanel Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
IsVisible="{Binding !!Progress.Total}"
Orientation="Vertical">
<TextBlock Text="{Binding Progress.Title}"
TextAlignment="Center"
Margin="0,0,0,4"/>
<ProgressBar Value="{Binding Progress.Percentage}"
Margin="16,4,16,16"/>
</StackPanel>
<StackPanel
IsVisible="False"
Grid.Column="1"
@ -519,7 +594,7 @@
<ScrollViewer
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="1"
Grid.Row="2"
x:Name="MainScrollViewer"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
@ -546,7 +621,7 @@
<!-- Overlay for draggable file panels -->
<Panel Name="OverlayPanel"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" />
Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" />
</Grid>
</controls:UserControlBase>

33
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)
{
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;
}
}

531
StabilityMatrix.Avalonia/Views/CivitAiBrowserPage.axaml

@ -0,0 +1,531 @@
<controls:UserControlBase
x:Class="StabilityMatrix.Avalonia.Views.CivitAiBrowserPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager"
xmlns:converters="clr-namespace:StabilityMatrix.Avalonia.Converters"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
d:DataContext="{x:Static designData:DesignData.CivitAiBrowserViewModel}"
d:DesignHeight="700"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="checkpointBrowser:CivitAiBrowserViewModel"
mc:Ignorable="d">
<UserControl.Styles>
<Style Selector="Border#HoverBorder">
<Setter Property="Transitions">
<Transitions>
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.237"/>
</Transitions>
</Setter>
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform"
Duration="0:0:0.237">
<TransformOperationsTransition.Easing>
<QuadraticEaseInOut/>
</TransformOperationsTransition.Easing>
</TransformOperationsTransition>
</Transitions>
</Setter>
</Style>
<Style Selector="^:pointerover">
<Setter Property="BoxShadow" Value="0 0 40 0 #60000000"/>
<Setter Property="Cursor" Value="Hand" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="RenderTransform" Value="scale(1.03, 1.03)"/>
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#CC000000" />
</Style>
</Style>
<Style Selector="^:not(:pointerover)">
<Setter Property="BoxShadow" Value="0 0 20 0 #60000000"/>
<Setter Property="Cursor" Value="Arrow" />
<Style Selector="^ asyncImageLoader|AdvancedImage">
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="RenderTransform" Value="scale(1, 1)"/>
</Style>
<Style Selector="^ Border#ModelCardBottom">
<Setter Property="Background" Value="#99000000" />
</Style>
</Style>
</Style>
</UserControl.Styles>
<UserControl.Resources>
<converters:KiloFormatterStringConverter x:Key="KiloFormatterConverter"/>
<DataTemplate x:Key="CivitModelTemplate" DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}">
<Border
Name="HoverBorder"
Padding="0"
BorderThickness="0"
Margin="8"
ClipToBounds="True"
CornerRadius="8">
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="{x:Static lang:Resources.Action_OpenOnCivitAi}"
Command="{Binding OpenModelCommand}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Border.ContextFlyout>
<Button
Name="ModelCard"
Classes="transparent-full"
Padding="0"
BorderThickness="0"
VerticalContentAlignment="Top"
CornerRadius="8"
Command="{Binding ShowVersionDialogCommand}"
CommandParameter="{Binding CivitModel}"
IsEnabled="{Binding !IsImporting}">
<Grid RowDefinitions="*, Auto">
<controls:BetterAdvancedImage
Grid.RowSpan="2"
CornerRadius="8"
Width="330"
Height="400"
Source="{Binding CardImage}"
Stretch="UniformToFill"
StretchDirection="Both"/>
<StackPanel
Grid.Row="0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Margin="0,8,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Classes="transparent-info"
Command="{Binding ToggleFavoriteCommand}"
FontSize="20"
IsVisible="{Binding !IsFavorite}">
<Grid>
<ui:SymbolIcon Symbol="StarAdd" />
</Grid>
</Button>
<Button
Margin="0,8,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Classes="success"
Command="{Binding ToggleFavoriteCommand}"
FontSize="20"
IsVisible="{Binding IsFavorite}">
<Grid>
<ui:SymbolIcon Symbol="StarFilled" />
</Grid>
</Button>
</StackPanel>
<!-- Username pill card -->
<Border
BoxShadow="inset 1.2 0 80 1.8 #66000000"
CornerRadius="16"
Margin="4"
Grid.Row="0"
HorizontalAlignment="Left"
VerticalAlignment="Bottom">
<Border.Resources>
<DropShadowEffect
x:Key="TextDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.9"/>
<DropShadowEffect
x:Key="ImageDropShadowEffect"
BlurRadius="12"
Color="#FF000000"
Opacity="0.2"/>
</Border.Resources>
<Button
Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding CivitModel.Creator.ProfileUrl}"
CornerRadius="16"
Classes="transparent"
Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<controls:BetterAdvancedImage
Width="22"
Height="22"
Effect="{StaticResource ImageDropShadowEffect}"
CornerRadius="11"
RenderOptions.BitmapInterpolationMode="HighQuality"
IsVisible="{Binding CivitModel.Creator.Image, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Source="{Binding CivitModel.Creator.Image}"/>
<TextBlock
VerticalAlignment="Center"
Effect="{StaticResource TextDropShadowEffect}"
Text="{Binding CivitModel.Creator.Username}"/>
</StackPanel>
</Button>
</Border>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<controls:Card
Height="24"
Margin="8,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="info">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding CivitModel.Type}" />
</controls:Card>
<controls:Card
Height="24"
Margin="4,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="info">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding CivitModel.BaseModelType}" />
</controls:Card>
<controls:Card
Height="24"
Margin="4,8,0,0"
Padding="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Classes="success"
IsVisible="{Binding ShowUpdateCard}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Text="{Binding UpdateCardText}" />
</controls:Card>
</StackPanel>
<Border
Grid.Row="0"
Grid.RowSpan="2"
Margin="0,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="#DD000000"
CornerRadius="8"
IsVisible="{Binding IsImporting}"
ZIndex="1" />
<StackPanel
Grid.Row="0"
Grid.RowSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsVisible="{Binding IsImporting}"
Orientation="Vertical"
ZIndex="2">
<controls:ProgressRing
Width="120"
Height="120"
HorizontalAlignment="Center"
VerticalAlignment="Center"
EndAngle="450"
IsIndeterminate="False"
StartAngle="90"
Value="{Binding Value}" />
<TextBlock
Margin="0,8,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Text, TargetNullValue=Importing...}" />
</StackPanel>
<Border
Name="ModelCardBottom"
Grid.Row="1">
<Grid ColumnDefinitions="*, Auto" RowDefinitions="Auto, Auto, Auto">
<!--
TextTrimming causing issues with unicode chars until
https://github.com/AvaloniaUI/Avalonia/pull/13385 is released
-->
<TextBlock
Grid.ColumnSpan="2"
MaxWidth="250"
Margin="8,0,8,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource TextControlForeground}"
LetterSpacing="0.33"
Text="{Binding CivitModel.Name}"
TextWrapping="NoWrap"
ToolTip.Tip="{Binding CivitModel.Name}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="8,-4,0,0"
VerticalAlignment="Center"
FontSize="11"
Foreground="{DynamicResource TextControlForeground}"
Text="{Binding CivitModel.LatestModelVersionName, FallbackValue=''}" />
<StackPanel
Grid.Row="2"
Grid.Column="0"
Orientation="Horizontal">
<controls:StarsRating
Margin="8,8,0,8"
Background="#66000000"
FontSize="16"
Foreground="{DynamicResource ThemeEldenRingOrangeColor}"
Value="{Binding CivitModel.Stats.Rating}" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.RatingCount}"
TextAlignment="Center" />
</StackPanel>
<StackPanel
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal">
<avalonia:Icon Value="fa-solid fa-heart" />
<TextBlock
Margin="4,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.FavoriteCount, Converter={StaticResource KiloFormatterConverter}}" />
<avalonia:Icon Margin="4,0" Value="fa-solid fa-download" />
<TextBlock
Margin="0,0,4,0"
VerticalAlignment="Center"
Text="{Binding CivitModel.Stats.DownloadCount, Converter={StaticResource KiloFormatterConverter}}" />
</StackPanel>
<Button
Grid.Row="0"
Grid.Column="1"
Width="32"
Margin="0,4,4,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
HorizontalContentAlignment="Right"
VerticalContentAlignment="Top"
BorderThickness="0"
Classes="transparent">
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
<Button.Flyout>
<MenuFlyout>
<MenuItem Command="{Binding OpenModelCommand}" Header="{x:Static lang:Resources.Action_OpenOnCivitAi}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</Grid>
</Border>
</Grid>
</Button>
</Border>
</DataTemplate>
</UserControl.Resources>
<Grid RowDefinitions="Auto,*,Auto">
<StackPanel Margin="8" Orientation="Vertical">
<Grid ColumnDefinitions="*,Auto">
<TextBox
HorizontalAlignment="Stretch"
KeyDown="InputElement_OnKeyDown"
Text="{Binding SearchQuery, Mode=TwoWay}"
Watermark="{x:Static lang:Resources.Label_ModelSearchWatermark}">
<TextBox.InnerRightContent>
<Button
Classes="transparent-full"
Command="{Binding ClearSearchQuery}"
IsVisible="{Binding SearchQuery.Length}">
<ui:SymbolIcon Symbol="Cancel" />
</Button>
</TextBox.InnerRightContent>
</TextBox>
<Button
Grid.Column="1"
Width="80"
Margin="8,0,8,0"
VerticalAlignment="Stretch"
Classes="accent"
Command="{Binding SearchModelsCommand}"
IsDefault="True">
<Grid>
<controls:ProgressRing
MinWidth="16"
MinHeight="16"
VerticalAlignment="Center"
BorderThickness="4"
IsIndeterminate="True"
IsVisible="{Binding SearchModelsCommand.IsRunning}" />
<TextBlock
VerticalAlignment="Center"
IsVisible="{Binding !SearchModelsCommand.IsRunning}"
Text="{x:Static lang:Resources.Action_Search}" />
</Grid>
</Button>
</Grid>
<DockPanel>
<StackPanel Margin="0,8,4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_Sort}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllSortModes}"
SelectedItem="{Binding SortMode}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_TimePeriod}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllCivitPeriods}"
SelectedItem="{Binding SelectedPeriod}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_ModelType}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding AllModelTypes}"
SelectedItem="{Binding SelectedModelType}" />
</StackPanel>
<StackPanel Margin="4,8" Orientation="Vertical">
<Label Content="{x:Static lang:Resources.Label_BaseModel}" />
<ComboBox
MinWidth="100"
ItemsSource="{Binding BaseModelOptions}"
SelectedItem="{Binding SelectedBaseModelType}" />
</StackPanel>
<CheckBox
Margin="8,8,0,0"
HorizontalAlignment="Right"
Content="{x:Static lang:Resources.Label_ShowNsfwContent}"
IsChecked="{Binding ShowNsfw, Mode=TwoWay}" />
</DockPanel>
</StackPanel>
<ScrollViewer
Grid.Row="1"
ScrollChanged="ScrollViewer_OnScrollChanged">
<ItemsRepeater ItemTemplate="{StaticResource CivitModelTemplate}"
HorizontalAlignment="Center"
ItemsSource="{Binding ModelCards}">
<ItemsRepeater.Layout>
<UniformGridLayout Orientation="Horizontal" />
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>
<TextBlock
Grid.Row="2"
Margin="8,8"
VerticalAlignment="Center"
Text="{x:Static lang:Resources.Label_DataProvidedByCivitAi}" />
<StackPanel Grid.Row="2"
HorizontalAlignment="Center"
IsVisible="{Binding HasSearched}"
Margin="0,8,0,8"
Orientation="Horizontal">
<Button
Margin="0,0,8,0"
Command="{Binding FirstPage}"
IsEnabled="{Binding CanGoToFirstPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_FirstPage}">
<avalonia:Icon Value="fa-solid fa-backward-fast" />
</Button>
<Button
Margin="0,0,16,0"
Command="{Binding PreviousPage}"
IsEnabled="{Binding CanGoToPreviousPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_PreviousPage}">
<avalonia:Icon Value="fa-solid fa-caret-left" />
</Button>
<TextBlock Margin="8,0,4,0" TextAlignment="Center"
Text="{x:Static lang:Resources.Label_Page}"
VerticalAlignment="Center"/>
<ui:NumberBox Value="{Binding CurrentPageNumber, FallbackValue=1}"
VerticalAlignment="Center"
SpinButtonPlacementMode="Hidden"
TextAlignment="Center"/>
<TextBlock Margin="4,0,8,0" VerticalAlignment="Center">
<Run Text="/"/>
<Run Text="{Binding TotalPages, FallbackValue=5}"/>
</TextBlock>
<Button
Margin="16,0,8,0"
Command="{Binding NextPage}"
IsEnabled="{Binding CanGoToNextPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_NextPage}">
<avalonia:Icon Value="fa-solid fa-caret-right" />
</Button>
<Button
Command="{Binding LastPage}"
IsEnabled="{Binding CanGoToLastPage}"
ToolTip.Tip="{x:Static lang:Resources.Label_LastPage}">
<avalonia:Icon Value="fa-solid fa-forward-fast" />
</Button>
</StackPanel>
<TextBlock
Grid.Row="0"
Grid.RowSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
IsVisible="{Binding NoResultsFound}"
Text="{Binding NoResultsText, FallbackValue=No results found}" />
<controls:ProgressRing
Grid.Row="1"
Width="128"
Height="128"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsIndeterminate="True"
IsVisible="{Binding ShowMainLoadingSpinner, FallbackValue=False}" />
</Grid>
</controls:UserControlBase>

41
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();
}
}
}

2
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?"/>
<ui:InfoBar
Severity="Informational"

188
StabilityMatrix.Avalonia/Views/HuggingFacePage.axaml

@ -0,0 +1,188 @@
<controls:UserControlBase
x:Class="StabilityMatrix.Avalonia.Views.HuggingFacePage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:huggingFacePage="clr-namespace:StabilityMatrix.Avalonia.ViewModels.HuggingFacePage"
xmlns:helpers="clr-namespace:StabilityMatrix.Avalonia.Helpers"
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser"
d:DataContext="{x:Static mocks:DesignData.HuggingFacePageViewModel}"
d:DesignHeight="650"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="checkpointBrowser:HuggingFacePageViewModel"
Focusable="True"
mc:Ignorable="d">
<ScrollViewer>
<Grid RowDefinitions="Auto,Auto,Auto,Auto"
ColumnDefinitions="Auto, *"
Margin="8"
HorizontalAlignment="Stretch">
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
IsVisible="{Binding !!NumSelected}"
Margin="8,0,8,0">
<Run Text="{Binding NumSelected}" />
<Run Text="models selected" />
</TextBlock>
<ui:CommandBar
Grid.Row="0"
Grid.Column="0"
Margin="0,-1,0,0"
VerticalAlignment="Center"
HorizontalAlignment="Left"
VerticalContentAlignment="Center"
DefaultLabelPosition="Right">
<ui:CommandBar.PrimaryCommands>
<ui:CommandBarButton
IsEnabled="{Binding !!NumSelected}"
IconSource="Download"
Foreground="{DynamicResource AccentButtonBackground}"
Command="{Binding ImportSelectedCommand}"
Label="{x:Static lang:Resources.Action_Import}" />
<ui:CommandBarSeparator />
<ui:CommandBarButton
Command="{Binding SelectAll}"
IconSource="SelectAll"
Label="{x:Static lang:Resources.Action_SelectAll}" />
<ui:CommandBarButton
IconSource="ClearSelection"
Command="{Binding ClearSelection}"
IsEnabled="{Binding !!NumSelected}"
Label="{x:Static lang:Resources.Action_ClearSelection}" />
<ui:CommandBarSeparator IsVisible="{Binding !!NumSelected}" />
</ui:CommandBar.PrimaryCommands>
</ui:CommandBar>
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
Orientation="Vertical"
Margin="0,8"
VerticalAlignment="Bottom">
<TextBlock Text="{Binding DownloadPercentText}"
TextAlignment="Center"
IsVisible="{Binding !!TotalProgress.Total}"
Margin="0,4"
HorizontalAlignment="Stretch" />
<ProgressBar Value="{Binding TotalProgress.Current}"
Maximum="{Binding TotalProgress.Total}"
IsVisible="{Binding !!TotalProgress.Total}"
IsIndeterminate="False" />
</StackPanel>
<ItemsRepeater Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
ItemsSource="{Binding Categories}">
<ItemsRepeater.Layout>
<StackLayout Orientation="Vertical" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type huggingFacePage:CategoryViewModel}">
<Expander IsExpanded="True" Margin="0,0,0,8">
<Expander.Header>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsChecked}"
Content="{Binding Title}" />
</StackPanel>
</Expander.Header>
<ItemsRepeater ItemsSource="{Binding Items}"
Margin="8,0">
<ItemsRepeater.Layout>
<UniformGridLayout MinColumnSpacing="8"
MinRowSpacing="8"
MinItemWidth="220"
ItemsStretch="Uniform" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type huggingFacePage:HuggingfaceItemViewModel}">
<Button
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Command="{Binding ToggleSelectedCommand}"
CornerRadius="8">
<Button.ContextFlyout>
<MenuFlyout>
<MenuItem Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding RepoUrl}"
Header="{x:Static lang:Resources.Action_OpenOnHuggingFace}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{x:Static lang:Resources.Label_License}"
Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding LicenseUrl}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Document" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.ContextFlyout>
<Grid ColumnDefinitions="Auto, *">
<CheckBox
VerticalAlignment="Center"
IsChecked="{Binding IsSelected}"
Content="{Binding Item.ModelName}" />
<Button
Grid.Column="1"
Width="24"
Margin="0,4,4,0"
Padding="4"
HorizontalAlignment="Right"
VerticalAlignment="Top"
HorizontalContentAlignment="Right"
VerticalContentAlignment="Top"
BorderThickness="0"
Classes="transparent">
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
<Button.Flyout>
<MenuFlyout>
<MenuItem Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding RepoUrl}"
Header="{x:Static lang:Resources.Action_OpenOnHuggingFace}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Open" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{x:Static lang:Resources.Label_License}"
Command="{x:Static helpers:IOCommands.OpenUrlCommand}"
CommandParameter="{Binding LicenseUrl}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="List" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</Grid>
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Expander>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
<!-- <Expander Grid.Column="0" -->
<!-- Grid.ColumnSpan="2" -->
<!-- Grid.Row="3" -->
<!-- Header="Manual Model Download"> -->
<!-- <Grid RowDefinitions="Auto, Auto" ColumnDefinitions="*, Auto"> -->
<!-- <TextBox Grid.Row="1" Grid.Column="0" -->
<!-- Watermark="Enter HuggingFace repo URL" -->
<!-- Margin="0,8" /> -->
<!-- <Button Grid.Row="1" Grid.Column="1" -->
<!-- Margin="8,8,0,8" -->
<!-- Classes="accent" -->
<!-- Content="Import" /> -->
<!-- </Grid> -->
<!-- </Expander> -->
</Grid>
</ScrollViewer>
</controls:UserControlBase>

13
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();
}
}

43
StabilityMatrix.Core/Helper/FileHash.cs

@ -39,10 +39,7 @@ public static class FileHash
}
}
public static async Task<string> GetSha256Async(
string filePath,
IProgress<ProgressReport>? progress = default
)
public static async Task<string> GetSha256Async(string filePath, IProgress<ProgressReport>? 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<string> GetBlake3Async(
string filePath,
IProgress<ProgressReport>? progress = default
)
public static async Task<string> GetBlake3Async(string filePath, IProgress<ProgressReport>? 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<byte>.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<Hash> GetBlake3Async(
Stream stream,
IProgress<ProgressReport>? progress = default
)
public static async Task<Hash> GetBlake3Async(Stream stream, IProgress<ProgressReport>? 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));
}
/// <summary>
/// Determines suitable buffer size for hashing based on stream length.
/// </summary>
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
};
}

46
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
);

12
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<ConnectedModelInfo>(
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;
}

23
StabilityMatrix.Core/Models/Database/LocalModelFile.cs

@ -42,8 +42,7 @@ public class LocalModelFile
/// <summary>
/// Relative file path from the shared folder type model directory.
/// </summary>
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<string> SupportedCheckpointExtensions =
new() { ".safetensors", ".pt", ".ckpt", ".pth", ".bin" };
public static readonly HashSet<string> SupportedImageExtensions =
new() { ".png", ".jpg", ".jpeg" };
public static readonly HashSet<string> SupportedMetadataExtensions = new() { ".json" };
[
".safetensors",
".pt",
".ckpt",
".pth",
".bin"
];
public static readonly HashSet<string> SupportedImageExtensions = [".png", ".jpg", ".jpeg", ".gif"];
public static readonly HashSet<string> SupportedMetadataExtensions = [".json"];
}

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

@ -15,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();
@ -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<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
@ -62,7 +59,9 @@ public class A3WebUI : BaseGitPackage
[SharedFolderType.ControlNet] = new[] { "models/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" },
[SharedFolderType.IpAdapter] = new[] { "models/ipadapter" }
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
@ -78,21 +77,20 @@ public class A3WebUI : BaseGitPackage
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> 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()
{
@ -104,42 +102,42 @@ public class A3WebUI : BaseGitPackage
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()
{
@ -147,23 +145,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<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };

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

@ -17,7 +17,12 @@ 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";
@ -37,14 +42,6 @@ public class ComfyUI : BaseGitPackage
public override SharedFolderMethod RecommendedSharedFolderMethod => SharedFolderMethod.Configuration;
public ComfyUI(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
// https://github.com/comfyanonymous/ComfyUI/blob/master/folder_paths.py#L11
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
@ -60,29 +57,30 @@ public class ComfyUI : BaseGitPackage
[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" },
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
new() { [SharedOutputType.Text2Img] = new[] { "output" } };
public override List<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
{
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,
@ -92,50 +90,50 @@ public class ComfyUI : BaseGitPackage
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";
@ -241,13 +239,24 @@ 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)
@ -258,17 +267,6 @@ public class ComfyUI : BaseGitPackage
}
}
void HandleExit(int i)
{
Debug.WriteLine($"Venv process exited with code {i}");
OnExit(i);
}
var args = $"\"{Path.Combine(installedPackagePath, command)}\" {arguments}";
VenvRunner?.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit);
}
public override Task SetupModelFolders(DirectoryPath installDirectory, SharedFolderMethod sharedFolderMethod)
{
switch (sharedFolderMethod)

15
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 FocusControlNet(
public class FocusControlNet(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
) : Fooocus(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;

9
StabilityMatrix.Core/Models/Packages/Fooocus.cs

@ -13,16 +13,13 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.Packages;
[Singleton(typeof(BasePackage))]
public class Fooocus : BaseGitPackage
{
public Fooocus(
public class Fooocus(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
) : BaseGitPackage(githubApi, settingsManager, downloadService, prerequisiteHelper)
{
public override string Name => "Fooocus";
public override string DisplayName { get; set; } = "Fooocus";
public override string Author => "lllyasviel";

19
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 FooocusMre(
public class FooocusMre(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
) : BaseGitPackage(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<ProcessOutput>? 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));

115
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<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };
@ -63,58 +61,48 @@ public class InvokeAI : BaseGitPackage
public override Dictionary<SharedFolderType, IReadOnlyList<string>> 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<SharedOutputType, IReadOnlyList<string>>? 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<LaunchOptionDefinition> LaunchOptions =>
new List<LaunchOptionDefinition>
{
new()
[
new LaunchOptionDefinition
{
Name = "Host",
Type = LaunchOptionType.String,
DefaultValue = "localhost",
Options = new List<string> { "--host" }
Options = ["--host"]
},
new()
new LaunchOptionDefinition
{
Name = "Port",
Type = LaunchOptionType.String,
DefaultValue = "9090",
Options = new List<string> { "--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<string> { "--allow-origins" }
Options = ["--allow-origins"]
},
new()
new LaunchOptionDefinition
{
Name = "Always use CPU",
Type = LaunchOptionType.Bool,
Options = new List<string> { "--always_use_cpu" }
Options = ["--always_use_cpu"]
},
new()
new LaunchOptionDefinition
{
Name = "Precision",
Type = LaunchOptionType.Bool,
Options = new List<string>
{
"--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<string> { "--free_gpu_mem" }
Options = ["--free_gpu_mem"]
},
LaunchOptionDefinition.Extras
};
];
public override IEnumerable<TorchVersion> 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<Argument>();
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 });
}
}

37
StabilityMatrix.Core/Models/Packages/KohyaSs.cs

@ -14,22 +14,14 @@ using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.Packages;
[Singleton(typeof(BasePackage))]
public class KohyaSs : BaseGitPackage
{
private readonly IPyRunner pyRunner;
public KohyaSs(
public class KohyaSs(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper,
IPyRunner pyRunner
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper)
IPyRunner runner
) : BaseGitPackage(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";
@ -58,59 +50,58 @@ public class KohyaSs : BaseGitPackage
public override IEnumerable<SharedFolderMethod> AvailableSharedFolderMethods => new[] { SharedFolderMethod.None };
public override List<LaunchOptionDefinition> LaunchOptions =>
new()
{
[
new LaunchOptionDefinition
{
Name = "Listen Address",
Type = LaunchOptionType.String,
DefaultValue = "127.0.0.1",
Options = new List<string> { "--listen" }
Options = ["--listen"]
},
new LaunchOptionDefinition
{
Name = "Port",
Type = LaunchOptionType.String,
Options = new List<string> { "--port" }
Options = ["--port"]
},
new LaunchOptionDefinition
{
Name = "Username",
Type = LaunchOptionType.String,
Options = new List<string> { "--username" }
Options = ["--username"]
},
new LaunchOptionDefinition
{
Name = "Password",
Type = LaunchOptionType.String,
Options = new List<string> { "--password" }
Options = ["--password"]
},
new LaunchOptionDefinition
{
Name = "Auto-Launch Browser",
Type = LaunchOptionType.Bool,
Options = new List<string> { "--inbrowser" }
Options = ["--inbrowser"]
},
new LaunchOptionDefinition
{
Name = "Share",
Type = LaunchOptionType.Bool,
Options = new List<string> { "--share" }
Options = ["--share"]
},
new LaunchOptionDefinition
{
Name = "Headless",
Type = LaunchOptionType.Bool,
Options = new List<string> { "--headless" }
Options = ["--headless"]
},
new LaunchOptionDefinition
{
Name = "Language",
Type = LaunchOptionType.String,
Options = new List<string> { "--language" }
Options = ["--language"]
},
LaunchOptionDefinition.Extras
};
];
public override async Task InstallPackage(
string installLocation,
@ -172,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(

20
StabilityMatrix.Core/Models/Packages/RuinedFooocus.cs

@ -1,34 +1,27 @@
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.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.Packages;
[Singleton(typeof(BasePackage))]
public class RuinedFooocus : Fooocus
{
public RuinedFooocus(
public class RuinedFooocus(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
) : Fooocus(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;
@ -44,8 +37,7 @@ public class RuinedFooocus : Fooocus
{
if (torchVersion == TorchVersion.Cuda)
{
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));

29
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<TorchVersion> 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");

44
StabilityMatrix.Core/Models/Packages/StableDiffusionUx.cs

@ -15,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();
@ -35,14 +40,6 @@ public class StableDiffusionUx : BaseGitPackage
public override PackageDifficulty InstallerSortOrder => PackageDifficulty.Simple;
public StableDiffusionUx(
IGithubApiCache githubApi,
ISettingsManager settingsManager,
IDownloadService downloadService,
IPrerequisiteHelper prerequisiteHelper
)
: base(githubApi, settingsManager, downloadService, prerequisiteHelper) { }
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{
@ -77,21 +74,20 @@ public class StableDiffusionUx : BaseGitPackage
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> 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()
{
@ -103,66 +99,66 @@ public class StableDiffusionUx : BaseGitPackage
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<SharedFolderMethod> AvailableSharedFolderMethods =>
new[] { SharedFolderMethod.Symlink, SharedFolderMethod.None };

42
StabilityMatrix.Core/Models/Packages/VladAutomatic.cs

@ -17,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();
@ -39,14 +44,6 @@ public class VladAutomatic : BaseGitPackage
public override IEnumerable<TorchVersion> 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<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
@ -85,21 +82,20 @@ public class VladAutomatic : BaseGitPackage
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
public override List<LaunchOptionDefinition> 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()
{
@ -111,61 +107,61 @@ public class VladAutomatic : BaseGitPackage
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 => "";

36
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<SharedFolderType, IReadOnlyList<string>> 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(

8
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,
}

18
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<ProgressReport>? progress = null);
Task<ConnectedModelInfo?> GetMetadataForFile(
FilePath filePath,
IProgress<ProgressReport>? progress = null,
bool forceReimport = false
);
Task UpdateExistingMetadata(DirectoryPath directory, IProgress<ProgressReport>? progress = null);
}

266
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<MetadataImportService> logger,
IDownloadService downloadService,
ModelFinder modelFinder
) : IMetadataImportService
{
public async Task ScanDirectoryForMissingInfo(DirectoryPath directory, IProgress<ProgressReport>? 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<ProgressReport>(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<ProgressReport>? progress = null)
{
progress?.Report(new ProgressReport(-1f, "Scanning directory...", isIndeterminate: true));
var cmInfoList = new Dictionary<FilePath, ConnectedModelInfo>();
foreach (var cmInfoPath in directory.EnumerateFiles("*.cm-info.json", SearchOption.AllDirectories))
{
var cmInfo = JsonSerializer.Deserialize<ConnectedModelInfo>(
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<ConnectedModelInfo?> GetMetadataForFile(
FilePath filePath,
IProgress<ProgressReport>? 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<ProgressReport>(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<string?> GetBlake3Hash(
FilePath? cmInfoPath,
FilePath checkpointFilePath,
IProgress<ProgressReport> hashProgress
)
{
if (string.IsNullOrWhiteSpace(cmInfoPath?.ToString()) || !File.Exists(cmInfoPath))
{
return await FileHash.GetBlake3Async(checkpointFilePath, hashProgress).ConfigureAwait(false);
}
var cmInfo = JsonSerializer.Deserialize<ConnectedModelInfo>(
await cmInfoPath.ReadAllTextAsync().ConfigureAwait(false)
);
return cmInfo?.Hashes.BLAKE3;
}
private Task DownloadImage(CivitImage image, FilePath modelFilePath, IProgress<ProgressReport>? 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);
}
}

6
StabilityMatrix.Core/Services/SettingsManager.cs

@ -20,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");
@ -605,6 +606,7 @@ public class SettingsManager : ISettingsManager
settingsFile.WriteAllText(settingsJson);
Loaded?.Invoke(this, EventArgs.Empty);
isLoaded = true;
return;
}
@ -621,6 +623,7 @@ public class SettingsManager : ISettingsManager
)
{
Settings = loadedSettings;
isLoaded = true;
}
Loaded?.Invoke(this, EventArgs.Empty);
@ -644,6 +647,9 @@ public class SettingsManager : ISettingsManager
settingsFile.Create();
}
if (!isLoaded)
return;
var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(Settings, SettingsSerializerContext.Default.Settings);
File.WriteAllBytes(SettingsPath, jsonBytes);

Loading…
Cancel
Save